Dropdown: Event callbacks and and floating props please
- [X] I have searched the Issues to see if this bug has already been reported
- [X] 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:
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>
</>
);
};
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>
),
};
Any updates to this or workarounds. Facing this issue in table action button.
using flowbite-react table and dropdown component.