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/demo/src/shake.js DELETED
@@ -1,11 +0,0 @@
1
- export const shake = (el) => {
2
- if (!el) return
3
-
4
- el.classList.remove('--animated-error')
5
- void el.offsetWidth
6
- el.classList.add('--animated-error')
7
- }
8
-
9
- export const clearShake = (el) => {
10
- el?.classList.remove('--animated-error')
11
- }
@@ -1,35 +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
- }}
26
- className='slideDown-enter-done'
27
- tabIndex={-1}
28
- >
29
- {children}
30
- </div>
31
- </CSSTransition>
32
- )
33
- }
34
-
35
- export default SlideDown
@@ -1,7 +0,0 @@
1
- import {defineConfig} from 'vite'
2
- import react from '@vitejs/plugin-react'
3
-
4
- export default defineConfig({
5
- plugins: [react()],
6
- base: '/react-animated-select/',
7
- })
package/src/SelectJSX.jsx DELETED
@@ -1,300 +0,0 @@
1
- import {memo, useCallback, useEffect, useLayoutEffect, useRef, useState} 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
- }
16
- else if (element?.raw !== undefined) {
17
- const recovered = normalizedOptions.find(o =>
18
- o.raw === element.raw ||
19
- o.original === element.raw ||
20
- o.userId === element.raw
21
- )
22
- if (recovered) {
23
- label = recovered.jsx ?? recovered.name
24
- }
25
- }
26
-
27
- if (label == null) {
28
- label = typeof element === 'object'
29
- ? (element.label ?? element.name ?? element.value ?? 'Selected item')
30
- : String(element)
31
- }
32
-
33
- const handleDelete = useCallback((e) => {
34
- e.stopPropagation()
35
- e.preventDefault()
36
- remove(element.id)
37
- }, [element.id, remove])
38
-
39
- const preventFocus = useCallback((e) => {
40
- e.stopPropagation()
41
- e.preventDefault()
42
- }, [])
43
-
44
- return (
45
- <div className='rac-multiple-selected-option'>
46
- {label}
47
- {renderIcon(DelIcon, {onClick: handleDelete, onMouseDown: preventFocus})}
48
- </div>
49
- )
50
- })
51
-
52
- const SelectJSX = memo(({
53
- selectRef,
54
- selectId,
55
- removeOption,
56
- renderOptions,
57
- selected,
58
- selectedIDs,
59
- setSelectedIds,
60
- normalizedOptions,
61
- title,
62
- visibility,
63
- active,
64
- hasOptions,
65
- hasActualValue,
66
- optionsClassName,
67
- selectedText,
68
-
69
- disabled,
70
- loading,
71
- error,
72
-
73
- registerOption,
74
- unregisterOption,
75
- handleBlur,
76
- handleFocus,
77
- handleToggle,
78
- handleKeyDown,
79
- handleListScroll,
80
- setAnimationFinished,
81
- clear,
82
-
83
- children,
84
- placeholder,
85
- className,
86
- style,
87
- duration,
88
- easing,
89
- offset,
90
- animateOpacity,
91
- unmount,
92
- ArrowIcon,
93
- ClearIcon,
94
- DelIcon,
95
- renderIcon,
96
- hasMore,
97
- loadButton
98
- }) => {
99
-
100
- const internalRef = useRef(null)
101
-
102
- const [titleHeight, setTitleHeight] = useState(null)
103
- const titleContentRef = useRef(null)
104
-
105
- useLayoutEffect(() => {
106
- const element = titleContentRef.current
107
- if (!element) return
108
-
109
- const observer = new ResizeObserver((entries) => {
110
- window.requestAnimationFrame(() => {
111
- if (!Array.isArray(entries) || !entries.length) return
112
-
113
- const newHeight = entries[0].contentRect.height
114
-
115
- setTitleHeight(newHeight)
116
- })
117
- })
118
-
119
- observer.observe(element)
120
-
121
- return () => {
122
- observer.disconnect()
123
- }
124
- }, [])
125
-
126
- useEffect(() => {
127
- if (selectRef) {
128
- if (typeof selectRef === 'function') selectRef(internalRef.current)
129
- else selectRef.current = internalRef.current
130
- }
131
- }, [selectRef])
132
-
133
- const remove = useCallback((id) => {
134
- if (removeOption) {
135
- removeOption(id)
136
- } else {
137
- setSelectedIds(prev => prev.filter(o => o.id !== id))
138
- }
139
- }, [removeOption, setSelectedIds])
140
-
141
- const renderSelectIDs = selectedIDs?.map((element, index) => (
142
- <Animated
143
- key={element.id ?? index}
144
- duration={duration}
145
- widthMode
146
- >
147
- <SelectedItem
148
- key={element.id ?? index}
149
- element={element}
150
- index={index}
151
- remove={remove}
152
- renderIcon={renderIcon}
153
- DelIcon={DelIcon}
154
- normalizedOptions={normalizedOptions}
155
- />
156
- </Animated>
157
- ))
158
-
159
- return (
160
- <SelectContext.Provider
161
- value={{registerOption, unregisterOption}}
162
- >
163
- {children}
164
- <div
165
- ref={internalRef}
166
- style={{
167
- ...style,
168
- '--rac-duration': `${duration}ms`,
169
- '--rac-duration-fast': 'calc(var(--rac-duration) * 0.5)',
170
- '--rac-duration-base': 'var(--rac-duration)',
171
- '--rac-duration-slow': 'calc(var(--rac-duration) * 1.3)'
172
- }}
173
- className={
174
- `rac-select
175
- ${className}
176
- ${(!hasOptions || disabled) ? 'rac-disabled-style' : ''}
177
- ${loading ? 'rac-loading-style' : ''}
178
- ${error ? 'rac-error-style' : ''}`}
179
- tabIndex={active ? 0 : -1}
180
- role='combobox'
181
- aria-haspopup='listbox'
182
- aria-expanded={visibility}
183
- aria-controls={`${selectId}-listbox`}
184
- aria-label={placeholder}
185
- aria-disabled={disabled || !hasOptions}
186
- {...(active && {
187
- onBlur: handleBlur,
188
- onFocus: handleFocus,
189
- onClick: handleToggle,
190
- onKeyDown: handleKeyDown
191
- })}
192
- >
193
- <div
194
- className={
195
- `rac-select-title-wrapper
196
- ${(!error && !loading && selected?.type === 'boolean')
197
- ?
198
- (selected.raw ? 'rac-true-option' : 'rac-false-option')
199
- : ''}
200
- `}
201
- style={{
202
- height: titleHeight ? `${titleHeight}px` : 'auto',
203
- boxSizing: 'content-box',
204
- overflow: 'hidden'
205
- }}
206
- >
207
- <div
208
- className='rac-select-title'
209
- ref={titleContentRef}
210
- >
211
- <TransitionGroup component={null}>
212
- {selectedIDs?.length && !selectedText ? (
213
- renderSelectIDs
214
- ) : (
215
- <Animated
216
- key={title}
217
- duration={duration}
218
- widthMode
219
- >
220
- <span className='rac-title-text'>{title}</span>
221
-
222
- <SlideLeft visibility={loading && !error} duration={duration}>
223
- <span className='rac-loading-dots'><i/><i/><i/></span>
224
- </SlideLeft>
225
- </Animated>
226
- )}
227
- </TransitionGroup>
228
- </div>
229
- </div>
230
-
231
- <div className='rac-select-buttons'>
232
- <SlideLeft
233
- visibility={hasActualValue && hasOptions && !disabled && !loading && !error}
234
- duration={duration}
235
- style={{display: 'grid'}}
236
- >
237
- {renderIcon(ClearIcon, {
238
- className: 'rac-select-cancel',
239
- onMouseDown: e => {
240
- e.preventDefault()
241
- e.stopPropagation()
242
- },
243
- onClick: clear
244
- })}
245
- </SlideLeft>
246
- <SlideLeft
247
- visibility={active}
248
- duration={duration}
249
- style={{display: 'grid'}}
250
- >
251
- <span
252
- className={`rac-select-arrow-wrapper ${visibility ? '--open' : ''}`}
253
- >
254
- {renderIcon(ArrowIcon, {
255
- className: 'rac-select-arrow-wrapper'
256
- })}
257
- </span>
258
- </SlideLeft>
259
- </div>
260
-
261
- <Options
262
- className={optionsClassName}
263
- visibility={visibility}
264
- selectRef={selectRef}
265
- onAnimationDone={() => setAnimationFinished(true)}
266
- unmount={unmount}
267
- duration={duration}
268
- easing={easing}
269
- offset={offset}
270
- animateOpacity={animateOpacity}
271
- style={{
272
- ...style,
273
- '--rac-duration': `${duration}ms`
274
- }}
275
- >
276
- <div
277
- onScroll={handleListScroll}
278
- tabIndex='-1'
279
- className='rac-select-list'
280
- role='listbox'
281
- aria-label='Options'
282
- >
283
- {renderOptions}
284
- {!loadButton && hasMore && (
285
- <div
286
- className='rac-select-option rac-disabled-option rac-loading-option'
287
- onClick={e => e.stopPropagation()}
288
- >
289
- <span className='rac-loading-option-title'>Loading</span>
290
- <span className='rac-loading-dots'><i/><i/><i/></span>
291
- </div>
292
- )}
293
- </div>
294
- </Options>
295
- </div>
296
- </SelectContext.Provider>
297
- )
298
- })
299
-
300
- export default SelectJSX
package/src/animated.jsx DELETED
@@ -1,80 +0,0 @@
1
- import {useRef} from 'react'
2
- import {CSSTransition} from 'react-transition-group'
3
-
4
- const Animated = ({children, duration, widthMode = false, ...props}) => {
5
- const nodeRef = useRef(null)
6
- return (
7
- <CSSTransition
8
- nodeRef={nodeRef}
9
- timeout={duration}
10
- classNames='rac-slide-left'
11
- {...props}
12
- onEnter={() => {
13
- const el = nodeRef.current
14
- if (widthMode) {
15
- el.style.width = '0px'
16
- } else {
17
- el.style.height = '0px'
18
- el.style.transform = 'translateY(-10px)'
19
- }
20
- el.style.opacity = '0'
21
- }}
22
- onEntering={() => {
23
- const el = nodeRef.current
24
- el.offsetHeight
25
- if (widthMode) {
26
- el.style.width = el.scrollWidth + 'px'
27
- } else {
28
- el.style.height = el.scrollHeight + 'px'
29
- el.style.transform = 'translateY(0)'
30
- }
31
- el.style.opacity = '1'
32
- }}
33
- onEntered={() => {
34
- const el = nodeRef.current
35
- el.style.width = widthMode ? 'auto' : ''
36
- el.style.height = widthMode ? '' : 'auto'
37
- el.style.opacity = '1'
38
- el.style.transform = ''
39
- }}
40
- onExit={() => {
41
- const el = nodeRef.current
42
- if (widthMode) {
43
- el.style.width = el.offsetWidth + 'px'
44
- } else {
45
- el.style.height = el.offsetHeight + 'px'
46
- el.style.position = 'absolute'
47
- }
48
- el.style.opacity = '1'
49
- }}
50
- onExiting={() => {
51
- const el = nodeRef.current
52
- if (widthMode) {
53
- el.style.width = '0px'
54
- } else {
55
- el.style.height = '0px'
56
- el.style.transform = 'translateY(10px)'
57
- }
58
- el.style.opacity = '0'
59
- }}
60
- >
61
- <div
62
- ref={nodeRef}
63
- style={{
64
- display: 'flex',
65
- alignItems: 'center',
66
- height: '100%',
67
- overflow: 'hidden',
68
- whiteSpace: 'nowrap',
69
- transition: `all ${duration}ms ease`,
70
- top: 0,
71
- left: 0
72
- }}
73
- >
74
- {children}
75
- </div>
76
- </CSSTransition>
77
- )
78
- }
79
-
80
- export default Animated
package/src/getText.jsx DELETED
@@ -1,11 +0,0 @@
1
- import React from 'react'
2
-
3
- const getText = (children) => {
4
- if (!children) return ''
5
- if (typeof children === 'string' || typeof children === 'number') return String(children)
6
- if (Array.isArray(children)) return children.map(getText).join(' ').replace(/\s+/g, ' ').trim()
7
- if (React.isValidElement(children)) return getText(children.props.children)
8
- return ''
9
- }
10
-
11
- export default getText
package/src/icons.jsx DELETED
@@ -1,43 +0,0 @@
1
- export const XMarkIcon = ({className = '', ...props}) => (
2
- <svg
3
- className={className}
4
- role='button'
5
- aria-label='Clear selection'
6
- xmlns="http://www.w3.org/2000/svg"
7
- viewBox="0 0 320 512"
8
- width="1em"
9
- height="1em"
10
- fill="currentColor"
11
- {...props}
12
- >
13
- <path d="M310.6 361.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L160 301.3 54.6 406.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L114.7 256 9.4 150.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 210.7 265.4 105.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3L205.3 256l105.3 105.4z"/>
14
- </svg>
15
- )
16
-
17
- export const ArrowUpIcon = ({className = '', ...props}) => (
18
- <svg
19
- className={className}
20
- xmlns="http://www.w3.org/2000/svg"
21
- viewBox="0 0 448 512"
22
- width="1em"
23
- height="1em"
24
- fill="currentColor"
25
- {...props}
26
- >
27
- <path d="M34.9 289.5l175.9-175.8c9.4-9.4 24.6-9.4 33.9 0L420.1 289.5c15.1 15.1 4.4 41-17 41H51.9c-21.4 0-32.1-25.9-17-41z"/>
28
- </svg>
29
- )
30
-
31
- export const CheckmarkIcon = ({className = '', ...props}) => (
32
- <svg
33
- className={className}
34
- xmlns="http://www.w3.org/2000/svg"
35
- viewBox="0 0 24 24"
36
- width="1em"
37
- height="1em"
38
- fill="currentColor"
39
- {...props}
40
- >
41
- <path d="M20.285 6.708a1 1 0 0 0-1.414-1.416l-9.192 9.192-4.243-4.244a1 1 0 1 0-1.414 1.416l5 5a1 1 0 0 0 1.414 0l9.849-9.948z"/>
42
- </svg>
43
- )
package/src/index.js DELETED
@@ -1,5 +0,0 @@
1
- export {default as Select} from './select'
2
- export {default as Option} from './option'
3
- export {default as OptGroup} from './optgroup'
4
-
5
- import './select.css'
package/src/makeId.jsx DELETED
@@ -1,21 +0,0 @@
1
- // if user does not provide a unique identifier, a unique ID is generated based on value, filtering out unwanted characters
2
- export const makeId = (str, fallback = 'invalid-option', seed = '') => {
3
- const safeSeed = seed ? seed.replace(/:/g, '') : ''
4
-
5
- if (typeof str !== 'string' || !str.trim()) {
6
- return safeSeed ? `${fallback}-${safeSeed}` : `${fallback}-${Math.random().toString(36).slice(2, 8)}`
7
- }
8
-
9
- const cleaned = str
10
- .normalize('NFKD')
11
- .replace(/[\u0300-\u036f]/g, '')
12
- .replace(/\s+/g, '-')
13
- .replace(/[^\p{L}\p{N}-]+/gu, '')
14
- .toLowerCase()
15
-
16
- if (!cleaned) {
17
- return safeSeed ? `${fallback}-${safeSeed}` : `${fallback}-${Math.random().toString(36).slice(2, 8)}`
18
- }
19
-
20
- return cleaned || `${fallback}-${Math.random().toString(36).slice(2, 8)}`
21
- }
package/src/optgroup.jsx DELETED
@@ -1,36 +0,0 @@
1
- import {useContext, useEffect, useMemo, createContext} from 'react'
2
- import {SelectContext} from './selectContext'
3
- import {makeId} from './makeId'
4
-
5
- export const GroupContext = createContext(null)
6
-
7
- export default function OptGroup({children, name, label, value, id, emptyGroupText = 'Empty group'}) {
8
- const ctx = useContext(SelectContext)
9
-
10
- const groupName = useMemo(() => {
11
- const val = name ?? label ?? value ?? id
12
- return (val !== undefined && val !== null && val !== '') ? String(val) : emptyGroupText
13
- }, [name, label, value, id, emptyGroupText])
14
-
15
- const groupId = useMemo(() => `group-marker-${makeId(groupName)}`, [groupName])
16
-
17
- useEffect(() => {
18
- if (!ctx) return
19
-
20
- const groupMarker = {
21
- id: groupId,
22
- group: groupName,
23
- isGroupMarker: true,
24
- disabled: true
25
- }
26
-
27
- ctx.registerOption(groupMarker)
28
- return () => ctx.unregisterOption(groupId)
29
- }, [ctx.registerOption, ctx.unregisterOption, groupId, groupName])
30
-
31
- return (
32
- <GroupContext.Provider value={groupName}>
33
- {children}
34
- </GroupContext.Provider>
35
- )
36
- }
package/src/option.jsx DELETED
@@ -1,50 +0,0 @@
1
- import {useEffect, useContext, useMemo, useId} from 'react'
2
- import {SelectContext} from './selectContext'
3
- import {GroupContext} from './optgroup'
4
- import getText from './getText'
5
-
6
- export default function Option({value, id, className, children, disabled, group: manualGroup}) {
7
- const ctx = useContext(SelectContext)
8
- const contextGroup = useContext(GroupContext)
9
-
10
- const registerOption = ctx?.registerOption;
11
- const unregisterOption = ctx?.unregisterOption;
12
-
13
- const uniqueId = useId()
14
- const stableId = useMemo(() => {
15
- return id ? String(id) : uniqueId.replace(/:/g, '')
16
- }, [id, uniqueId])
17
-
18
- useEffect(() => {
19
- if (!registerOption) return
20
-
21
- const textFallback = getText(children)
22
- const hasJsx = children !== undefined && children !== null
23
- let finalLabel = ''
24
-
25
- if (textFallback) {
26
- finalLabel = textFallback
27
- } else if (id !== undefined && id !== null && id !== '') {
28
- finalLabel = String(id)
29
- } else if (value !== undefined && value !== null && value !== '') {
30
- finalLabel = String(value)
31
- }
32
-
33
- const option = {
34
- id: stableId,
35
- value: value !== undefined ? value : textFallback,
36
- label: finalLabel,
37
- jsx: children,
38
- hasJsx,
39
- className,
40
- disabled: !!disabled,
41
- group: manualGroup || contextGroup || null
42
- }
43
-
44
- registerOption(option)
45
- return () => unregisterOption(stableId)
46
-
47
- }, [stableId, value, children, className, disabled, manualGroup, contextGroup, registerOption, unregisterOption])
48
-
49
- return null
50
- }