react-animated-select 0.2.9 → 0.3.1
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 +70 -28
- package/demo/README.md +16 -0
- package/demo/eslint.config.js +29 -0
- package/demo/index.html +13 -0
- package/demo/package-lock.json +3436 -0
- package/demo/package.json +37 -0
- package/demo/public/vite.svg +1 -0
- package/demo/src/App.tsx +412 -0
- package/demo/src/main.jsx +9 -0
- package/demo/src/rac.css +754 -0
- package/demo/src/shake.js +11 -0
- package/demo/src/slideDown.jsx +35 -0
- package/demo/vite.config.js +7 -0
- package/dist/index.cjs.js +5 -8
- package/dist/index.css +1 -1
- package/dist/index.es.js +666 -569
- package/index.d.ts +10 -1
- package/package.json +3 -2
- package/src/option.jsx +5 -1
- package/src/select.css +7 -0
- package/src/select.jsx +71 -40
- package/src/useSelect.jsx +77 -8
- package/src/useSelectLogic.jsx +88 -49
package/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {FC, ReactNode} from 'react'
|
|
1
|
+
import {FC, ReactNode, CSSProperties, ElementType} from 'react'
|
|
2
2
|
|
|
3
3
|
export interface SelectProps {
|
|
4
4
|
|
|
@@ -38,6 +38,15 @@ export interface SelectProps {
|
|
|
38
38
|
style?: CSSProperties
|
|
39
39
|
ArrowIcon?: ElementType | string | ReactNode
|
|
40
40
|
ClearIcon?: ElementType | string | ReactNode
|
|
41
|
+
|
|
42
|
+
hasMore?: boolean
|
|
43
|
+
loadMore?: () => void
|
|
44
|
+
loadButton?: boolean
|
|
45
|
+
loadButtonText?: string
|
|
46
|
+
loadMoreText?: string
|
|
47
|
+
loadOffset?: number
|
|
48
|
+
loadAhead?: number
|
|
49
|
+
childrenFirst?: boolean
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
export const Select: FC<SelectProps>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-animated-select",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "l1nway (https://github.com/l1nway/)",
|
|
6
6
|
"description": "Animated, accessible, and flexible select component for React",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"url": "git+https://github.com/l1nway/react-animated-select.git"
|
|
17
17
|
},
|
|
18
18
|
"license": "MIT",
|
|
19
|
-
"homepage": "https://github.
|
|
19
|
+
"homepage": "https://l1nway.github.io/react-animated-select/",
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "vite build",
|
|
22
22
|
"dev": "vite build --watch"
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"react-transition-group": "^4.4.5"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
+
"@types/react": "^19.2.9",
|
|
43
44
|
"@vitejs/plugin-react": "^5.1.2",
|
|
44
45
|
"vite": "^7.3.1",
|
|
45
46
|
"vite-plugin-lib-inject-css": "^2.2.2"
|
package/src/option.jsx
CHANGED
|
@@ -10,11 +10,15 @@ export default function Option({value, id, className, children, disabled}) {
|
|
|
10
10
|
if (!ctx) return
|
|
11
11
|
|
|
12
12
|
const textFallback = getText(children)
|
|
13
|
+
|
|
14
|
+
const finalLabel = (typeof children === 'string' && children !== '')
|
|
15
|
+
? children
|
|
16
|
+
: (textFallback || String(value ?? id ?? ''))
|
|
13
17
|
|
|
14
18
|
const option = {
|
|
15
19
|
id: String(id ?? makeId(String(textFallback))),
|
|
16
20
|
value: value !== undefined ? value : textFallback,
|
|
17
|
-
label:
|
|
21
|
+
label: finalLabel,
|
|
18
22
|
jsx: children,
|
|
19
23
|
className,
|
|
20
24
|
disabled: !!disabled
|
package/src/select.css
CHANGED
|
@@ -56,11 +56,13 @@
|
|
|
56
56
|
--rac-list-max-height: 250px;
|
|
57
57
|
|
|
58
58
|
--rac-option-padding: 0.5em;
|
|
59
|
+
--rac-option-min-height: 1em;
|
|
59
60
|
--rac-disabled-option-color: color-mix(in srgb, GrayText, CanvasText 20%);
|
|
60
61
|
--rac-invalid-option-color: color-mix(in srgb, var(--rac-base-red), CanvasText 10%);
|
|
61
62
|
--rac-true-option-color: color-mix(in srgb, var(--rac-base-green), CanvasText 10%);
|
|
62
63
|
--rac-false-option-color: color-mix(in srgb, var(--rac-base-red), CanvasText 10%);
|
|
63
64
|
--rac-warning-option-color: color-mix(in srgb, var(--rac-base-yellow), CanvasText 10%);
|
|
65
|
+
--rac-loading-option-opacity: 0.75;
|
|
64
66
|
|
|
65
67
|
background: var(--rac-select-background);
|
|
66
68
|
padding: var(--rac-select-padding);
|
|
@@ -197,6 +199,7 @@
|
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
.rac-select-option {
|
|
202
|
+
min-height: var(--rac-option-min-height);
|
|
200
203
|
padding: var(--rac-option-padding);
|
|
201
204
|
transition: background-color var(--rac-duration-fast) cubic-bezier(0.4,0,0.2,1);
|
|
202
205
|
}
|
|
@@ -232,4 +235,8 @@
|
|
|
232
235
|
|
|
233
236
|
.rac-false-option {
|
|
234
237
|
color: var(--rac-false-option-color);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.rac-loading-option {
|
|
241
|
+
cursor: wait;
|
|
235
242
|
}
|
package/src/select.jsx
CHANGED
|
@@ -2,11 +2,11 @@ import './select.css'
|
|
|
2
2
|
|
|
3
3
|
import {XMarkIcon, ArrowUpIcon} from './icons'
|
|
4
4
|
import {forwardRef, useImperativeHandle, useRef, useMemo, useState, useEffect, useCallback, useId, isValidElement, cloneElement} from 'react'
|
|
5
|
-
|
|
6
5
|
import {SelectContext} from './selectContext'
|
|
7
|
-
import useSelect from './useSelect'
|
|
8
|
-
import {useSelectLogic} from './useSelectLogic'
|
|
9
6
|
import {makeId} from './makeId'
|
|
7
|
+
|
|
8
|
+
import useSelect from './useSelect'
|
|
9
|
+
import useSelectLogic from './useSelectLogic'
|
|
10
10
|
import Options from './options'
|
|
11
11
|
import SlideLeft from './slideLeft'
|
|
12
12
|
|
|
@@ -44,9 +44,23 @@ const Select = forwardRef(({
|
|
|
44
44
|
className = '',
|
|
45
45
|
ArrowIcon = ArrowUpIcon,
|
|
46
46
|
ClearIcon = XMarkIcon,
|
|
47
|
+
hasMore = false,
|
|
48
|
+
loadMore = () => {},
|
|
49
|
+
loadButton = false,
|
|
50
|
+
loadButtonText = 'Load more',
|
|
51
|
+
loadMoreText = 'Loading',
|
|
52
|
+
loadOffset = 100,
|
|
53
|
+
loadAhead = 3,
|
|
54
|
+
childrenFirst = false,
|
|
47
55
|
...props
|
|
48
56
|
}, ref) => {
|
|
49
57
|
|
|
58
|
+
const [loadingTitle, setLoadingTitle] = useState(loadButton ? loadButtonText : loadMoreText)
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
setLoadingTitle(loadButton ? loadButtonText : loadMoreText)
|
|
62
|
+
}, [loadButton, loadButtonText, loadMoreText])
|
|
63
|
+
|
|
50
64
|
const reactId = useId()
|
|
51
65
|
|
|
52
66
|
const selectId = useMemo(() => reactId.replace(/:/g, ''), [reactId])
|
|
@@ -95,16 +109,23 @@ const Select = forwardRef(({
|
|
|
95
109
|
})
|
|
96
110
|
}, [alwaysOpen, ownBehavior])
|
|
97
111
|
|
|
98
|
-
const {normalizedOptions, selected, selectOption, clear, hasOptions, active, selectedValue, disabled, loading, error, placeholder, invalidOption,
|
|
112
|
+
const {normalizedOptions, selected, selectOption, clear, hasOptions, active, selectedValue, disabled, loading, error, placeholder, invalidOption, emptyText, disabledText, loadingText, errorText} = useSelectLogic({...props, visibility, setVisibility, jsxOptions, hasMore,loadButton, loadingTitle, loadMore, loadMoreText, setLoadingTitle, childrenFirst})
|
|
99
113
|
|
|
100
114
|
// event handler functions for interacting with the select
|
|
101
|
-
const {handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, setHighlightedIndex} = useSelect({
|
|
115
|
+
const {handleListScroll, handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, setHighlightedIndex} = useSelect({
|
|
116
|
+
setLoadingTitle,
|
|
117
|
+
loadButton,
|
|
118
|
+
loadButtonText,
|
|
119
|
+
hasMore,
|
|
120
|
+
loadMore,
|
|
102
121
|
disabled,
|
|
103
122
|
isOpen: visibility,
|
|
104
123
|
setIsOpen: setVisibility,
|
|
105
124
|
options: normalizedOptions,
|
|
106
|
-
selectOption
|
|
107
|
-
selected
|
|
125
|
+
selectOption,
|
|
126
|
+
selected,
|
|
127
|
+
loadOffset,
|
|
128
|
+
loadAhead
|
|
108
129
|
})
|
|
109
130
|
|
|
110
131
|
const [animationFinished, setAnimationFinished] = useState(false)
|
|
@@ -178,9 +199,11 @@ const Select = forwardRef(({
|
|
|
178
199
|
|
|
179
200
|
if (index === highlightedIndex) optionClass += ' rac-highlighted'
|
|
180
201
|
|
|
181
|
-
if (element.disabled) optionClass += ' rac-disabled-option'
|
|
202
|
+
if (element.disabled || element.loading) optionClass += ' rac-disabled-option'
|
|
203
|
+
|
|
204
|
+
if (element.invalid) optionClass += ' rac-invalid-option'
|
|
182
205
|
|
|
183
|
-
if (element.
|
|
206
|
+
if (element.loadMore && loadingTitle == loadMoreText) optionClass += ' rac-loading-option'
|
|
184
207
|
|
|
185
208
|
if (typeof element.raw === 'boolean') {
|
|
186
209
|
optionClass += element.raw ? ' rac-true-option' : ' rac-false-option'
|
|
@@ -193,45 +216,31 @@ const Select = forwardRef(({
|
|
|
193
216
|
return (
|
|
194
217
|
<div
|
|
195
218
|
className={optionClass}
|
|
196
|
-
onClick={(e) =>
|
|
197
|
-
|
|
219
|
+
onClick={(e) => {
|
|
220
|
+
if (element.loading) {
|
|
221
|
+
e.stopPropagation()
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
selectOption(element, e)
|
|
225
|
+
}}
|
|
226
|
+
onMouseEnter={() => (!element.disabled && !element.loading) && setHighlightedIndex(index)}
|
|
198
227
|
key={element.id}
|
|
199
228
|
id={optionId}
|
|
200
229
|
role='option'
|
|
201
230
|
aria-selected={selected?.id === element.id}
|
|
202
|
-
aria-disabled={element.disabled}
|
|
231
|
+
aria-disabled={element.disabled || element.loading}
|
|
232
|
+
data-loading={element.loading}
|
|
203
233
|
>
|
|
204
234
|
{element.jsx ?? element.name}
|
|
235
|
+
{element.loading && (
|
|
236
|
+
<span className='rac-loading-dots'>
|
|
237
|
+
<i/><i/><i/>
|
|
238
|
+
</span>
|
|
239
|
+
)}
|
|
205
240
|
</div>
|
|
206
241
|
)
|
|
207
242
|
}), [normalizedOptions, selectOption, selectId, selected, highlightedIndex])
|
|
208
243
|
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
211
|
-
|
|
212
|
-
const receivedType = typeof options
|
|
213
|
-
if (options && typeof options !== 'object') {
|
|
214
|
-
console.error(
|
|
215
|
-
`%c[Select Library]:%c Invalid prop %coptions%c.\n` +
|
|
216
|
-
`Expected %cArray%c or %cObject%c, but received %c${receivedType}%c.\n`,
|
|
217
|
-
'color: #ff4d4f; font-weight: bold;', 'color: default;',
|
|
218
|
-
'color: #1890ff; font-weight: bold;', 'color: default;',
|
|
219
|
-
'color: #52c41a; font-weight: bold;', 'color: default;',
|
|
220
|
-
'color: #52c41a; font-weight: bold;', 'color: default;',
|
|
221
|
-
'color: #ff4d4f; font-weight: bold;', 'color: default;'
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (isControlled && defaultValue !== undefined) {
|
|
226
|
-
console.warn(
|
|
227
|
-
`%c[Select Library]:%c .\n` +
|
|
228
|
-
``,
|
|
229
|
-
'color: #faad14; font-weight: bold;', 'color: default;'
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}, [options, value, defaultValue, isControlled])
|
|
234
|
-
|
|
235
244
|
return(
|
|
236
245
|
<SelectContext.Provider
|
|
237
246
|
value={{registerOption, unregisterOption}}
|
|
@@ -294,8 +303,12 @@ const Select = forwardRef(({
|
|
|
294
303
|
style={{display: 'grid'}}
|
|
295
304
|
>
|
|
296
305
|
{renderIcon(ClearIcon, {
|
|
297
|
-
className: 'rac-select-cancel',
|
|
298
|
-
|
|
306
|
+
className: 'rac-select-cancel',
|
|
307
|
+
onMouseDown: (e) => {
|
|
308
|
+
e.preventDefault()
|
|
309
|
+
e.stopPropagation()
|
|
310
|
+
},
|
|
311
|
+
onClick: clear
|
|
299
312
|
})}
|
|
300
313
|
</SlideLeft>
|
|
301
314
|
<SlideLeft
|
|
@@ -323,11 +336,29 @@ const Select = forwardRef(({
|
|
|
323
336
|
animateOpacity={animateOpacity}
|
|
324
337
|
>
|
|
325
338
|
<div
|
|
339
|
+
onScroll={handleListScroll}
|
|
340
|
+
tabIndex='-1'
|
|
326
341
|
className='rac-select-list'
|
|
327
342
|
role='listbox'
|
|
328
343
|
aria-label='Options'
|
|
329
344
|
>
|
|
330
345
|
{renderOptions}
|
|
346
|
+
{!loadButton && hasMore ?
|
|
347
|
+
<div
|
|
348
|
+
onClick={e => e.stopPropagation()}
|
|
349
|
+
onMouseEnter={e => e.preventDefault()}
|
|
350
|
+
className='rac-select-option rac-disabled-option rac-loading-option'
|
|
351
|
+
>
|
|
352
|
+
<span
|
|
353
|
+
className='rac-loading-option-title'
|
|
354
|
+
>
|
|
355
|
+
Loading
|
|
356
|
+
</span>
|
|
357
|
+
<span className='rac-loading-dots'>
|
|
358
|
+
<i/><i/><i/>
|
|
359
|
+
</span>
|
|
360
|
+
</div>
|
|
361
|
+
: null}
|
|
331
362
|
</div>
|
|
332
363
|
</Options>
|
|
333
364
|
</div>
|
package/src/useSelect.jsx
CHANGED
|
@@ -6,12 +6,61 @@ function useSelect({
|
|
|
6
6
|
setIsOpen,
|
|
7
7
|
options,
|
|
8
8
|
selectOption,
|
|
9
|
-
selected
|
|
9
|
+
selected,
|
|
10
|
+
hasMore,
|
|
11
|
+
loadMore,
|
|
12
|
+
loadButton,
|
|
13
|
+
loadButtonText,
|
|
14
|
+
setLoadingTitle,
|
|
15
|
+
loadOffset,
|
|
16
|
+
loadAhead
|
|
10
17
|
}) {
|
|
11
18
|
const justFocused = useRef(false)
|
|
12
19
|
const lastWindowFocusTime = useRef(0)
|
|
13
20
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
14
21
|
|
|
22
|
+
const loadingTriggered = useRef(false)
|
|
23
|
+
|
|
24
|
+
const prevOptionsLength = useRef(options?.length || 0)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (options && options.length !== prevOptionsLength.current) {
|
|
28
|
+
loadingTriggered.current = false
|
|
29
|
+
prevOptionsLength.current = options.length
|
|
30
|
+
loadButton && setLoadingTitle(loadButtonText)
|
|
31
|
+
}
|
|
32
|
+
}, [options])
|
|
33
|
+
|
|
34
|
+
const safeLoadMore = useCallback(() => {
|
|
35
|
+
if (!hasMore || loadingTriggered.current) return
|
|
36
|
+
|
|
37
|
+
loadingTriggered.current = true
|
|
38
|
+
loadMore()
|
|
39
|
+
}, [hasMore, loadMore])
|
|
40
|
+
|
|
41
|
+
const handleListScroll = useCallback((e) => {
|
|
42
|
+
if (loadButton || !hasMore || loadingTriggered.current) return
|
|
43
|
+
|
|
44
|
+
const {scrollTop, scrollHeight, clientHeight} = e.currentTarget
|
|
45
|
+
|
|
46
|
+
const threshold = loadOffset
|
|
47
|
+
|
|
48
|
+
if (scrollHeight - scrollTop <= clientHeight + threshold) {
|
|
49
|
+
safeLoadMore()
|
|
50
|
+
}
|
|
51
|
+
}, [loadButton, hasMore, safeLoadMore])
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (loadButton) return
|
|
55
|
+
|
|
56
|
+
if (isOpen && hasMore && highlightedIndex >= 0) {
|
|
57
|
+
const threshold = loadAhead
|
|
58
|
+
if (highlightedIndex >= options.length - threshold) {
|
|
59
|
+
safeLoadMore()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}, [loadButton, highlightedIndex, isOpen, hasMore, options.length, safeLoadMore])
|
|
63
|
+
|
|
15
64
|
useEffect(() => {
|
|
16
65
|
const handleWindowFocus = () => {
|
|
17
66
|
lastWindowFocusTime.current = Date.now()
|
|
@@ -22,6 +71,10 @@ function useSelect({
|
|
|
22
71
|
|
|
23
72
|
useEffect(() => {
|
|
24
73
|
if (isOpen) {
|
|
74
|
+
if (highlightedIndex >= 0 && highlightedIndex < options.length) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
25
78
|
let index = -1
|
|
26
79
|
if (selected) {
|
|
27
80
|
const selectedIndex = options.findIndex(o => o.id === selected.id && !o.disabled)
|
|
@@ -34,7 +87,7 @@ function useSelect({
|
|
|
34
87
|
} else {
|
|
35
88
|
setHighlightedIndex(-1)
|
|
36
89
|
}
|
|
37
|
-
}, [isOpen,
|
|
90
|
+
}, [isOpen, options])
|
|
38
91
|
|
|
39
92
|
const handleBlur = useCallback((e) => {
|
|
40
93
|
if (e.currentTarget.contains(e.relatedTarget)) return
|
|
@@ -60,25 +113,40 @@ function useSelect({
|
|
|
60
113
|
|
|
61
114
|
const handleToggle = useCallback((e) => {
|
|
62
115
|
if (disabled) return
|
|
63
|
-
if (e.target.closest('.rac-select-cancel')) return
|
|
116
|
+
if (e.target.closest && e.target.closest('.rac-select-cancel')) return
|
|
64
117
|
if (justFocused.current) return
|
|
65
118
|
|
|
66
119
|
setIsOpen(!isOpen)
|
|
67
120
|
}, [disabled, isOpen, setIsOpen])
|
|
68
121
|
|
|
69
122
|
const getNextIndex = (current, direction) => {
|
|
70
|
-
|
|
123
|
+
const isNavigable = (opt) => opt && !opt.disabled && !opt.loading
|
|
124
|
+
|
|
125
|
+
if (!options.some(isNavigable)) return -1
|
|
126
|
+
|
|
71
127
|
let next = current
|
|
72
128
|
let loops = 0
|
|
129
|
+
|
|
73
130
|
do {
|
|
74
131
|
next += direction
|
|
75
|
-
|
|
76
|
-
if (next >= options.length)
|
|
132
|
+
|
|
133
|
+
if (next >= options.length) {
|
|
134
|
+
if (hasMore && !loadButton) return current
|
|
135
|
+
next = 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (next < 0) {
|
|
139
|
+
if (hasMore && !loadButton) return current
|
|
140
|
+
next = options.length - 1
|
|
141
|
+
}
|
|
142
|
+
|
|
77
143
|
loops++
|
|
78
|
-
} while (options[next]
|
|
144
|
+
} while (!isNavigable(options[next]) && loops <= options.length)
|
|
145
|
+
|
|
79
146
|
return next
|
|
80
147
|
}
|
|
81
148
|
|
|
149
|
+
|
|
82
150
|
const handleKeyDown = useCallback((e) => {
|
|
83
151
|
if (disabled) return
|
|
84
152
|
|
|
@@ -128,7 +196,8 @@ function useSelect({
|
|
|
128
196
|
handleToggle,
|
|
129
197
|
handleKeyDown,
|
|
130
198
|
highlightedIndex,
|
|
131
|
-
setHighlightedIndex
|
|
199
|
+
setHighlightedIndex,
|
|
200
|
+
handleListScroll
|
|
132
201
|
}), [handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, setHighlightedIndex])
|
|
133
202
|
}
|
|
134
203
|
|
package/src/useSelectLogic.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {useState, useMemo, useCallback, useId, useEffect} from 'react'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function useSelectLogic({
|
|
4
4
|
options = [],
|
|
5
5
|
jsxOptions = [],
|
|
6
6
|
value,
|
|
@@ -17,7 +17,14 @@ export function useSelectLogic({
|
|
|
17
17
|
disabledOption = 'Disabled option',
|
|
18
18
|
emptyOption = 'Empty option',
|
|
19
19
|
invalidOption = 'Invalid option',
|
|
20
|
-
setVisibility
|
|
20
|
+
setVisibility,
|
|
21
|
+
hasMore,
|
|
22
|
+
loadButton,
|
|
23
|
+
setLoadingTitle,
|
|
24
|
+
loadingTitle,
|
|
25
|
+
loadMoreText,
|
|
26
|
+
loadMore,
|
|
27
|
+
childrenFirst
|
|
21
28
|
}) {
|
|
22
29
|
const stableId = useId()
|
|
23
30
|
const isControlled = value !== undefined
|
|
@@ -40,7 +47,7 @@ export function useSelectLogic({
|
|
|
40
47
|
value: val,
|
|
41
48
|
userId: null,
|
|
42
49
|
disabled: true,
|
|
43
|
-
|
|
50
|
+
invalid: true,
|
|
44
51
|
label: invalidOption,
|
|
45
52
|
original: originalItem
|
|
46
53
|
})
|
|
@@ -48,25 +55,25 @@ export function useSelectLogic({
|
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
if (val === '') {
|
|
51
|
-
flat.push({
|
|
52
|
-
key: `empty-str-${flat.length}`,
|
|
53
|
-
value: '',
|
|
54
|
-
userId: null,
|
|
55
|
-
disabled: true,
|
|
56
|
-
label: emptyOption,
|
|
57
|
-
original: originalItem
|
|
58
|
+
flat.push({
|
|
59
|
+
key: `empty-str-${flat.length}`,
|
|
60
|
+
value: '',
|
|
61
|
+
userId: null,
|
|
62
|
+
disabled: true,
|
|
63
|
+
label: emptyOption,
|
|
64
|
+
original: originalItem
|
|
58
65
|
})
|
|
59
66
|
return
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
if (val === null || val === undefined) {
|
|
63
|
-
flat.push({
|
|
64
|
-
key: `empty-${flat.length}`,
|
|
65
|
-
value: null,
|
|
66
|
-
userId: null,
|
|
67
|
-
disabled: true,
|
|
68
|
-
label: emptyOption,
|
|
69
|
-
original: originalItem
|
|
70
|
+
flat.push({
|
|
71
|
+
key: `empty-${flat.length}`,
|
|
72
|
+
value: null,
|
|
73
|
+
userId: null,
|
|
74
|
+
disabled: true,
|
|
75
|
+
label: emptyOption,
|
|
76
|
+
original: originalItem
|
|
70
77
|
})
|
|
71
78
|
return
|
|
72
79
|
}
|
|
@@ -80,17 +87,6 @@ export function useSelectLogic({
|
|
|
80
87
|
original: originalItem
|
|
81
88
|
})
|
|
82
89
|
return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
86
|
-
flat.push({
|
|
87
|
-
key: val.id ?? val.value ?? val.name ?? key ?? `obj-${flat.length}`,
|
|
88
|
-
value: val,
|
|
89
|
-
userId: computedUserId,
|
|
90
|
-
disabled: !!val.disabled,
|
|
91
|
-
label: val.name ?? val.label ?? String(key),
|
|
92
|
-
original: originalItem
|
|
93
|
-
})
|
|
94
90
|
} else {
|
|
95
91
|
flat.push({
|
|
96
92
|
key: key ?? `opt-${flat.length}`,
|
|
@@ -105,15 +101,30 @@ export function useSelectLogic({
|
|
|
105
101
|
if (Array.isArray(options)) {
|
|
106
102
|
options.forEach((item, index) => {
|
|
107
103
|
if (item && typeof item === 'object' && Object.keys(item).length === 1 && item.disabled === true) {
|
|
108
|
-
flat.push({
|
|
104
|
+
flat.push({key: `dis-${index}`, value: null, userId: null, disabled: true, label: disabledOption, original: item})
|
|
109
105
|
} else if (isOptionObject(item)) {
|
|
110
|
-
const stableUserId = item.id ?? (typeof item.value !== 'object' ? item.value : (item.label ?? item.name ?? item.value))
|
|
106
|
+
const stableUserId = item.id ?? (typeof item.value !== 'object' ? item.value : (item.label ?? item.name ?? item.value))
|
|
107
|
+
|
|
108
|
+
let rawLabel = item.name || item.label || item.id || item.value
|
|
109
|
+
|
|
110
|
+
if (rawLabel === null || rawLabel === undefined || rawLabel === '') {
|
|
111
|
+
const fallbackEntry = Object.entries(item).find(([k, v]) =>
|
|
112
|
+
k !== 'disabled' && v !== null && v !== undefined && v !== ''
|
|
113
|
+
)
|
|
114
|
+
if (fallbackEntry) {
|
|
115
|
+
rawLabel = fallbackEntry[1]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const hasNoContent = rawLabel === null || rawLabel === undefined || rawLabel === ''
|
|
120
|
+
const finalLabel = hasNoContent ? emptyOption : String(rawLabel)
|
|
121
|
+
|
|
111
122
|
flat.push({
|
|
112
123
|
key: item.id ?? item.value ?? item.name ?? `opt-${index}`,
|
|
113
124
|
value: item.value !== undefined ? item.value : (item.id !== undefined ? item.id : item),
|
|
114
125
|
userId: stableUserId,
|
|
115
|
-
disabled: !!item.disabled,
|
|
116
|
-
label:
|
|
126
|
+
disabled: hasNoContent || !!item.disabled,
|
|
127
|
+
label: finalLabel,
|
|
117
128
|
original: item
|
|
118
129
|
})
|
|
119
130
|
} else if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
@@ -133,22 +144,44 @@ export function useSelectLogic({
|
|
|
133
144
|
raw: item.value,
|
|
134
145
|
original: item.original,
|
|
135
146
|
disabled: item.disabled,
|
|
136
|
-
|
|
147
|
+
invalid: item.invalid,
|
|
137
148
|
type: typeof item.value === 'boolean' ? 'boolean' : 'normal'
|
|
138
149
|
}))
|
|
139
150
|
|
|
140
|
-
const jsxOpts = jsxOptions.map((opt, index) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
const jsxOpts = jsxOptions.map((opt, index) => {
|
|
152
|
+
const hasNoValue = opt.value === null || opt.value === undefined
|
|
153
|
+
const hasNoLabel = opt.label === null || opt.label === undefined || opt.label === ''
|
|
154
|
+
|
|
155
|
+
const isActuallyEmpty = hasNoValue && hasNoLabel
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
...opt,
|
|
159
|
+
id: `jsx-${stableId.replace(/:/g, '')}-${opt.id}-${index}`,
|
|
160
|
+
userId: opt.id,
|
|
161
|
+
raw: opt.value,
|
|
162
|
+
original: opt.value,
|
|
163
|
+
name: isActuallyEmpty ? emptyOption : opt.label,
|
|
164
|
+
disabled: opt.disabled || isActuallyEmpty,
|
|
165
|
+
type: typeof opt.value === 'boolean' ? 'boolean' : 'normal'
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const combined = childrenFirst ? [...jsxOpts, ...propOpts] : [...propOpts, ...jsxOpts]
|
|
149
170
|
|
|
150
|
-
|
|
151
|
-
|
|
171
|
+
if (hasMore && loadButton) {
|
|
172
|
+
const isLoading = loadingTitle === loadMoreText
|
|
173
|
+
|
|
174
|
+
combined.push({
|
|
175
|
+
id: 'special-load-more-id',
|
|
176
|
+
name: loadingTitle,
|
|
177
|
+
loadMore: true,
|
|
178
|
+
loading: isLoading,
|
|
179
|
+
type: 'special'
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return combined
|
|
184
|
+
}, [options, jsxOptions, stableId, emptyOption, disabledOption, hasMore, loadButton, loadingTitle, loadMoreText])
|
|
152
185
|
|
|
153
186
|
const findIdByValue = useCallback((val) => {
|
|
154
187
|
if (val === undefined || val === null) return null
|
|
@@ -189,9 +222,15 @@ export function useSelectLogic({
|
|
|
189
222
|
}, [selectedId, normalizedOptions])
|
|
190
223
|
|
|
191
224
|
const selectOption = useCallback((option, e) => {
|
|
192
|
-
if (option.disabled) {
|
|
225
|
+
if (option.disabled || option.loadMore) {
|
|
193
226
|
e?.stopPropagation()
|
|
194
227
|
e?.preventDefault()
|
|
228
|
+
|
|
229
|
+
if (loadingTitle !== loadMoreText) {
|
|
230
|
+
setLoadingTitle(loadMoreText)
|
|
231
|
+
loadMore()
|
|
232
|
+
}
|
|
233
|
+
|
|
195
234
|
return
|
|
196
235
|
}
|
|
197
236
|
|
|
@@ -200,9 +239,7 @@ export function useSelectLogic({
|
|
|
200
239
|
setVisibility(false)
|
|
201
240
|
}, [onChange, setVisibility])
|
|
202
241
|
|
|
203
|
-
const clear = useCallback((
|
|
204
|
-
e.preventDefault()
|
|
205
|
-
e.stopPropagation()
|
|
242
|
+
const clear = useCallback(() => {
|
|
206
243
|
setSelectedId(null)
|
|
207
244
|
onChange?.(null, null)
|
|
208
245
|
}, [onChange])
|
|
@@ -215,4 +252,6 @@ export function useSelectLogic({
|
|
|
215
252
|
placeholder, emptyText, disabledText, loadingText, errorText,
|
|
216
253
|
disabledOption, emptyOption, invalidOption, disabled, loading, error
|
|
217
254
|
}
|
|
218
|
-
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default useSelectLogic
|