react-animated-select 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/dist/index.cjs.js +12 -10
- package/dist/index.css +1 -1
- package/dist/index.es.js +1297 -1132
- package/index.d.ts +5 -1
- package/package.json +8 -4
- package/.github/workflows/publish.yml +0 -32
- package/demo/README.md +0 -16
- package/demo/eslint.config.js +0 -29
- package/demo/index.html +0 -13
- package/demo/package-lock.json +0 -3619
- package/demo/package.json +0 -35
- package/demo/public/vite.svg +0 -1
- package/demo/src/App.tsx +0 -437
- package/demo/src/main.jsx +0 -9
- package/demo/src/rac.css +0 -746
- package/demo/src/shake.js +0 -11
- package/demo/src/slideDown.jsx +0 -35
- package/demo/vite.config.js +0 -7
- package/src/animated.jsx +0 -80
- package/src/getText.jsx +0 -11
- package/src/icons.jsx +0 -43
- package/src/index.js +0 -5
- package/src/makeId.jsx +0 -21
- package/src/optgroup.jsx +0 -36
- package/src/option.jsx +0 -50
- package/src/options.jsx +0 -197
- package/src/select.css +0 -356
- package/src/select.jsx +0 -339
- package/src/selectContext.js +0 -3
- package/src/selectJSX.jsx +0 -265
- package/src/slideDown.jsx +0 -36
- package/src/slideLeft.jsx +0 -41
- package/src/useSelect.jsx +0 -186
- package/src/useSelectLogic.jsx +0 -413
- package/vite.config.js +0 -27
package/src/selectJSX.jsx
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import {memo, useCallback, useEffect, useRef} from 'react'
|
|
2
|
-
import {SelectContext} from './selectContext'
|
|
3
|
-
import Options from './options'
|
|
4
|
-
import SlideLeft from './slideLeft'
|
|
5
|
-
import Animated from './animated'
|
|
6
|
-
import {TransitionGroup} from 'react-transition-group'
|
|
7
|
-
|
|
8
|
-
const SelectedItem = memo(({element, index, remove, renderIcon, DelIcon, normalizedOptions}) => {
|
|
9
|
-
let label = null
|
|
10
|
-
|
|
11
|
-
if (element?.jsx) {
|
|
12
|
-
label = element.jsx
|
|
13
|
-
} else if (element?.name) {
|
|
14
|
-
label = element.name
|
|
15
|
-
} else if (element?.raw !== undefined) {
|
|
16
|
-
const recovered = normalizedOptions.find(o =>
|
|
17
|
-
o.raw === element.raw ||
|
|
18
|
-
o.original === element.raw ||
|
|
19
|
-
o.userId === element.raw
|
|
20
|
-
)
|
|
21
|
-
if (recovered) {
|
|
22
|
-
label = recovered.jsx ?? recovered.name
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (label == null) {
|
|
27
|
-
label = typeof element === 'object'
|
|
28
|
-
? (element.label ?? element.name ?? element.value ?? 'Selected item')
|
|
29
|
-
: String(element)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const handleDelete = useCallback((e) => {
|
|
33
|
-
e.stopPropagation()
|
|
34
|
-
e.preventDefault()
|
|
35
|
-
remove(element.id)
|
|
36
|
-
}, [element.id, remove])
|
|
37
|
-
|
|
38
|
-
const preventFocus = useCallback((e) => {
|
|
39
|
-
e.stopPropagation()
|
|
40
|
-
e.preventDefault()
|
|
41
|
-
}, [])
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<div className='rac-multiple-selected-option'>
|
|
45
|
-
{label}
|
|
46
|
-
{renderIcon(DelIcon, {onClick: handleDelete, onMouseDown: preventFocus})}
|
|
47
|
-
</div>
|
|
48
|
-
)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
const SelectJSX = memo(({
|
|
52
|
-
selectRef,
|
|
53
|
-
selectId,
|
|
54
|
-
removeOption,
|
|
55
|
-
renderOptions,
|
|
56
|
-
selected,
|
|
57
|
-
selectedIDs,
|
|
58
|
-
setSelectedIds,
|
|
59
|
-
normalizedOptions,
|
|
60
|
-
title,
|
|
61
|
-
visibility,
|
|
62
|
-
active,
|
|
63
|
-
hasOptions,
|
|
64
|
-
hasActualValue,
|
|
65
|
-
optionsClassName,
|
|
66
|
-
selectedText,
|
|
67
|
-
|
|
68
|
-
disabled,
|
|
69
|
-
loading,
|
|
70
|
-
error,
|
|
71
|
-
|
|
72
|
-
registerOption,
|
|
73
|
-
unregisterOption,
|
|
74
|
-
handleBlur,
|
|
75
|
-
handleFocus,
|
|
76
|
-
handleToggle,
|
|
77
|
-
handleKeyDown,
|
|
78
|
-
handleListScroll,
|
|
79
|
-
setAnimationFinished,
|
|
80
|
-
clear,
|
|
81
|
-
|
|
82
|
-
children,
|
|
83
|
-
placeholder,
|
|
84
|
-
className,
|
|
85
|
-
style,
|
|
86
|
-
duration,
|
|
87
|
-
easing,
|
|
88
|
-
offset,
|
|
89
|
-
animateOpacity,
|
|
90
|
-
unmount,
|
|
91
|
-
ArrowIcon,
|
|
92
|
-
ClearIcon,
|
|
93
|
-
DelIcon,
|
|
94
|
-
renderIcon,
|
|
95
|
-
hasMore,
|
|
96
|
-
loadButton
|
|
97
|
-
}) => {
|
|
98
|
-
|
|
99
|
-
const internalRef = useRef(null)
|
|
100
|
-
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (selectRef) {
|
|
103
|
-
if (typeof selectRef === 'function') selectRef(internalRef.current)
|
|
104
|
-
else selectRef.current = internalRef.current
|
|
105
|
-
}
|
|
106
|
-
}, [selectRef])
|
|
107
|
-
|
|
108
|
-
const remove = useCallback((id) => {
|
|
109
|
-
if (removeOption) {
|
|
110
|
-
removeOption(id)
|
|
111
|
-
} else {
|
|
112
|
-
setSelectedIds(prev => prev.filter(o => o.id !== id))
|
|
113
|
-
}
|
|
114
|
-
}, [removeOption, setSelectedIds])
|
|
115
|
-
|
|
116
|
-
const renderSelectIDs = selectedIDs?.map((element, index) => (
|
|
117
|
-
<Animated
|
|
118
|
-
key={element.id ?? index}
|
|
119
|
-
duration={duration}
|
|
120
|
-
widthMode
|
|
121
|
-
>
|
|
122
|
-
<SelectedItem
|
|
123
|
-
key={element.id ?? index}
|
|
124
|
-
element={element}
|
|
125
|
-
index={index}
|
|
126
|
-
remove={remove}
|
|
127
|
-
renderIcon={renderIcon}
|
|
128
|
-
DelIcon={DelIcon}
|
|
129
|
-
normalizedOptions={normalizedOptions}
|
|
130
|
-
/>
|
|
131
|
-
</Animated>
|
|
132
|
-
))
|
|
133
|
-
|
|
134
|
-
return (
|
|
135
|
-
<SelectContext.Provider
|
|
136
|
-
value={{registerOption, unregisterOption}}
|
|
137
|
-
>
|
|
138
|
-
{children}
|
|
139
|
-
<div
|
|
140
|
-
ref={internalRef}
|
|
141
|
-
style={{
|
|
142
|
-
...style,
|
|
143
|
-
'--rac-duration': `${duration}ms`,
|
|
144
|
-
'--rac-duration-fast': 'calc(var(--rac-duration) * 0.5)',
|
|
145
|
-
'--rac-duration-base': 'var(--rac-duration)',
|
|
146
|
-
'--rac-duration-slow': 'calc(var(--rac-duration) * 1.3)'
|
|
147
|
-
}}
|
|
148
|
-
className={
|
|
149
|
-
`rac-select
|
|
150
|
-
${className}
|
|
151
|
-
${(!hasOptions || disabled) ? 'rac-disabled-style' : ''}
|
|
152
|
-
${loading ? 'rac-loading-style' : ''}
|
|
153
|
-
${error ? 'rac-error-style' : ''}`}
|
|
154
|
-
tabIndex={active ? 0 : -1}
|
|
155
|
-
role='combobox'
|
|
156
|
-
aria-haspopup='listbox'
|
|
157
|
-
aria-expanded={visibility}
|
|
158
|
-
aria-controls={`${selectId}-listbox`}
|
|
159
|
-
aria-label={placeholder}
|
|
160
|
-
aria-disabled={disabled || !hasOptions}
|
|
161
|
-
{...(active && {
|
|
162
|
-
onBlur: handleBlur,
|
|
163
|
-
onFocus: handleFocus,
|
|
164
|
-
onClick: handleToggle,
|
|
165
|
-
onKeyDown: handleKeyDown
|
|
166
|
-
})}
|
|
167
|
-
>
|
|
168
|
-
<div
|
|
169
|
-
className={
|
|
170
|
-
`rac-select-title
|
|
171
|
-
${(!error && !loading && selected?.type === 'boolean')
|
|
172
|
-
?
|
|
173
|
-
(selected.raw ? 'rac-true-option' : 'rac-false-option')
|
|
174
|
-
: ''}
|
|
175
|
-
`}
|
|
176
|
-
>
|
|
177
|
-
<TransitionGroup component={null}>
|
|
178
|
-
{selectedIDs?.length && !selectedText ? (
|
|
179
|
-
renderSelectIDs
|
|
180
|
-
) : (
|
|
181
|
-
<Animated
|
|
182
|
-
key={title}
|
|
183
|
-
duration={duration}
|
|
184
|
-
widthMode
|
|
185
|
-
>
|
|
186
|
-
<span className='rac-title-text'>{title}</span>
|
|
187
|
-
|
|
188
|
-
<SlideLeft visibility={loading && !error} duration={duration}>
|
|
189
|
-
<span className='rac-loading-dots'><i/><i/><i/></span>
|
|
190
|
-
</SlideLeft>
|
|
191
|
-
</Animated>
|
|
192
|
-
)}
|
|
193
|
-
</TransitionGroup>
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
<div className='rac-select-buttons'>
|
|
197
|
-
<SlideLeft
|
|
198
|
-
visibility={hasActualValue && hasOptions && !disabled && !loading && !error}
|
|
199
|
-
duration={duration}
|
|
200
|
-
style={{display: 'grid'}}
|
|
201
|
-
>
|
|
202
|
-
{renderIcon(ClearIcon, {
|
|
203
|
-
className: 'rac-select-cancel',
|
|
204
|
-
onMouseDown: e => {
|
|
205
|
-
e.preventDefault()
|
|
206
|
-
e.stopPropagation()
|
|
207
|
-
},
|
|
208
|
-
onClick: clear
|
|
209
|
-
})}
|
|
210
|
-
</SlideLeft>
|
|
211
|
-
<SlideLeft
|
|
212
|
-
visibility={active}
|
|
213
|
-
duration={duration}
|
|
214
|
-
style={{display: 'grid'}}
|
|
215
|
-
>
|
|
216
|
-
<span
|
|
217
|
-
className={`rac-select-arrow-wrapper ${visibility ? '--open' : ''}`}
|
|
218
|
-
>
|
|
219
|
-
{renderIcon(ArrowIcon, {
|
|
220
|
-
className: 'rac-select-arrow-wrapper'
|
|
221
|
-
})}
|
|
222
|
-
</span>
|
|
223
|
-
</SlideLeft>
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
<Options
|
|
227
|
-
className={optionsClassName}
|
|
228
|
-
visibility={visibility}
|
|
229
|
-
selectRef={selectRef}
|
|
230
|
-
onAnimationDone={() => setAnimationFinished(true)}
|
|
231
|
-
unmount={unmount}
|
|
232
|
-
duration={duration}
|
|
233
|
-
easing={easing}
|
|
234
|
-
offset={offset}
|
|
235
|
-
animateOpacity={animateOpacity}
|
|
236
|
-
style={{
|
|
237
|
-
...style,
|
|
238
|
-
'--rac-duration': `${duration}ms`
|
|
239
|
-
}}
|
|
240
|
-
>
|
|
241
|
-
<div
|
|
242
|
-
onScroll={handleListScroll}
|
|
243
|
-
tabIndex='-1'
|
|
244
|
-
className='rac-select-list'
|
|
245
|
-
role='listbox'
|
|
246
|
-
aria-label='Options'
|
|
247
|
-
>
|
|
248
|
-
{renderOptions}
|
|
249
|
-
{!loadButton && hasMore && (
|
|
250
|
-
<div
|
|
251
|
-
className='rac-select-option rac-disabled-option rac-loading-option'
|
|
252
|
-
onClick={e => e.stopPropagation()}
|
|
253
|
-
>
|
|
254
|
-
<span className='rac-loading-option-title'>Loading</span>
|
|
255
|
-
<span className='rac-loading-dots'><i/><i/><i/></span>
|
|
256
|
-
</div>
|
|
257
|
-
)}
|
|
258
|
-
</div>
|
|
259
|
-
</Options>
|
|
260
|
-
</div>
|
|
261
|
-
</SelectContext.Provider>
|
|
262
|
-
)
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
export default SelectJSX
|
package/src/slideDown.jsx
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import {CSSTransition} from 'react-transition-group'
|
|
2
|
-
import {useRef} from 'react'
|
|
3
|
-
|
|
4
|
-
function SlideDown({visibility, children, duration = 300}) {
|
|
5
|
-
const nodeRef = useRef(null)
|
|
6
|
-
|
|
7
|
-
return(
|
|
8
|
-
<CSSTransition
|
|
9
|
-
in={visibility}
|
|
10
|
-
timeout={300}
|
|
11
|
-
classNames='slideDown'
|
|
12
|
-
unmountOnExit
|
|
13
|
-
nodeRef={nodeRef}
|
|
14
|
-
onEnter={() => (nodeRef.current.style.height = '0px')}
|
|
15
|
-
onEntering={() => (nodeRef.current.style.height = nodeRef.current.scrollHeight + 'px')}
|
|
16
|
-
onEntered={() => (nodeRef.current.style.height = 'auto')}
|
|
17
|
-
onExit={() => (nodeRef.current.style.height = nodeRef.current.scrollHeight + 'px')}
|
|
18
|
-
onExiting={() => (nodeRef.current.style.height = '0px')}
|
|
19
|
-
>
|
|
20
|
-
<div
|
|
21
|
-
ref={nodeRef}
|
|
22
|
-
style={{
|
|
23
|
-
overflow: 'hidden',
|
|
24
|
-
transition: `height ${duration}ms ease`,
|
|
25
|
-
paddingLeft: '1em'
|
|
26
|
-
}}
|
|
27
|
-
className='slideDown-enter-done'
|
|
28
|
-
tabIndex={-1}
|
|
29
|
-
>
|
|
30
|
-
{children}
|
|
31
|
-
</div>
|
|
32
|
-
</CSSTransition>
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export default SlideDown
|
package/src/slideLeft.jsx
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import {CSSTransition} from 'react-transition-group'
|
|
2
|
-
import {useRef} from 'react'
|
|
3
|
-
|
|
4
|
-
function SlideLeft({
|
|
5
|
-
visibility,
|
|
6
|
-
children,
|
|
7
|
-
duration = 300,
|
|
8
|
-
unmount,
|
|
9
|
-
style
|
|
10
|
-
}) {
|
|
11
|
-
const nodeRef = useRef(null)
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<CSSTransition
|
|
15
|
-
in={visibility}
|
|
16
|
-
timeout={duration}
|
|
17
|
-
classNames='rac-slide-left'
|
|
18
|
-
unmountOnExit
|
|
19
|
-
nodeRef={nodeRef}
|
|
20
|
-
onEnter={() => (nodeRef.current.style.width = '0px')}
|
|
21
|
-
onEntering={() => (nodeRef.current.style.width = nodeRef.current.scrollWidth + 'px')}
|
|
22
|
-
onEntered={() => (nodeRef.current.style.width = 'auto')}
|
|
23
|
-
onExit={() => (nodeRef.current.style.width = nodeRef.current.scrollWidth + 'px')}
|
|
24
|
-
onExiting={() => (nodeRef.current.style.width = '0px')}
|
|
25
|
-
onExited={() => unmount?.()}
|
|
26
|
-
>
|
|
27
|
-
<div
|
|
28
|
-
ref={nodeRef}
|
|
29
|
-
style={{
|
|
30
|
-
...style,
|
|
31
|
-
overflow: 'hidden',
|
|
32
|
-
transition: `width ${duration}ms ease`
|
|
33
|
-
}}
|
|
34
|
-
>
|
|
35
|
-
{children}
|
|
36
|
-
</div>
|
|
37
|
-
</CSSTransition>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export default SlideLeft
|
package/src/useSelect.jsx
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import {useState, useRef, useCallback, useEffect, useMemo} from 'react'
|
|
2
|
-
|
|
3
|
-
function useSelect({
|
|
4
|
-
disabled,
|
|
5
|
-
open,
|
|
6
|
-
setOpen,
|
|
7
|
-
options = [],
|
|
8
|
-
selectOption,
|
|
9
|
-
selected,
|
|
10
|
-
multiple,
|
|
11
|
-
hasMore,
|
|
12
|
-
loadMore,
|
|
13
|
-
loadButton,
|
|
14
|
-
loadButtonText,
|
|
15
|
-
setLoadingTitle,
|
|
16
|
-
loadOffset,
|
|
17
|
-
loadAhead,
|
|
18
|
-
expandedGroups
|
|
19
|
-
}) {
|
|
20
|
-
const justFocused = useRef(false)
|
|
21
|
-
const lastWindowFocusTime = useRef(0)
|
|
22
|
-
const loadingTriggered = useRef(false)
|
|
23
|
-
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
24
|
-
|
|
25
|
-
// loading state synchronization
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
// flag is reset if value of the loadButton or hasMore props has changed
|
|
28
|
-
loadingTriggered.current = false
|
|
29
|
-
|
|
30
|
-
if (loadButton) {
|
|
31
|
-
setLoadingTitle(loadButtonText)
|
|
32
|
-
}
|
|
33
|
-
}, [options.length, hasMore, loadButton, loadButtonText, setLoadingTitle])
|
|
34
|
-
|
|
35
|
-
// safely call loadMore prop
|
|
36
|
-
const safeLoadMore = useCallback(() => {
|
|
37
|
-
if (!hasMore || loadingTriggered.current) return
|
|
38
|
-
loadingTriggered.current = true
|
|
39
|
-
loadMore()
|
|
40
|
-
}, [hasMore, loadMore])
|
|
41
|
-
|
|
42
|
-
// calling a function when scrolling almost to the end;
|
|
43
|
-
// loadOffset is a prop indicating how many pixels before end loadMore will be called
|
|
44
|
-
const handleListScroll = useCallback((e) => {
|
|
45
|
-
if (loadButton || !hasMore || loadingTriggered.current) return
|
|
46
|
-
|
|
47
|
-
const {scrollTop, scrollHeight, clientHeight} = e.currentTarget
|
|
48
|
-
if (scrollHeight - scrollTop <= clientHeight + loadOffset) {
|
|
49
|
-
safeLoadMore()
|
|
50
|
-
}
|
|
51
|
-
}, [loadButton, hasMore, loadOffset, safeLoadMore])
|
|
52
|
-
|
|
53
|
-
// call a function when scrolling through options using keys;
|
|
54
|
-
// loadAhead prop how many options before the end it will be called
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
if (!loadButton && open && hasMore && highlightedIndex >= options.length - loadAhead) {
|
|
57
|
-
safeLoadMore()
|
|
58
|
-
}
|
|
59
|
-
}, [highlightedIndex, open, hasMore, options.length, loadAhead, loadButton, safeLoadMore])
|
|
60
|
-
|
|
61
|
-
// force refocus blocking if the user exits the browser or the page
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
const handleWindowFocus = () => {lastWindowFocusTime.current = Date.now()}
|
|
64
|
-
window.addEventListener('focus', handleWindowFocus)
|
|
65
|
-
return () => window.removeEventListener('focus', handleWindowFocus)
|
|
66
|
-
}, [])
|
|
67
|
-
|
|
68
|
-
// set highlighting to the first available option by default unless otherwise selected
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
if (!open) {
|
|
71
|
-
setHighlightedIndex(-1)
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// blocking the reset of an index if it is already within the array (exmpl after loading)
|
|
76
|
-
if (highlightedIndex >= 0 && highlightedIndex < options.length) {
|
|
77
|
-
if (!options[highlightedIndex] || options[highlightedIndex].hidden || options[highlightedIndex].groupHeader) {
|
|
78
|
-
} else return
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let index = -1
|
|
82
|
-
if (selected) {
|
|
83
|
-
const firstSelected = multiple ? selected[0] : selected
|
|
84
|
-
if (firstSelected) {
|
|
85
|
-
index = options.findIndex(o => o.id === firstSelected.id && !o.disabled && !o.hidden && !o.groupHeader)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (index === -1) {
|
|
90
|
-
index = options.findIndex(o => !o.disabled && !o.hidden && !o.groupHeader)
|
|
91
|
-
}
|
|
92
|
-
setHighlightedIndex(index)
|
|
93
|
-
}, [open, options, selected])
|
|
94
|
-
|
|
95
|
-
// find the next available option to switch to using the keyboard
|
|
96
|
-
const getNextIndex = useCallback((current, direction) => {
|
|
97
|
-
const isNavigable = (opt) =>
|
|
98
|
-
opt &&
|
|
99
|
-
!opt?.groupHeader &&
|
|
100
|
-
(!opt?.group || expandedGroups?.has(opt?.group)) &&
|
|
101
|
-
!opt?.disabled &&
|
|
102
|
-
!opt?.loading
|
|
103
|
-
const len = options.length
|
|
104
|
-
if (len === 0) return -1
|
|
105
|
-
|
|
106
|
-
let next = current
|
|
107
|
-
// я не шарю нихуя в математике
|
|
108
|
-
for (let i = 0; i < len; i++) {
|
|
109
|
-
next = (next + direction + len) % len
|
|
110
|
-
|
|
111
|
-
// if autoloading is active but loadButton is inactive, then infinite scrolling is blocked
|
|
112
|
-
if (!loadButton && hasMore) {
|
|
113
|
-
if (direction > 0 && next === 0) return current
|
|
114
|
-
if (direction < 0 && next === len - 1) return current
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (isNavigable(options[next])) return next
|
|
118
|
-
}
|
|
119
|
-
return current
|
|
120
|
-
}, [options, hasMore, loadButton, expandedGroups])
|
|
121
|
-
|
|
122
|
-
// closing the selector if focus is lost
|
|
123
|
-
const handleBlur = useCallback((e) => {
|
|
124
|
-
const clickedInsidePortal = e.relatedTarget?.closest('.rac-options')
|
|
125
|
-
|
|
126
|
-
if (!e.currentTarget.contains(e.relatedTarget) && !clickedInsidePortal) {
|
|
127
|
-
setOpen(false)
|
|
128
|
-
}
|
|
129
|
-
}, [setOpen])
|
|
130
|
-
|
|
131
|
-
// opening the selector when receiving focus
|
|
132
|
-
const handleFocus = useCallback(() => {
|
|
133
|
-
if (disabled || document.hidden || (Date.now() - lastWindowFocusTime.current < 100)) return
|
|
134
|
-
|
|
135
|
-
if (!open) {
|
|
136
|
-
setOpen(true)
|
|
137
|
-
justFocused.current = true
|
|
138
|
-
setTimeout(() => {justFocused.current = false}, 200)
|
|
139
|
-
}
|
|
140
|
-
}, [disabled, open, setOpen])
|
|
141
|
-
|
|
142
|
-
// processing toggle click on select
|
|
143
|
-
const handleToggle = useCallback((e) => {
|
|
144
|
-
if (disabled || e.target.closest('.rac-select-cancel') || justFocused.current) return
|
|
145
|
-
setOpen(!open)
|
|
146
|
-
}, [disabled, open, setOpen])
|
|
147
|
-
|
|
148
|
-
// hotkey processing
|
|
149
|
-
const handleKeyDown = useCallback((e) => {
|
|
150
|
-
if (disabled) return
|
|
151
|
-
|
|
152
|
-
switch (e.key) {
|
|
153
|
-
case 'Enter':
|
|
154
|
-
case ' ':
|
|
155
|
-
e.preventDefault()
|
|
156
|
-
if (open) {
|
|
157
|
-
if (highlightedIndex !== -1 && options[highlightedIndex]) {
|
|
158
|
-
selectOption(options[highlightedIndex], e)
|
|
159
|
-
}
|
|
160
|
-
} else setOpen(true)
|
|
161
|
-
break
|
|
162
|
-
case 'Escape':
|
|
163
|
-
e.preventDefault()
|
|
164
|
-
setOpen(false)
|
|
165
|
-
break
|
|
166
|
-
case 'ArrowDown':
|
|
167
|
-
e.preventDefault()
|
|
168
|
-
open ? setHighlightedIndex(prev => getNextIndex(prev, 1)) : setOpen(true)
|
|
169
|
-
break
|
|
170
|
-
case 'ArrowUp':
|
|
171
|
-
e.preventDefault()
|
|
172
|
-
open ? setHighlightedIndex(prev => getNextIndex(prev, -1)) : setOpen(true)
|
|
173
|
-
break
|
|
174
|
-
case 'Tab':
|
|
175
|
-
if (open) setOpen(false)
|
|
176
|
-
break
|
|
177
|
-
}
|
|
178
|
-
}, [disabled, open, setOpen, highlightedIndex, options, selectOption, getNextIndex])
|
|
179
|
-
|
|
180
|
-
return useMemo(() => ({
|
|
181
|
-
handleBlur, handleFocus, handleToggle, handleKeyDown,
|
|
182
|
-
highlightedIndex, setHighlightedIndex, handleListScroll
|
|
183
|
-
}), [handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, handleListScroll])
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export default useSelect
|