|
1 | 1 | import * as React from "react"; |
2 | | -import { Menu } from "@headlessui/react"; |
| 2 | +import { Menu, Transition, Portal } from "@headlessui/react"; |
3 | 3 | import { cn } from "../../utils/cn"; |
4 | 4 | import { ZINDEX } from "../../utils/z-index"; |
5 | 5 |
|
6 | 6 | export interface DropdownProps { |
7 | 7 | trigger: React.ReactNode; |
8 | 8 | children: React.ReactNode; |
9 | 9 | align?: "left" | "right"; |
| 10 | + direction?: "up" | "down"; |
10 | 11 | className?: string; |
11 | 12 | } |
12 | 13 |
|
13 | 14 | export const Dropdown: React.FC<DropdownProps> = ({ |
14 | 15 | trigger, |
15 | 16 | children, |
16 | 17 | align = "left", |
| 18 | + direction = "down", |
17 | 19 | className, |
18 | 20 | }) => { |
| 21 | + const buttonRef = React.useRef<HTMLDivElement>(null); |
| 22 | + |
| 23 | + const updatePosition = React.useCallback((open: boolean) => { |
| 24 | + if (!buttonRef.current || !open) return; |
| 25 | + const rect = buttonRef.current.getBoundingClientRect(); |
| 26 | + |
| 27 | + document.documentElement.style.setProperty('--x', `${rect.left}px`); |
| 28 | + document.documentElement.style.setProperty('--rx', `${window.innerWidth - rect.right}px`); |
| 29 | + document.documentElement.style.setProperty('--y', `${rect.bottom + 8}px`); |
| 30 | + document.documentElement.style.setProperty('--by', `${window.innerHeight - rect.top + 8}px`); |
| 31 | + }, []); |
| 32 | + |
19 | 33 | return ( |
20 | | - <Menu as="div" className="relative inline-block text-left"> |
21 | | - <Menu.Button className="inline-flex cursor-pointer"> |
22 | | - {trigger} |
23 | | - </Menu.Button> |
| 34 | + <Menu> |
| 35 | + {({ open }) => ( |
| 36 | + <div className="relative inline-block text-left"> |
| 37 | + <div ref={buttonRef}> |
| 38 | + <Menu.Button |
| 39 | + className="inline-flex cursor-pointer" |
| 40 | + onClick={() => updatePosition(true)} |
| 41 | + > |
| 42 | + {trigger} |
| 43 | + </Menu.Button> |
| 44 | + </div> |
24 | 45 |
|
25 | | - <Menu.Items |
26 | | - className={cn( |
27 | | - "absolute mt-2 rounded-lg shadow-lg", |
28 | | - "bg-white dark:bg-gray-800", |
29 | | - "border border-gray-200 dark:border-gray-700", |
30 | | - "focus:outline-none", |
31 | | - "min-w-[8rem] py-1", |
32 | | - { |
33 | | - "left-0": align === "left", |
34 | | - "right-0": align === "right", |
35 | | - }, |
36 | | - className |
37 | | - )} |
38 | | - style={{ zIndex: ZINDEX.dropdown }} |
39 | | - > |
40 | | - {children} |
41 | | - </Menu.Items> |
| 46 | + <Portal> |
| 47 | + <Transition |
| 48 | + show={open} |
| 49 | + enter="transition duration-100 ease-out" |
| 50 | + enterFrom="transform scale-95 opacity-0" |
| 51 | + enterTo="transform scale-100 opacity-100" |
| 52 | + leave="transition duration-75 ease-out" |
| 53 | + leaveFrom="transform scale-100 opacity-100" |
| 54 | + leaveTo="transform scale-95 opacity-0" |
| 55 | + beforeEnter={() => updatePosition(true)} |
| 56 | + afterLeave={() => updatePosition(false)} |
| 57 | + > |
| 58 | + <Menu.Items |
| 59 | + className={cn( |
| 60 | + "fixed rounded-lg shadow-lg", |
| 61 | + "bg-white dark:bg-gray-800", |
| 62 | + "border border-gray-200 dark:border-gray-700", |
| 63 | + "focus:outline-none", |
| 64 | + "min-w-[8rem] py-1", |
| 65 | + "z-[100]", |
| 66 | + className |
| 67 | + )} |
| 68 | + style={{ |
| 69 | + left: align === "left" ? "var(--x)" : "unset", |
| 70 | + right: align === "right" ? "var(--rx)" : "unset", |
| 71 | + top: direction === "down" ? "var(--y)" : "unset", |
| 72 | + bottom: direction === "up" ? "var(--by)" : "unset", |
| 73 | + }} |
| 74 | + > |
| 75 | + {children} |
| 76 | + </Menu.Items> |
| 77 | + </Transition> |
| 78 | + </Portal> |
| 79 | + </div> |
| 80 | + )} |
42 | 81 | </Menu> |
43 | 82 | ); |
44 | 83 | }; |
|
0 commit comments