react-animated-select 0.5.6 → 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 -15
- package/dist/index.cjs.js +12 -10
- package/dist/index.css +1 -1
- package/dist/index.es.js +1265 -1121
- package/index.d.ts +3 -0
- 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/SelectJSX.jsx +0 -300
- 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 -222
- package/src/select.css +0 -367
- package/src/select.jsx +0 -343
- package/src/selectContext.js +0 -3
- package/src/slideDown.jsx +0 -36
- package/src/slideLeft.jsx +0 -41
- package/src/useSelect.jsx +0 -198
- package/src/useSelectLogic.jsx +0 -415
- package/vite.config.js +0 -27
package/src/select.jsx
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
import './select.css'
|
|
2
|
-
import {XMarkIcon, ArrowUpIcon, CheckmarkIcon} from './icons'
|
|
3
|
-
import {forwardRef, useImperativeHandle, useRef, useMemo, useState, useEffect, useCallback, useId, isValidElement, cloneElement} from 'react'
|
|
4
|
-
import {makeId} from './makeId'
|
|
5
|
-
|
|
6
|
-
import SelectJSX from './SelectJSX'
|
|
7
|
-
import useSelect from './useSelect'
|
|
8
|
-
import useSelectLogic from './useSelectLogic'
|
|
9
|
-
import SlideDown from './slideDown'
|
|
10
|
-
import SlideLeft from './slideLeft'
|
|
11
|
-
|
|
12
|
-
// universal icon display
|
|
13
|
-
const renderIcon = (Icon, defaultProps) => {
|
|
14
|
-
if (!Icon) return null
|
|
15
|
-
if (typeof Icon === 'string') return <img src={Icon} {...defaultProps} alt=''/>
|
|
16
|
-
if (isValidElement(Icon)) return cloneElement(Icon, defaultProps)
|
|
17
|
-
if (typeof Icon === 'function' || (typeof Icon === 'object' && Icon.$$typeof)) {
|
|
18
|
-
const IconComponent = Icon
|
|
19
|
-
return <IconComponent {...defaultProps}/>
|
|
20
|
-
}
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// adding classes to style options according to their state
|
|
25
|
-
const getOptionClassName = (element, index, highlightedIndex, selectedId, loadingTitle, loadMoreText, invalidOption, selectedIDs) => {
|
|
26
|
-
const multipleSelected = selectedIDs?.some(o => o.id === element.id)
|
|
27
|
-
|
|
28
|
-
if (element.groupHeader) {
|
|
29
|
-
return 'rac-select-option rac-group-option'
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return [
|
|
33
|
-
'rac-select-option',
|
|
34
|
-
element.className,
|
|
35
|
-
(multipleSelected || selectedId === element.id) && 'rac-selected',
|
|
36
|
-
index === highlightedIndex && 'rac-highlighted',
|
|
37
|
-
(element.disabled || element.loading) && 'rac-disabled-option',
|
|
38
|
-
(element.invalid || element.name === invalidOption) && 'rac-invalid-option',
|
|
39
|
-
(element.loadMore && loadingTitle === loadMoreText) && 'rac-loading-option',
|
|
40
|
-
typeof element.raw === 'boolean' && (element.raw ? 'rac-true-option' : 'rac-false-option')
|
|
41
|
-
].filter(Boolean).join(' ')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const Select = forwardRef(({
|
|
45
|
-
unmount,
|
|
46
|
-
children,
|
|
47
|
-
visibility: externalVisibility,
|
|
48
|
-
ownBehavior = false,
|
|
49
|
-
alwaysOpen = false,
|
|
50
|
-
duration = 300,
|
|
51
|
-
easing = 'ease-out',
|
|
52
|
-
offset = 0,
|
|
53
|
-
animateOpacity = true,
|
|
54
|
-
style = {},
|
|
55
|
-
className = '',
|
|
56
|
-
ArrowIcon = ArrowUpIcon,
|
|
57
|
-
ClearIcon = XMarkIcon,
|
|
58
|
-
DelIcon = XMarkIcon,
|
|
59
|
-
CheckIcon = CheckmarkIcon,
|
|
60
|
-
hasMore = false,
|
|
61
|
-
loadMore = () => {console.warn('loadMore not implemented')},
|
|
62
|
-
loadButton = false,
|
|
63
|
-
loadButtonText = 'Load more',
|
|
64
|
-
loadMoreText = 'Loading',
|
|
65
|
-
selectedText = undefined,
|
|
66
|
-
loadOffset = 100,
|
|
67
|
-
loadAhead = 3,
|
|
68
|
-
childrenFirst = false,
|
|
69
|
-
groupsClosed = false,
|
|
70
|
-
optionsClassName = '',
|
|
71
|
-
...props
|
|
72
|
-
}, ref) => {
|
|
73
|
-
|
|
74
|
-
const reactId = useId()
|
|
75
|
-
const selectId = useMemo(() => reactId.replace(/:/g, ''), [reactId])
|
|
76
|
-
const [jsxOptions, setJsxOptions] = useState([])
|
|
77
|
-
const [internalVisibility, setInternalVisibility] = useState(false)
|
|
78
|
-
const [loadingTitle, setLoadingTitle] = useState(loadButton ? loadButtonText : loadMoreText)
|
|
79
|
-
const [animationFinished, setAnimationFinished] = useState(false)
|
|
80
|
-
const selectRef = useRef(null)
|
|
81
|
-
|
|
82
|
-
const registerOption = useCallback((opt) => {
|
|
83
|
-
setJsxOptions(prev => {
|
|
84
|
-
const index = prev.findIndex(o => o.id === opt.id)
|
|
85
|
-
if (index !== -1) {
|
|
86
|
-
const existing = prev[index]
|
|
87
|
-
if (
|
|
88
|
-
existing.label === opt.label &&
|
|
89
|
-
existing.value === opt.value &&
|
|
90
|
-
existing.disabled === opt.disabled &&
|
|
91
|
-
existing.group === opt.group
|
|
92
|
-
) {
|
|
93
|
-
return prev
|
|
94
|
-
}
|
|
95
|
-
const next = [...prev]
|
|
96
|
-
next[index] = opt
|
|
97
|
-
return next
|
|
98
|
-
}
|
|
99
|
-
return [...prev, opt]
|
|
100
|
-
})
|
|
101
|
-
}, [])
|
|
102
|
-
|
|
103
|
-
const unregisterOption = useCallback((id) => {
|
|
104
|
-
setJsxOptions(prev => {
|
|
105
|
-
const filtered = prev.filter(o => o.id !== id)
|
|
106
|
-
return filtered.length === prev.length ? prev : filtered
|
|
107
|
-
})
|
|
108
|
-
}, [])
|
|
109
|
-
|
|
110
|
-
// select visibility control
|
|
111
|
-
const visibility = alwaysOpen ? true : (ownBehavior ? !!externalVisibility : internalVisibility)
|
|
112
|
-
|
|
113
|
-
const setVisibility = useCallback((newState) => {
|
|
114
|
-
if (alwaysOpen || ownBehavior) return
|
|
115
|
-
setInternalVisibility(newState)
|
|
116
|
-
}, [alwaysOpen, ownBehavior])
|
|
117
|
-
|
|
118
|
-
const logic = useSelectLogic({
|
|
119
|
-
...props, visibility, setVisibility, jsxOptions, hasMore,
|
|
120
|
-
loadButton, loadingTitle, loadMore, loadMoreText, setLoadingTitle, childrenFirst, groupsClosed
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
const {multiple, normalizedOptions, selected, selectOption, clear, removeOption, hasOptions, active, selectedValue, disabled, loading, error, placeholder, invalidOption, emptyText, disabledText, loadingText, errorText, expandedGroups, selectedIDs, setSelectedIds} = logic
|
|
124
|
-
|
|
125
|
-
const behavior = useSelect({setLoadingTitle, loadButton, loadButtonText, hasMore, loadMore, disabled, multiple, open: visibility, setOpen: setVisibility, options: normalizedOptions, selectOption, selected, loadOffset, loadAhead, expandedGroups, selectedIDs})
|
|
126
|
-
|
|
127
|
-
const {handleListScroll, handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, setHighlightedIndex} = behavior
|
|
128
|
-
|
|
129
|
-
useImperativeHandle(ref, () => selectRef.current)
|
|
130
|
-
|
|
131
|
-
useEffect(() => {
|
|
132
|
-
if (!visibility) setAnimationFinished(false)
|
|
133
|
-
}, [visibility])
|
|
134
|
-
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
if (error || disabled || loading || !hasOptions) setVisibility(false)
|
|
137
|
-
}, [error, disabled, loading, hasOptions, setVisibility])
|
|
138
|
-
|
|
139
|
-
useEffect(() => {
|
|
140
|
-
if (visibility && animationFinished && highlightedIndex !== -1) {
|
|
141
|
-
const option = normalizedOptions[highlightedIndex]
|
|
142
|
-
if (option) {
|
|
143
|
-
const domElement = document.getElementById(`opt-${selectId}-${makeId(option.id)}`)
|
|
144
|
-
domElement?.scrollIntoView({block: 'nearest'})
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}, [highlightedIndex, visibility, animationFinished, normalizedOptions, selectId])
|
|
148
|
-
|
|
149
|
-
const hasActualValue = useMemo(() => (
|
|
150
|
-
selectedValue !== undefined &&
|
|
151
|
-
selectedValue !== null &&
|
|
152
|
-
!(Array.isArray(selectedValue) && selectedValue.length === 0) &&
|
|
153
|
-
!(typeof selectedValue === 'object' && Object.keys(selectedValue).length === 0)
|
|
154
|
-
), [selectedValue])
|
|
155
|
-
|
|
156
|
-
const title = useMemo(() => {
|
|
157
|
-
if (error) return errorText
|
|
158
|
-
if (loading) return loadingText
|
|
159
|
-
if (disabled) return disabledText
|
|
160
|
-
if (hasActualValue && selectedText) return selectedText
|
|
161
|
-
|
|
162
|
-
if (selected) return selected.jsx ?? selected.name
|
|
163
|
-
|
|
164
|
-
if (hasActualValue) {
|
|
165
|
-
const recovered = normalizedOptions.find(o => o.raw === selectedValue)
|
|
166
|
-
if (recovered) return recovered.name
|
|
167
|
-
return (typeof selectedValue === 'object' && selectedValue !== null)
|
|
168
|
-
? (selectedValue.name ?? selectedValue.label ?? 'Selected Object')
|
|
169
|
-
: String(selectedValue)
|
|
170
|
-
}
|
|
171
|
-
return hasOptions ? placeholder : emptyText
|
|
172
|
-
}, [disabled, loading, error, hasOptions, selected, selectedValue, placeholder, errorText, loadingText, disabledText, emptyText, hasActualValue, normalizedOptions])
|
|
173
|
-
|
|
174
|
-
const renderOptions = useMemo(() => {
|
|
175
|
-
const nodes = []
|
|
176
|
-
let currentGroupChildren = []
|
|
177
|
-
let currentGroupName = null
|
|
178
|
-
|
|
179
|
-
const groupCounts = normalizedOptions.reduce((acc, opt) => {
|
|
180
|
-
if (opt.group) {
|
|
181
|
-
acc[opt.group] = (acc[opt.group] || 0) + 1
|
|
182
|
-
}
|
|
183
|
-
return acc
|
|
184
|
-
}, {})
|
|
185
|
-
|
|
186
|
-
const flushGroup = (name) => {
|
|
187
|
-
if (name === null || currentGroupChildren.length === 0) return
|
|
188
|
-
|
|
189
|
-
nodes.push(
|
|
190
|
-
<SlideDown
|
|
191
|
-
key={`slide-${name}`}
|
|
192
|
-
visibility={expandedGroups.has(name)}
|
|
193
|
-
>
|
|
194
|
-
{currentGroupChildren}
|
|
195
|
-
</SlideDown>
|
|
196
|
-
)
|
|
197
|
-
currentGroupChildren = []
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const createOptionNode = (element, index) => (
|
|
201
|
-
<div
|
|
202
|
-
key={element.id}
|
|
203
|
-
id={`opt-${selectId}-${makeId(element.id)}`}
|
|
204
|
-
role='option'
|
|
205
|
-
aria-selected={selected?.id === element.id}
|
|
206
|
-
aria-disabled={element.disabled || element.loading}
|
|
207
|
-
className={getOptionClassName(element, index, highlightedIndex, selected?.id, loadingTitle, loadMoreText, invalidOption, selectedIDs)}
|
|
208
|
-
onClick={(e) => !element.loading && selectOption(element, e)}
|
|
209
|
-
onMouseEnter={() => (!element.disabled && !element.loading) && setHighlightedIndex(index)}
|
|
210
|
-
>
|
|
211
|
-
{element.jsx
|
|
212
|
-
??
|
|
213
|
-
<span className='rac-option-title'>
|
|
214
|
-
{element.name}
|
|
215
|
-
</span>}
|
|
216
|
-
{element.loading && <span className='rac-loading-dots'><i/><i/><i/></span>}
|
|
217
|
-
{multiple && !element.disabled ?
|
|
218
|
-
<div className='rac-checkbox'>
|
|
219
|
-
{renderIcon(
|
|
220
|
-
CheckmarkIcon, {
|
|
221
|
-
className: `
|
|
222
|
-
rac-checkmark
|
|
223
|
-
${selectedIDs?.some(o => o.id === element.id)
|
|
224
|
-
?
|
|
225
|
-
'--checked'
|
|
226
|
-
:
|
|
227
|
-
''
|
|
228
|
-
}`})}
|
|
229
|
-
</div> : null}
|
|
230
|
-
|
|
231
|
-
</div>
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
normalizedOptions.forEach((element, index) => {
|
|
235
|
-
const isHeader = element.groupHeader
|
|
236
|
-
const belongsToGroup = !!element.group
|
|
237
|
-
|
|
238
|
-
if (isHeader || (!belongsToGroup && currentGroupName !== null)) {
|
|
239
|
-
flushGroup(currentGroupName)
|
|
240
|
-
if (!isHeader) currentGroupName = null
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (isHeader) {
|
|
244
|
-
currentGroupName = element.name
|
|
245
|
-
const open = expandedGroups.has(element.name)
|
|
246
|
-
const hasChildren = groupCounts[element.name] > 0
|
|
247
|
-
|
|
248
|
-
nodes.push(
|
|
249
|
-
<div
|
|
250
|
-
key={element.id}
|
|
251
|
-
className={[
|
|
252
|
-
'rac-group-header',
|
|
253
|
-
element.disabled && 'rac-disabled-group'
|
|
254
|
-
].filter(Boolean).join(' ')}
|
|
255
|
-
onClick={(e) => selectOption(element, e)}
|
|
256
|
-
>
|
|
257
|
-
<span className='rac-group-title-text'>{element.name}</span>
|
|
258
|
-
<SlideLeft
|
|
259
|
-
visibility={hasChildren && !element.disabled}
|
|
260
|
-
duration={duration}
|
|
261
|
-
// style={{display: 'grid'}}
|
|
262
|
-
>
|
|
263
|
-
<span className={`rac-group-arrow-wrapper ${open ? '--open' : ''}`}>
|
|
264
|
-
{renderIcon(ArrowIcon, {className: 'rac-select-arrow-wrapper'})}
|
|
265
|
-
</span>
|
|
266
|
-
</SlideLeft>
|
|
267
|
-
</div>
|
|
268
|
-
)
|
|
269
|
-
} else if (belongsToGroup) {
|
|
270
|
-
currentGroupChildren.push(createOptionNode(element, index))
|
|
271
|
-
} else {
|
|
272
|
-
nodes.push(createOptionNode(element, index))
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
flushGroup(currentGroupName)
|
|
277
|
-
|
|
278
|
-
return nodes
|
|
279
|
-
}, [
|
|
280
|
-
normalizedOptions, selectOption, selectId, selected, highlightedIndex,
|
|
281
|
-
loadingTitle, loadMoreText, invalidOption, setHighlightedIndex,
|
|
282
|
-
expandedGroups, ArrowIcon
|
|
283
|
-
])
|
|
284
|
-
|
|
285
|
-
return (
|
|
286
|
-
<SelectJSX
|
|
287
|
-
selectedText={selectedText}
|
|
288
|
-
selectRef={selectRef}
|
|
289
|
-
selectId={selectId}
|
|
290
|
-
selectedIDs={selectedIDs}
|
|
291
|
-
setSelectedIds={setSelectedIds}
|
|
292
|
-
multiple={multiple}
|
|
293
|
-
removeOption={removeOption}
|
|
294
|
-
optionsClassName={optionsClassName}
|
|
295
|
-
|
|
296
|
-
renderIcon={renderIcon}
|
|
297
|
-
normalizedOptions={normalizedOptions}
|
|
298
|
-
renderOptions={renderOptions}
|
|
299
|
-
selected={selected}
|
|
300
|
-
title={title}
|
|
301
|
-
visibility={visibility}
|
|
302
|
-
active={active}
|
|
303
|
-
hasOptions={hasOptions}
|
|
304
|
-
hasActualValue={hasActualValue}
|
|
305
|
-
highlightedIndex={highlightedIndex}
|
|
306
|
-
animationFinished={animationFinished}
|
|
307
|
-
|
|
308
|
-
disabled={disabled}
|
|
309
|
-
loading={loading}
|
|
310
|
-
error={error}
|
|
311
|
-
|
|
312
|
-
setVisibility={setVisibility}
|
|
313
|
-
setHighlightedIndex={setHighlightedIndex}
|
|
314
|
-
setAnimationFinished={setAnimationFinished}
|
|
315
|
-
handleBlur={handleBlur}
|
|
316
|
-
handleFocus={handleFocus}
|
|
317
|
-
handleToggle={handleToggle}
|
|
318
|
-
handleKeyDown={handleKeyDown}
|
|
319
|
-
handleListScroll={handleListScroll}
|
|
320
|
-
selectOption={selectOption}
|
|
321
|
-
clear={clear}
|
|
322
|
-
registerOption={registerOption}
|
|
323
|
-
unregisterOption={unregisterOption}
|
|
324
|
-
|
|
325
|
-
children={children}
|
|
326
|
-
placeholder={placeholder}
|
|
327
|
-
className={className}
|
|
328
|
-
style={style}
|
|
329
|
-
duration={duration}
|
|
330
|
-
easing={easing}
|
|
331
|
-
offset={offset}
|
|
332
|
-
animateOpacity={animateOpacity}
|
|
333
|
-
unmount={unmount}
|
|
334
|
-
ArrowIcon={ArrowIcon}
|
|
335
|
-
ClearIcon={ClearIcon}
|
|
336
|
-
DelIcon={DelIcon}
|
|
337
|
-
hasMore={hasMore}
|
|
338
|
-
loadButton={loadButton}
|
|
339
|
-
/>
|
|
340
|
-
)
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
export default Select
|
package/src/selectContext.js
DELETED
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,198 +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
|
-
selectedIDs,
|
|
11
|
-
multiple,
|
|
12
|
-
hasMore,
|
|
13
|
-
loadMore,
|
|
14
|
-
loadButton,
|
|
15
|
-
loadButtonText,
|
|
16
|
-
setLoadingTitle,
|
|
17
|
-
loadOffset,
|
|
18
|
-
loadAhead,
|
|
19
|
-
expandedGroups
|
|
20
|
-
}) {
|
|
21
|
-
const justFocused = useRef(false)
|
|
22
|
-
const lastWindowFocusTime = useRef(0)
|
|
23
|
-
const loadingTriggered = useRef(false)
|
|
24
|
-
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
25
|
-
|
|
26
|
-
// loading state synchronization
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
// flag is reset if value of the loadButton or hasMore props has changed
|
|
29
|
-
loadingTriggered.current = false
|
|
30
|
-
|
|
31
|
-
if (loadButton) {
|
|
32
|
-
setLoadingTitle(loadButtonText)
|
|
33
|
-
}
|
|
34
|
-
}, [options.length, hasMore, loadButton, loadButtonText, setLoadingTitle])
|
|
35
|
-
|
|
36
|
-
// safely call loadMore prop
|
|
37
|
-
const safeLoadMore = useCallback(() => {
|
|
38
|
-
if (!hasMore || loadingTriggered.current) return
|
|
39
|
-
loadingTriggered.current = true
|
|
40
|
-
loadMore()
|
|
41
|
-
}, [hasMore, loadMore])
|
|
42
|
-
|
|
43
|
-
// calling a function when scrolling almost to the end;
|
|
44
|
-
// loadOffset is a prop indicating how many pixels before end loadMore will be called
|
|
45
|
-
const handleListScroll = useCallback((e) => {
|
|
46
|
-
if (loadButton || !hasMore || loadingTriggered.current) return
|
|
47
|
-
|
|
48
|
-
const {scrollTop, scrollHeight, clientHeight} = e.currentTarget
|
|
49
|
-
if (scrollHeight - scrollTop <= clientHeight + loadOffset) {
|
|
50
|
-
safeLoadMore()
|
|
51
|
-
}
|
|
52
|
-
}, [loadButton, hasMore, loadOffset, safeLoadMore])
|
|
53
|
-
|
|
54
|
-
// call a function when scrolling through options using keys;
|
|
55
|
-
// loadAhead prop how many options before the end it will be called
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (!loadButton && open && hasMore && highlightedIndex >= options.length - loadAhead) {
|
|
58
|
-
safeLoadMore()
|
|
59
|
-
}
|
|
60
|
-
}, [highlightedIndex, open, hasMore, options.length, loadAhead, loadButton, safeLoadMore])
|
|
61
|
-
|
|
62
|
-
// force refocus blocking if the user exits the browser or the page
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
const handleWindowFocus = () => {lastWindowFocusTime.current = Date.now()}
|
|
65
|
-
window.addEventListener('focus', handleWindowFocus)
|
|
66
|
-
return () => window.removeEventListener('focus', handleWindowFocus)
|
|
67
|
-
}, [])
|
|
68
|
-
|
|
69
|
-
// set highlighting to the first available option by default unless otherwise selected
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
if (!open) {
|
|
72
|
-
setHighlightedIndex(-1)
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// blocking the reset of an index if it is already within the array (exmpl after loading)
|
|
77
|
-
if (highlightedIndex >= 0 && highlightedIndex < options.length) {
|
|
78
|
-
if (!options[highlightedIndex] || options[highlightedIndex].hidden || options[highlightedIndex].groupHeader) {
|
|
79
|
-
} else return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let index = -1
|
|
83
|
-
if (selected && !multiple) {
|
|
84
|
-
const firstSelected = multiple ? selected[0] : selected
|
|
85
|
-
if (firstSelected) {
|
|
86
|
-
index = options.findIndex(o => o.id === firstSelected.id && !o.disabled && !o.hidden && !o.groupHeader)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (multiple && selectedIDs.length) {
|
|
91
|
-
const ids = new Set(selectedIDs.map(o => o.id))
|
|
92
|
-
index = options.findIndex(
|
|
93
|
-
o =>
|
|
94
|
-
ids.has(o.id) &&
|
|
95
|
-
!o.disabled &&
|
|
96
|
-
!o.hidden &&
|
|
97
|
-
!o.groupHeader
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (index === -1) {
|
|
102
|
-
index = options.findIndex(o => !o.disabled && !o.hidden && !o.groupHeader)
|
|
103
|
-
}
|
|
104
|
-
setHighlightedIndex(index)
|
|
105
|
-
}, [open, options, selected])
|
|
106
|
-
|
|
107
|
-
// find the next available option to switch to using the keyboard
|
|
108
|
-
const getNextIndex = useCallback((current, direction) => {
|
|
109
|
-
const isNavigable = (opt) =>
|
|
110
|
-
opt &&
|
|
111
|
-
!opt?.groupHeader &&
|
|
112
|
-
(!opt?.group || expandedGroups?.has(opt?.group)) &&
|
|
113
|
-
!opt?.disabled &&
|
|
114
|
-
!opt?.loading
|
|
115
|
-
const len = options.length
|
|
116
|
-
if (len === 0) return -1
|
|
117
|
-
|
|
118
|
-
let next = current
|
|
119
|
-
// я не шарю нихуя в математике
|
|
120
|
-
for (let i = 0; i < len; i++) {
|
|
121
|
-
next = (next + direction + len) % len
|
|
122
|
-
|
|
123
|
-
// if autoloading is active but loadButton is inactive, then infinite scrolling is blocked
|
|
124
|
-
if (!loadButton && hasMore) {
|
|
125
|
-
if (direction > 0 && next === 0) return current
|
|
126
|
-
if (direction < 0 && next === len - 1) return current
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (isNavigable(options[next])) return next
|
|
130
|
-
}
|
|
131
|
-
return current
|
|
132
|
-
}, [options, hasMore, loadButton, expandedGroups])
|
|
133
|
-
|
|
134
|
-
// closing the selector if focus is lost
|
|
135
|
-
const handleBlur = useCallback((e) => {
|
|
136
|
-
const clickedInsidePortal = e.relatedTarget?.closest('.rac-options')
|
|
137
|
-
|
|
138
|
-
if (!e.currentTarget.contains(e.relatedTarget) && !clickedInsidePortal) {
|
|
139
|
-
setOpen(false)
|
|
140
|
-
}
|
|
141
|
-
}, [setOpen])
|
|
142
|
-
|
|
143
|
-
// opening the selector when receiving focus
|
|
144
|
-
const handleFocus = useCallback(() => {
|
|
145
|
-
if (disabled || document.hidden || (Date.now() - lastWindowFocusTime.current < 100)) return
|
|
146
|
-
|
|
147
|
-
if (!open) {
|
|
148
|
-
setOpen(true)
|
|
149
|
-
justFocused.current = true
|
|
150
|
-
setTimeout(() => {justFocused.current = false}, 200)
|
|
151
|
-
}
|
|
152
|
-
}, [disabled, open, setOpen])
|
|
153
|
-
|
|
154
|
-
// processing toggle click on select
|
|
155
|
-
const handleToggle = useCallback((e) => {
|
|
156
|
-
if (disabled || e.target.closest('.rac-select-cancel') || justFocused.current) return
|
|
157
|
-
setOpen(!open)
|
|
158
|
-
}, [disabled, open, setOpen])
|
|
159
|
-
|
|
160
|
-
// hotkey processing
|
|
161
|
-
const handleKeyDown = useCallback((e) => {
|
|
162
|
-
if (disabled) return
|
|
163
|
-
|
|
164
|
-
switch (e.key) {
|
|
165
|
-
case 'Enter':
|
|
166
|
-
case ' ':
|
|
167
|
-
e.preventDefault()
|
|
168
|
-
if (open) {
|
|
169
|
-
if (highlightedIndex !== -1 && options[highlightedIndex]) {
|
|
170
|
-
selectOption(options[highlightedIndex], e)
|
|
171
|
-
}
|
|
172
|
-
} else setOpen(true)
|
|
173
|
-
break
|
|
174
|
-
case 'Escape':
|
|
175
|
-
e.preventDefault()
|
|
176
|
-
setOpen(false)
|
|
177
|
-
break
|
|
178
|
-
case 'ArrowDown':
|
|
179
|
-
e.preventDefault()
|
|
180
|
-
open ? setHighlightedIndex(prev => getNextIndex(prev, 1)) : setOpen(true)
|
|
181
|
-
break
|
|
182
|
-
case 'ArrowUp':
|
|
183
|
-
e.preventDefault()
|
|
184
|
-
open ? setHighlightedIndex(prev => getNextIndex(prev, -1)) : setOpen(true)
|
|
185
|
-
break
|
|
186
|
-
case 'Tab':
|
|
187
|
-
if (open) setOpen(false)
|
|
188
|
-
break
|
|
189
|
-
}
|
|
190
|
-
}, [disabled, open, setOpen, highlightedIndex, options, selectOption, getNextIndex])
|
|
191
|
-
|
|
192
|
-
return useMemo(() => ({
|
|
193
|
-
handleBlur, handleFocus, handleToggle, handleKeyDown,
|
|
194
|
-
highlightedIndex, setHighlightedIndex, handleListScroll
|
|
195
|
-
}), [handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, handleListScroll])
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export default useSelect
|