Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dropdown: Event callbacks and and floating props please #1515

Open
2 tasks done
lancegliser opened this issue Dec 3, 2024 · 1 comment
Open
2 tasks done

Dropdown: Event callbacks and and floating props please #1515

lancegliser opened this issue Dec 3, 2024 · 1 comment

Comments

@lancegliser
Copy link

lancegliser commented Dec 3, 2024

  • I have searched the Issues to see if this bug has already been reported
  • I have tested the latest version

Summary

Describe how it should work, and provide examples of the solution, which might include screenshots or code snippets.

<Dropdown /> should have exposed just a few properties and I'd have not ended up double bag it.

I think I'd need:

  • On close callback
  • Option to use <FloatingPortal />
  • useBaseFloating, useFloatingInteractions
  • Base button

Context

What are you trying to accomplish? How is your use case affected by not having this feature?

I've needed to implement a context menu (triple dot) as the last column in a table's row. The last row's <Dropdown /> can have it's elements cut off by the table (or any container's) overflow, or worse cause scrolling if it's overflow-x-auto. I need a way to remove it from dom, portaling, floating etc to achieve the below:

image

It should have been so much easier.

Here's a quick hack I'm using to get around the current implementation:

import { FloatingPortal, useFloating } from "@floating-ui/react";
import { Dropdown, type DropdownProps } from "flowbite-react";
import { useEffect, useId, useState, type FC, type ReactNode } from "react";
import { useBaseFloating } from "../../../hooks";

export type DropdownPortalProps = Omit<DropdownProps, "label" | "trigger"> & {
  /** Elements to act as trigger. You must supply your own <button>, unlike standard <Dropdown label /> */
  label: ReactNode;
  "data-testid"?: string;
};
export const DropdownPortal: FC<DropdownPortalProps> = ({
  label,
  ...props
}) => {
  const { refs, floatingStyles } = useBaseFloating({
    placement: "bottom",
  });
  const id = useId();

  const [isOpen, setIsOpen] = useState(false);
  const handleOpen = () => {
    setIsOpen(true);
    setDropdownState(true);
  };
  const handleClose = () => {
    setIsOpen(false);
    setDropdownState(false);
  };
  const setDropdownState = (isOpen: boolean) => {
    const dropdownTrigger = refs.floating.current?.querySelector("button");
    if (!dropdownTrigger) return;

    const isExpanded = dropdownTrigger.getAttribute("aria-expanded") === "true";
    if (isExpanded !== isOpen) {
      dropdownTrigger.click();
    }
  };

  // Create a click outside listener sync between the trigger's state and our isOpen.
  // Click outside events can cause the dropdown to fire, without us being made aware.
  // <Dropdown /> offers no event hooks to broadcast it's state handling this.
  useEffect(() => {
    if (!isOpen) return;

    const onDocumentClick = (event: MouseEvent) => {
      const reference = refs.reference.current;
      const floating = refs.floating.current;
      if (!reference || !floating) return;

      if (
        // @ts-expect-error Meh
        !reference.contains(event.target) &&
        // @ts-expect-error Meh
        !floating.contains(event.target)
      ) {
        setIsOpen(false);
      }
    };
    document.addEventListener("click", onDocumentClick);
    return () => {
      document.removeEventListener("click", onDocumentClick);
    };
  }, [isOpen, refs.floating, refs.reference]);

  return (
    <>
      <div
        aria-controls={id}
        aria-haspopup="menu"
        aria-expanded={isOpen}
        ref={refs.setReference}
        onClick={isOpen ? handleClose : handleOpen}
        data-testid="flowbite-dropdown-portal"
      >
        {label}
      </div>
      <FloatingPortal>
        <div
          aria-expanded={isOpen}
          data-testid="flowbite-dropdown-portal"
          id={id}
          ref={refs.setFloating}
          style={floatingStyles}
        >
          <Dropdown
            label={null}
            placement="bottom"
            inline
            arrowIcon={false}
            {...props}
          />
        </div>
      </FloatingPortal>
    </>
  );
};
@lancegliser
Copy link
Author

If it helps, here's the story file for reproducing:

import type { Meta, StoryObj } from "@storybook/react";
import { Dropdown } from "flowbite-react";
import { PrimaryButton } from "../../Buttons";
import { ViewIcon } from "../../Icons";
import { DropdownPortal } from "./DropdownPortal";

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
const meta: Meta<typeof DropdownPortal> = {
  component: DropdownPortal,
  // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/7.0/react/writing-docs/docs-page
  tags: ["autodocs"],
  parameters: {
    // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
    layout: "centered",
  },
  // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
  argTypes: {},
  args: {
    children: (
      <>
        <Dropdown.Item icon={ViewIcon}>A</Dropdown.Item>
        <Dropdown.Item icon={ViewIcon}>B</Dropdown.Item>
        <Dropdown.Item icon={ViewIcon}>C</Dropdown.Item>
        <Dropdown.Item icon={ViewIcon}>D</Dropdown.Item>
        <Dropdown.Item icon={ViewIcon}>F</Dropdown.Item>
      </>
    ),
    label: <PrimaryButton>Dropdown</PrimaryButton>,
  },
};

export default meta;
type Story = StoryObj<typeof DropdownPortal>;

// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
export const Default: Story = {
  // More on args: https://storybook.js.org/docs/react/writing-stories/args
  args: {},
};

export const OverflowXAuto: Story = {
  args: {},
  render: (args) => (
    <div className="h-8 w-40 overflow-x-auto bg-orange-500">
      <DropdownPortal {...args} />
    </div>
  ),
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant