11import * as React from "react" ;
2- import { createPortal } from "react-dom " ;
2+ import { Menu } from "@headlessui/ react" ;
33import { cn } from "../../utils/cn" ;
44import { ZINDEX } from "../../utils/z-index" ;
55
66export interface DropdownProps {
77 trigger : React . ReactNode ;
88 children : React . ReactNode ;
99 align ?: "left" | "right" ;
10- direction ?: "down" | "up" ;
11- variant ?: "default" | "nav" ;
1210 className ?: string ;
1311}
1412
1513export const Dropdown : React . FC < DropdownProps > = ( {
1614 trigger,
1715 children,
1816 align = "left" ,
19- direction = "down" ,
20- variant = "default" ,
2117 className,
2218} ) => {
23- const [ isOpen , setIsOpen ] = React . useState ( false ) ;
24- const [ position , setPosition ] = React . useState < { top ?: number ; bottom ?: number ; left : number ; right : number } > ( { left : 0 , right : 0 } ) ;
25- const dropdownRef = React . useRef < HTMLDivElement > ( null ) ;
26- const triggerRef = React . useRef < HTMLDivElement > ( null ) ;
27-
28- React . useEffect ( ( ) => {
29- const handleClickOutside = ( event : MouseEvent ) => {
30- if ( dropdownRef . current && ! dropdownRef . current . contains ( event . target as Node ) ) {
31- setIsOpen ( false ) ;
32- }
33- } ;
34-
35- const handleEscape = ( event : KeyboardEvent ) => {
36- if ( event . key === 'Escape' ) {
37- setIsOpen ( false ) ;
38- }
39- } ;
40-
41- document . addEventListener ( "mousedown" , handleClickOutside ) ;
42- if ( variant === "nav" ) {
43- document . addEventListener ( "keydown" , handleEscape ) ;
44- }
45-
46- return ( ) => {
47- document . removeEventListener ( "mousedown" , handleClickOutside ) ;
48- if ( variant === "nav" ) {
49- document . removeEventListener ( "keydown" , handleEscape ) ;
50- }
51- } ;
52- } , [ variant ] ) ;
53-
54- // Update position when scrolling or resizing
55- React . useEffect ( ( ) => {
56- if ( ! isOpen ) return ;
57-
58- const updatePosition = ( ) => {
59- if ( ! triggerRef . current ) return ;
60- const rect = triggerRef . current . getBoundingClientRect ( ) ;
61-
62- setPosition ( {
63- left : rect . left ,
64- right : window . innerWidth - rect . right ,
65- ...( direction === 'down'
66- ? { top : rect . bottom + 8 }
67- : { bottom : window . innerHeight - rect . top + 8 }
68- ) ,
69- } ) ;
70- } ;
71-
72- updatePosition ( ) ;
73- window . addEventListener ( 'scroll' , updatePosition , true ) ;
74- window . addEventListener ( 'resize' , updatePosition ) ;
75-
76- return ( ) => {
77- window . removeEventListener ( 'scroll' , updatePosition , true ) ;
78- window . removeEventListener ( 'resize' , updatePosition ) ;
79- } ;
80- } , [ isOpen , direction ] ) ;
81-
82- const triggerClasses = cn ( {
83- "cursor-pointer" : true ,
84- "flex items-center gap-1 text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200 transition-colors" :
85- variant === "nav" ,
86- } ) ;
87-
88- const menuClasses = cn (
89- "absolute rounded-lg shadow-lg min-w-[8rem] py-1" ,
90- variant === "default" && [
91- "bg-white dark:bg-gray-900" ,
92- "border border-gray-200 dark:border-gray-800" ,
93- ] ,
94- variant === "nav" && [
95- "bg-white dark:bg-gray-800" ,
96- "border border-gray-200 dark:border-gray-700" ,
97- "w-48 rounded-md" ,
98- ] ,
99- {
100- "left-0" : align === "left" ,
101- "right-0" : align === "right" ,
102- "top-full mt-2" : direction === "down" ,
103- "bottom-full mb-2" : direction === "up" ,
104- } ,
105- className
106- ) ;
107-
10819 return (
109- < div className = "relative inline-block" ref = { dropdownRef } >
110- < div
111- ref = { triggerRef }
112- onClick = { ( ) => setIsOpen ( ! isOpen ) }
113- className = { triggerClasses }
114- >
20+ < Menu as = "div" className = "relative inline-block text-left" >
21+ < Menu . Button className = "inline-flex cursor-pointer" >
11522 { trigger }
116- </ div >
117- { isOpen && createPortal (
118- < div
119- className = { menuClasses }
120- style = { {
121- position : 'fixed' ,
122- ...position ,
123- zIndex : ZINDEX . dropdown ,
124- } }
125- >
126- { children }
127- </ div > ,
128- document . body
129- ) }
130- </ div >
23+ </ Menu . Button >
24+
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 >
42+ </ Menu >
13143 ) ;
13244} ;
13345
134- export interface DropdownItemProps extends React . ButtonHTMLAttributes < HTMLButtonElement > {
135- icon ?: React . ReactNode ;
46+ interface DropdownItemProps {
47+ children : React . ReactNode ;
48+ href ?: string ;
49+ onClick ?: ( ) => void ;
50+ className ?: string ;
13651}
13752
138- export const DropdownItem = React . forwardRef < HTMLButtonElement , DropdownItemProps > (
139- ( { className, children, icon, ...props } , ref ) => (
140- < button
141- ref = { ref }
142- className = { cn (
143- "flex w-full items-center px-3 py-2 text-sm gap-2" ,
144- "text-gray-700 dark:text-gray-200" ,
145- "hover:bg-gray-100 dark:hover:bg-gray-800" ,
146- "focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800" ,
147- className
148- ) }
149- { ...props }
150- >
151- { icon }
152- { children }
153- </ button >
154- )
53+ export const DropdownItem = React . forwardRef < HTMLDivElement , DropdownItemProps > (
54+ ( { children, href, onClick, className } , ref ) => {
55+ const content = (
56+ < div
57+ ref = { ref }
58+ className = { cn (
59+ "w-full px-4 py-2 text-sm" ,
60+ "text-gray-700 dark:text-gray-200" ,
61+ className
62+ ) }
63+ >
64+ { children }
65+ </ div >
66+ ) ;
67+
68+ return (
69+ < Menu . Item >
70+ { ( { active } ) =>
71+ href ? (
72+ < a
73+ href = { href }
74+ onClick = { onClick }
75+ className = { cn (
76+ "block w-full" ,
77+ active && "bg-gray-100 dark:bg-gray-700"
78+ ) }
79+ >
80+ { content }
81+ </ a >
82+ ) : (
83+ < button
84+ type = "button"
85+ onClick = { onClick }
86+ className = { cn (
87+ "block w-full text-left" ,
88+ active && "bg-gray-100 dark:bg-gray-700"
89+ ) }
90+ >
91+ { content }
92+ </ button >
93+ )
94+ }
95+ </ Menu . Item >
96+ ) ;
97+ }
15598) ;
99+
156100DropdownItem . displayName = "DropdownItem" ;
0 commit comments