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/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
@@ -1,3 +0,0 @@
1
- import {createContext} from 'react'
2
-
3
- export const SelectContext = createContext(null)
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