@@ -37,12 +37,15 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
3737 * </CDropdownMenu>
3838 * </CDropdown>
3939 *
40- * @type 'start' | 'end' | { xs: 'start' | 'end' } | { sm: 'start' | 'end' } | { md: 'start' | 'end' } | { lg: 'start' | 'end' } | { xl: 'start' | 'end'} | { xxl: 'start' | 'end'}
40+ * @type 'start' | 'end' | { xs: 'start' | 'end' } | { sm: 'start' | 'end' } |
41+ * { md: 'start' | 'end' } | { lg: 'start' | 'end' } | { xl: 'start' | 'end'} |
42+ * { xxl: 'start' | 'end'}
4143 */
4244 alignment ?: Alignments
4345
4446 /**
45- * Determines the root node component (native HTML element or a custom React component) for the React Dropdown.
47+ * Determines the root node component (native HTML element or a custom React
48+ * component) for the React Dropdown.
4649 */
4750 as ?: ElementType
4851
@@ -65,7 +68,8 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
6568 className ?: string
6669
6770 /**
68- * Appends the React Dropdown Menu to a specific element. You can pass an HTML element or a function returning an element. Defaults to `document.body`.
71+ * Appends the React Dropdown Menu to a specific element. You can pass an HTML
72+ * element or a function returning an element. Defaults to `document.body`.
6973 *
7074 * @example
7175 * // Append the menu to a custom container
@@ -78,7 +82,8 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
7882 container ?: DocumentFragment | Element | ( ( ) => DocumentFragment | Element | null ) | null
7983
8084 /**
81- * Applies a darker color scheme to the React Dropdown Menu, often used within dark navbars.
85+ * Applies a darker color scheme to the React Dropdown Menu, often used within
86+ * dark navbars.
8287 */
8388 dark ?: boolean
8489
@@ -88,7 +93,8 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
8893 direction ?: 'center' | 'dropup' | 'dropup-center' | 'dropend' | 'dropstart'
8994
9095 /**
91- * Defines x and y offsets ([x, y]) for the React Dropdown Menu relative to its target.
96+ * Defines x and y offsets ([x, y]) for the React Dropdown Menu relative to
97+ * its target.
9298 *
9399 * @example
94100 * // Offset the menu 10px in X and 5px in Y direction
@@ -111,19 +117,23 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
111117 onShow ?: ( ) => void
112118
113119 /**
114- * Determines the placement of the React Dropdown Menu after Popper.js modifiers.
120+ * Determines the placement of the React Dropdown Menu after Popper.js
121+ * modifiers.
115122 *
116123 * @type 'auto' | 'auto-start' | 'auto-end' | 'top-end' | 'top' | 'top-start' | 'bottom-end' | 'bottom' | 'bottom-start' | 'right-start' | 'right' | 'right-end' | 'left-start' | 'left' | 'left-end'
117124 */
118125 placement ?: Placements
119126
120127 /**
121- * Enables or disables dynamic positioning via Popper.js for the React Dropdown Menu.
128+ * Enables or disables dynamic positioning via Popper.js for the React
129+ * Dropdown Menu.
122130 */
123131 popper ?: boolean
124132
125133 /**
126- * Provides a custom Popper.js configuration or a function that returns a modified Popper.js configuration for advanced positioning of the React Dropdown Menu. [Read more](https://popper.js.org/docs/v2/constructors/#options)
134+ * Provides a custom Popper.js configuration or a function that returns a
135+ * modified Popper.js configuration for advanced positioning of the React
136+ * Dropdown Menu. [Read more](https://popper.js.org/docs/v2/constructors/#options)
127137 *
128138 * @example
129139 * // Providing a custom popper config
@@ -143,7 +153,8 @@ export interface CDropdownProps extends HTMLAttributes<HTMLDivElement | HTMLLIEl
143153 popperConfig ?: Partial < Options > | ( ( defaultPopperConfig : Partial < Options > ) => Partial < Options > )
144154
145155 /**
146- * Renders the React Dropdown Menu using a React Portal, allowing it to escape the DOM hierarchy for improved positioning.
156+ * Renders the React Dropdown Menu using a React Portal, allowing it to escape
157+ * the DOM hierarchy for improved positioning.
147158 *
148159 * @since 4.8.0
149160 */
@@ -202,6 +213,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
202213 const dropdownMenuRef = useRef < HTMLDivElement | HTMLUListElement > ( null )
203214 const forkedRef = useForkedRef ( ref , dropdownRef )
204215 const [ dropdownToggleElement , setDropdownToggleElement ] = useState < HTMLElement | null > ( null )
216+ const [ pendingKeyDownEvent , setPendingKeyDownEvent ] = useState < KeyboardEvent | null > ( null )
205217 const [ _visible , setVisible ] = useState ( visible )
206218 const { initPopper, destroyPopper } = usePopper ( )
207219
@@ -249,29 +261,14 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
249261 }
250262 } , [ dropdownToggleElement ] )
251263
252- const handleShow = ( ) => {
253- const toggleElement = dropdownToggleElement
254- const menuElement = dropdownMenuRef . current
255-
256- if ( toggleElement && menuElement ) {
257- setVisible ( true )
258-
259- if ( allowPopperUse ) {
260- initPopper ( toggleElement , menuElement , computedPopperConfig )
261- }
262-
263- toggleElement . focus ( )
264- toggleElement . addEventListener ( 'keydown' , handleKeydown )
265- menuElement . addEventListener ( 'keydown' , handleKeydown )
266-
267- window . addEventListener ( 'mouseup' , handleMouseUp )
268- window . addEventListener ( 'keyup' , handleKeyup )
269-
270- onShow ?.( )
264+ useEffect ( ( ) => {
265+ if ( pendingKeyDownEvent !== null ) {
266+ handleKeydown ( pendingKeyDownEvent )
267+ setPendingKeyDownEvent ( null )
271268 }
272- }
269+ } , [ pendingKeyDownEvent ] )
273270
274- const handleHide = ( ) => {
271+ const handleHide = useCallback ( ( ) => {
275272 setVisible ( false )
276273
277274 const toggleElement = dropdownToggleElement
@@ -288,51 +285,96 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
288285 window . removeEventListener ( 'keyup' , handleKeyup )
289286
290287 onHide ?.( )
291- }
288+ } , [ dropdownToggleElement , allowPopperUse , destroyPopper , onHide ] )
292289
293- const handleKeydown = ( event : KeyboardEvent ) => {
294- if (
295- _visible &&
296- dropdownMenuRef . current &&
297- ( event . key === 'ArrowDown' || event . key === 'ArrowUp' )
298- ) {
290+ const handleKeydown = useCallback ( ( event : KeyboardEvent ) => {
291+ if ( dropdownMenuRef . current && ( event . key === 'ArrowDown' || event . key === 'ArrowUp' ) ) {
299292 event . preventDefault ( )
300293 const target = event . target as HTMLElement
301- const items : HTMLElement [ ] = Array . from (
302- dropdownMenuRef . current . querySelectorAll ( '.dropdown-item:not(.disabled):not(:disabled)' )
303- )
294+ const items = [
295+ ...dropdownMenuRef . current . querySelectorAll (
296+ '.dropdown-item:not(.disabled):not(:disabled)'
297+ ) ,
298+ ] as HTMLElement [ ]
304299 getNextActiveElement ( items , target , event . key === 'ArrowDown' , true ) . focus ( )
305300 }
306- }
301+ } , [ ] )
307302
308- const handleKeyup = ( event : KeyboardEvent ) => {
309- if ( autoClose === false ) {
310- return
311- }
303+ const handleKeyup = useCallback (
304+ ( event : KeyboardEvent ) => {
305+ if ( autoClose === false ) {
306+ return
307+ }
312308
313- if ( event . key === 'Escape' ) {
314- handleHide ( )
315- }
316- }
309+ if ( event . key === 'Escape' ) {
310+ handleHide ( )
311+ dropdownToggleElement ?. focus ( )
312+ }
313+ } ,
314+ [ autoClose , handleHide ]
315+ )
317316
318- const handleMouseUp = ( event : Event ) => {
319- if ( ! dropdownToggleElement || ! dropdownMenuRef . current ) {
320- return
321- }
317+ const handleMouseUp = useCallback (
318+ ( event : Event ) => {
319+ if ( ! dropdownToggleElement || ! dropdownMenuRef . current ) {
320+ return
321+ }
322322
323- if ( dropdownToggleElement . contains ( event . target as HTMLElement ) ) {
324- return
325- }
323+ if ( dropdownToggleElement . contains ( event . target as HTMLElement ) ) {
324+ return
325+ }
326326
327- if (
328- autoClose === true ||
329- ( autoClose === 'inside' && dropdownMenuRef . current . contains ( event . target as HTMLElement ) ) ||
330- ( autoClose === 'outside' && ! dropdownMenuRef . current . contains ( event . target as HTMLElement ) )
331- ) {
332- setTimeout ( ( ) => handleHide ( ) , 1 )
333- return
334- }
335- }
327+ if (
328+ autoClose === true ||
329+ ( autoClose === 'inside' &&
330+ dropdownMenuRef . current . contains ( event . target as HTMLElement ) ) ||
331+ ( autoClose === 'outside' &&
332+ ! dropdownMenuRef . current . contains ( event . target as HTMLElement ) )
333+ ) {
334+ setTimeout ( ( ) => handleHide ( ) , 1 )
335+ return
336+ }
337+ } ,
338+ [ autoClose , dropdownToggleElement , handleHide ]
339+ )
340+
341+ const handleShow = useCallback (
342+ ( event ?: KeyboardEvent ) => {
343+ const toggleElement = dropdownToggleElement
344+ const menuElement = dropdownMenuRef . current
345+
346+ if ( toggleElement && menuElement ) {
347+ setVisible ( true )
348+
349+ if ( allowPopperUse ) {
350+ initPopper ( toggleElement , menuElement , computedPopperConfig )
351+ }
352+
353+ toggleElement . focus ( )
354+ toggleElement . addEventListener ( 'keydown' , handleKeydown )
355+ menuElement . addEventListener ( 'keydown' , handleKeydown )
356+
357+ window . addEventListener ( 'mouseup' , handleMouseUp )
358+ window . addEventListener ( 'keyup' , handleKeyup )
359+
360+ if ( event && ( event . key === 'ArrowDown' || event . key === 'ArrowUp' ) ) {
361+ setPendingKeyDownEvent ( event )
362+ }
363+
364+ onShow ?.( )
365+ }
366+ } ,
367+ [
368+ dropdownToggleElement ,
369+ allowPopperUse ,
370+ initPopper ,
371+ computedPopperConfig ,
372+ handleKeydown ,
373+ handleMouseUp ,
374+ handleKeyup ,
375+ onShow ,
376+ ]
377+ )
336378
337379 const contextValues = {
338380 alignment,
0 commit comments