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/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