Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 2d4329f

Browse files
committed
refactor(CDropdown): improve arrow keys handling
1 parent 07ffa62 commit 2d4329f

File tree

3 files changed

+121
-69
lines changed

3 files changed

+121
-69
lines changed

‎packages/coreui-react/src/components/dropdown/CDropdown.tsx

Lines changed: 107 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

‎packages/coreui-react/src/components/dropdown/CDropdownContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface CDropdownContextProps {
88
dropdownMenuRef: RefObject<HTMLDivElement | HTMLUListElement | null>
99
dropdownToggleRef: (node: HTMLElement | null) => void
1010
handleHide?: () => void
11-
handleShow?: () => void
11+
handleShow?: (event?: KeyboardEvent) => void
1212
popper?: boolean
1313
portal?: boolean
1414
variant?: 'btn-group' | 'dropdown' | 'input-group' | 'nav-item'

‎packages/coreui-react/src/components/dropdown/CDropdownToggle.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@ export interface CDropdownToggleProps extends Omit<CButtonProps, 'type'> {
1818
*/
1919
custom?: boolean
2020
/**
21-
* If a dropdown `variant` is set to `nav-item` then render the toggler as a link instead of a button.
21+
* If a dropdown `variant` is set to `nav-item` then render the toggler as a
22+
* link instead of a button.
2223
*
2324
* @since 5.0.0
2425
*/
2526
navLink?: boolean
2627
/**
27-
* Similarly, create split button dropdowns with virtually the same markup as single button dropdowns, but with the addition of `.dropdown-toggle-split` className for proper spacing around the dropdown caret.
28+
* Similarly, create split button dropdowns with virtually the same markup as
29+
* single button dropdowns, but with the addition of `.dropdown-toggle-split`
30+
* className for proper spacing around the dropdown caret.
2831
*/
2932
split?: boolean
3033
/**
31-
* Sets which event handlers you’d like provided to your toggle prop. You can specify one trigger or an array of them.
34+
* Sets which event handlers you'd like provided to your toggle prop. You can
35+
* specify one trigger or an array of them.
3236
*
3337
* @type 'hover' | 'focus' | 'click'
3438
*/
@@ -64,6 +68,12 @@ export const CDropdownToggle: FC<CDropdownToggleProps> = ({
6468
onFocus: () => handleShow?.(),
6569
onBlur: () => handleHide?.(),
6670
}),
71+
onKeyDown: (event: KeyboardEvent) => {
72+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
73+
event.preventDefault()
74+
handleShow?.(event)
75+
}
76+
},
6777
}
6878

6979
const togglerProps = {

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /