1- /** biome-ignore-all lint/a11y: no a11y for now */
2-
31import { Tippy } from '@/components/ui' ;
42import { ChevronDown } from 'lucide-react' ;
53import { createContext , useContext , useEffect , useRef , useState } from 'react' ;
64import { createPortal } from 'react-dom' ;
75
8- // Context
96type DropdownContextType = {
107 open : boolean ;
118 setOpen : React . Dispatch < React . SetStateAction < boolean > > ;
129 buttonRef : React . RefObject < HTMLButtonElement | null > ;
1310} ;
1411
15- const DropdownContext = createContext < DropdownContextType > ( {
16- open : false ,
17- setOpen : ( ) => { } ,
18- buttonRef : { current : null } ,
19- } ) ;
12+ const DropdownContext = createContext < DropdownContextType | null > ( null ) ;
13+
14+ function useDropdownContext ( ) {
15+ const context = useContext ( DropdownContext ) ;
16+ if ( ! context ) {
17+ throw new Error ( 'Dropdown components must be used within a Dropdown' ) ;
18+ }
19+ return context ;
20+ }
2021
21- // Dropdown Root
22+ // Dropdown Component
2223type DropdownProps = {
2324 children : React . ReactNode ;
2425} ;
2526
2627const Dropdown = ( { children } : DropdownProps ) => {
2728 const [ open , setOpen ] = useState ( false ) ;
28- const dropdownRef = useRef < HTMLDivElement > ( null ) ;
2929 const buttonRef = useRef < HTMLButtonElement > ( null ) ;
3030
3131 return (
3232 < DropdownContext . Provider value = { { open, setOpen, buttonRef } } >
33- < div ref = { dropdownRef } className = "dropdown" >
34- { children }
35- </ div >
33+ < div className = "dropdown" > { children } </ div >
3634 </ DropdownContext . Provider >
3735 ) ;
3836} ;
3937
4038// Dropdown Button
4139type DropdownButtonProps = {
4240 children : React . ReactNode ;
43- icon ?: React . ReactNode ;
4441} ;
4542
46- function DropdownButton ( { children } : DropdownButtonProps ) {
47- const { open, setOpen, buttonRef } = useContext ( DropdownContext ) ;
43+ const DropdownButton = ( { children } : DropdownButtonProps ) => {
44+ const { open, setOpen, buttonRef } = useDropdownContext ( ) ;
4845
4946 const toggleOpen = ( ) => setOpen ( ! open ) ;
5047
5148 return (
5249 < Tippy label = { open ? 'Close' : 'Open' } >
53- < button ref = { buttonRef } onClick = { toggleOpen } className = "dropdown-button" type = "button" >
50+ < button
51+ ref = { buttonRef }
52+ onClick = { ( e ) => {
53+ e . stopPropagation ( ) ;
54+ toggleOpen ( ) ;
55+ } }
56+ className = "dropdown-button"
57+ type = "button"
58+ >
5459 { children }
5560 < ChevronDown className = { `dropdown-icon ${ open ? 'rotate' : '' } ` } />
5661 </ button >
5762 </ Tippy >
5863 ) ;
59- }
64+ } ;
6065
66+ // Dropdown Content
6167type DropdownContentProps = {
6268 children : React . ReactNode ;
6369} ;
6470
65- function DropdownContent ( { children } : DropdownContentProps ) {
66- const { open, buttonRef, setOpen } = useContext ( DropdownContext ) ;
71+ const DropdownContent = ( { children } : DropdownContentProps ) => {
72+ const { open, buttonRef, setOpen } = useDropdownContext ( ) ;
6773 const contentRef = useRef < HTMLDivElement > ( null ) ;
6874 const [ coords , setCoords ] = useState ( { top : 0 , left : 0 , width : 0 } ) ;
6975 const [ ready , setReady ] = useState ( false ) ;
@@ -72,7 +78,6 @@ function DropdownContent({ children }: DropdownContentProps) {
7278 const updatePosition = ( ) => {
7379 const button = buttonRef . current ;
7480 const content = contentRef . current ;
75-
7681 if ( button && content ) {
7782 const buttonRect = button . getBoundingClientRect ( ) ;
7883 const contentWidth = content . offsetWidth || buttonRect . width ;
@@ -104,24 +109,22 @@ function DropdownContent({ children }: DropdownContentProps) {
104109 }
105110 } ;
106111
107- useEffect ( ( ) => {
108- document . body . classList . toggle ( 'dropdown-open' , open ) ;
109- } , [ open ] ) ;
110-
111112 useEffect ( ( ) => {
112113 if ( open ) {
113114 setReady ( false ) ;
114115 setShow ( false ) ;
115116 updatePosition ( ) ;
116-
117117 window . addEventListener ( 'resize' , updatePosition ) ;
118-
119118 return ( ) => {
120119 window . removeEventListener ( 'resize' , updatePosition ) ;
121120 } ;
122121 }
123122 } , [ open ] ) ;
124123
124+ useEffect ( ( ) => {
125+ document . body . classList . toggle ( 'dropdown-open' , open ) ;
126+ } , [ open ] ) ;
127+
125128 if ( ! open ) return null ;
126129
127130 return createPortal (
@@ -137,43 +140,51 @@ function DropdownContent({ children }: DropdownContentProps) {
137140 left : coords . left ,
138141 minWidth : coords . width ,
139142 } }
143+ onClick = { ( e ) => e . stopPropagation ( ) }
140144 >
141145 { children }
142146 </ div >
143147 </ div > ,
144148 document . body
145149 ) ;
146- }
150+ } ;
147151
148152// Dropdown List
149153type DropdownListProps = React . HTMLAttributes < HTMLUListElement > & {
150154 children : React . ReactNode ;
151155} ;
152156
153- function DropdownList ( { children, ...props } : DropdownListProps ) {
154- const { setOpen } = useContext ( DropdownContext ) ;
157+ const DropdownList = ( { children, ...props } : DropdownListProps ) => {
158+ const { setOpen } = useDropdownContext ( ) ;
155159
156160 return (
157- < ul onClick = { ( ) => setOpen ( false ) } className = "dropdown-list" { ...props } >
161+ < ul
162+ className = "dropdown-list"
163+ { ...props }
164+ onClick = { ( e ) => {
165+ e . stopPropagation ( ) ;
166+ setOpen ( false ) ;
167+ } }
168+ >
158169 { children }
159170 </ ul >
160171 ) ;
161- }
172+ } ;
162173
163174// Dropdown Item
164175type DropdownItemProps = React . ButtonHTMLAttributes < HTMLButtonElement > & {
165176 children : React . ReactNode ;
166177} ;
167178
168- function DropdownItem ( { children, ...props } : DropdownItemProps ) {
179+ const DropdownItem = ( { children, ...props } : DropdownItemProps ) => {
169180 return (
170181 < li className = "dropdown-item" >
171182 < button className = "dropdown-item-button" { ...props } >
172183 { children }
173184 </ button >
174185 </ li >
175186 ) ;
176- }
187+ } ;
177188
178189// Attach subcomponents
179190Dropdown . Button = DropdownButton ;
0 commit comments