-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathuseActionMenu.hook.ts
More file actions
124 lines (110 loc) · 3.77 KB
/
Copy pathuseActionMenu.hook.ts
File metadata and controls
124 lines (110 loc) · 3.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { ChangeEvent, createRef, RefObject, useEffect, useRef, useState } from 'react'
import { usePopover, UsePopoverProps } from '../Popover'
import { UseActionMenuProps } from './types'
import { filterActionMenuOptions, getActionMenuFlatOptions } from './utils'
export const useActionMenu = <T extends string | number>({
id,
position = 'bottom',
alignment = 'start',
width = 'auto',
options,
isSearchable,
onOpen,
}: UseActionMenuProps<T>) => {
// STATES
const [focusedIndex, setFocusedIndex] = useState(-1)
const [searchTerm, setSearchTerm] = useState('')
// CONSTANTS
const filteredOptions = isSearchable ? filterActionMenuOptions(options, searchTerm) : options
const flatOptions = getActionMenuFlatOptions(filteredOptions)
// REFS
const itemsRef = useRef<RefObject<HTMLAnchorElement | HTMLButtonElement>[]>(
flatOptions.map(() => createRef<HTMLAnchorElement | HTMLButtonElement>()),
)
useEffect(() => {
itemsRef.current = flatOptions.map(() => createRef<HTMLAnchorElement | HTMLButtonElement>())
}, [flatOptions.length])
// HANDLERS
const handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value)
}
const getNextIndex = (start: number, arrowDirection: 1 | -1) => {
let index = start
const totalOptions = flatOptions.length
for (let i = 0; i < totalOptions; i++) {
index = (index + arrowDirection + totalOptions) % totalOptions
if (!flatOptions[index]?.option?.isDisabled) {
return index
}
}
return start
}
const handlePopoverKeyDown: UsePopoverProps['onPopoverKeyDown'] = (e, openState, closePopover) => {
e.stopPropagation()
if (openState) {
switch (e.key) {
case 'Escape':
e.preventDefault()
closePopover()
break
case 'ArrowDown':
e.preventDefault()
setFocusedIndex((i) => getNextIndex(i, 1))
break
case 'ArrowUp':
e.preventDefault()
setFocusedIndex((i) => getNextIndex(i, -1))
break
case 'Enter':
case ' ': {
e.preventDefault()
const selectedItem = flatOptions[focusedIndex].option
const selectedItemRef = itemsRef.current[focusedIndex].current
if (!selectedItem.isDisabled && selectedItemRef) {
selectedItemRef.click()
}
break
}
default:
}
}
}
const handleTriggerKeyDown: UsePopoverProps['onTriggerKeyDown'] = (e, openState, closePopover) => {
if (!openState && (e.key === 'Enter' || e.key === ' ')) {
setFocusedIndex(0)
}
handlePopoverKeyDown(e, openState, closePopover)
}
// POPOVER HOOK
const { open, closePopover, overlayProps, popoverProps, triggerProps, scrollableRef } = usePopover({
id,
position,
alignment,
width,
onOpen,
onPopoverKeyDown: handlePopoverKeyDown,
onTriggerKeyDown: handleTriggerKeyDown,
})
// CLEANING UP STATES AFTER ACTION MENU CLOSE
useEffect(() => {
if (!open) {
setFocusedIndex(-1)
setSearchTerm('')
}
}, [open])
return {
open,
flatOptions,
filteredOptions,
focusedIndex,
itemsRef,
triggerProps,
overlayProps,
popoverProps,
setFocusedIndex,
closePopover,
searchTerm,
handleSearch,
scrollableRef,
}
}