themesberg / themesberg/flowbite-react

Dropdown: Event callbacks and and floating props please

Open
#1,515 3 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
2.1k
Forks
506
PR merge metrics
No merged PRs in 30d

Description

  • 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>
    </>
  );
};

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the Dropdown component API and the existing floating-related hooks, then compare them with the supplied DropdownPortal example. Review how the requested close callback, FloatingPortal option, floating hooks, and base button would fit together. Done means the supported API handles the context-menu and overflow use case without the custom wrapper.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.