react-animated-select 0.3.1 → 0.3.6

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.
@@ -0,0 +1,148 @@
1
+ import {memo} from 'react'
2
+ import {SelectContext} from './selectContext'
3
+ import Options from './options'
4
+ import SlideLeft from './slideLeft'
5
+
6
+ const SelectJSX = memo(({
7
+ selectRef,
8
+ selectId,
9
+
10
+ renderOptions,
11
+ selected,
12
+ title,
13
+ visibility,
14
+ active,
15
+ hasOptions,
16
+ hasActualValue,
17
+
18
+ disabled,
19
+ loading,
20
+ error,
21
+
22
+ registerOption,
23
+ unregisterOption,
24
+ handleBlur,
25
+ handleFocus,
26
+ handleToggle,
27
+ handleKeyDown,
28
+ handleListScroll,
29
+ setAnimationFinished,
30
+ clear,
31
+
32
+ children,
33
+ renderedDropdown,
34
+ placeholder,
35
+ className,
36
+ style,
37
+ duration,
38
+ easing,
39
+ offset,
40
+ animateOpacity,
41
+ unmount,
42
+ ArrowIcon,
43
+ ClearIcon,
44
+ renderIcon,
45
+ hasMore,
46
+ loadButton
47
+ }) => {
48
+ return (
49
+ <SelectContext.Provider
50
+ value={{registerOption, unregisterOption}}
51
+ >
52
+ {children}
53
+ {renderedDropdown}
54
+ <div
55
+ ref={selectRef}
56
+ style={{'--rac-duration': `${duration}ms`, ...style}}
57
+ className={
58
+ `rac-select
59
+ ${className}
60
+ ${(!hasOptions || disabled) ? 'rac-disabled-style' : ''}
61
+ ${loading ? 'rac-loading-style' : ''}
62
+ ${error ? 'rac-error-style' : ''}`}
63
+ tabIndex={active ? 0 : -1}
64
+ role='combobox'
65
+ aria-haspopup='listbox'
66
+ aria-expanded={visibility}
67
+ aria-controls={`${selectId}-listbox`}
68
+ aria-label={placeholder}
69
+ aria-disabled={disabled || !hasOptions}
70
+ {...(active && {
71
+ onBlur: handleBlur,
72
+ onFocus: handleFocus,
73
+ onClick: handleToggle,
74
+ onKeyDown: handleKeyDown
75
+ })}
76
+ >
77
+ <div className={`rac-select-title ${(!error && !loading && selected?.type === 'boolean') ? (selected.raw ? 'rac-true-option' : 'rac-false-option') : ''}`}>
78
+ <span className='rac-title-text' key={title}>{title}</span>
79
+ <SlideLeft visibility={loading && !error} duration={duration}>
80
+ <span className='rac-loading-dots'><i/><i/><i/></span>
81
+ </SlideLeft>
82
+ </div>
83
+
84
+ <div className='rac-select-buttons'>
85
+ <SlideLeft
86
+ visibility={hasActualValue && hasOptions && !disabled && !loading && !error}
87
+ duration={duration}
88
+ style={{display: 'grid'}}
89
+ >
90
+ {renderIcon(ClearIcon, {
91
+ className: 'rac-select-cancel',
92
+ onMouseDown: e => {
93
+ e.preventDefault()
94
+ e.stopPropagation()
95
+ },
96
+ onClick: clear
97
+ })}
98
+ </SlideLeft>
99
+ <SlideLeft
100
+ visibility={active}
101
+ duration={duration}
102
+ style={{display: 'grid'}}
103
+ >
104
+ <span
105
+ className={`rac-select-arrow-wrapper ${visibility ? '--open' : ''}`}
106
+ >
107
+ {renderIcon(ArrowIcon, {
108
+ className: 'rac-select-arrow-wrapper'
109
+ })}
110
+ </span>
111
+ </SlideLeft>
112
+ </div>
113
+
114
+ <Options
115
+ visibility={visibility}
116
+ selectRef={selectRef}
117
+ onAnimationDone={() => setAnimationFinished(true)}
118
+ unmount={unmount}
119
+ duration={duration}
120
+ easing={easing}
121
+ offset={offset}
122
+ animateOpacity={animateOpacity}
123
+ >
124
+ <div
125
+ onScroll={handleListScroll}
126
+ tabIndex='-1'
127
+ className='rac-select-list'
128
+ role='listbox'
129
+ aria-label='Options'
130
+ >
131
+ {renderOptions}
132
+ {!loadButton && hasMore && (
133
+ <div
134
+ className='rac-select-option rac-disabled-option rac-loading-option'
135
+ onClick={e => e.stopPropagation()}
136
+ >
137
+ <span className='rac-loading-option-title'>Loading</span>
138
+ <span className='rac-loading-dots'><i/><i/><i/></span>
139
+ </div>
140
+ )}
141
+ </div>
142
+ </Options>
143
+ </div>
144
+ </SelectContext.Provider>
145
+ )
146
+ })
147
+
148
+ export default SelectJSX
@@ -0,0 +1,36 @@
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/useSelect.jsx CHANGED
@@ -1,10 +1,10 @@
1
- import {useState, useRef, useCallback, useMemo, useEffect} from 'react'
1
+ import {useState, useRef, useCallback, useEffect, useMemo} from 'react'
2
2
 
3
3
  function useSelect({
4
4
  disabled,
5
- isOpen,
6
- setIsOpen,
7
- options,
5
+ open,
6
+ setOpen,
7
+ options = [],
8
8
  selectOption,
9
9
  selected,
10
10
  hasMore,
@@ -17,136 +17,118 @@ function useSelect({
17
17
  }) {
18
18
  const justFocused = useRef(false)
19
19
  const lastWindowFocusTime = useRef(0)
20
- const [highlightedIndex, setHighlightedIndex] = useState(-1)
21
-
22
20
  const loadingTriggered = useRef(false)
21
+ const [highlightedIndex, setHighlightedIndex] = useState(-1)
23
22
 
24
- const prevOptionsLength = useRef(options?.length || 0)
25
-
23
+ // loading state synchronization
26
24
  useEffect(() => {
27
- if (options && options.length !== prevOptionsLength.current) {
28
- loadingTriggered.current = false
29
- prevOptionsLength.current = options.length
30
- loadButton && setLoadingTitle(loadButtonText)
25
+ // flag is reset if value of the loadButton or hasMore props has changed
26
+ loadingTriggered.current = false
27
+
28
+ if (loadButton) {
29
+ setLoadingTitle(loadButtonText)
31
30
  }
32
- }, [options])
31
+ }, [options.length, hasMore, loadButton, loadButtonText, setLoadingTitle])
33
32
 
33
+ // safely call loadMore prop
34
34
  const safeLoadMore = useCallback(() => {
35
35
  if (!hasMore || loadingTriggered.current) return
36
-
37
36
  loadingTriggered.current = true
38
37
  loadMore()
39
38
  }, [hasMore, loadMore])
40
39
 
40
+ // calling a function when scrolling almost to the end;
41
+ // loadOffset is a prop indicating how many pixels before end loadMore will be called
41
42
  const handleListScroll = useCallback((e) => {
42
43
  if (loadButton || !hasMore || loadingTriggered.current) return
43
44
 
44
45
  const {scrollTop, scrollHeight, clientHeight} = e.currentTarget
45
-
46
- const threshold = loadOffset
47
-
48
- if (scrollHeight - scrollTop <= clientHeight + threshold) {
46
+ if (scrollHeight - scrollTop <= clientHeight + loadOffset) {
49
47
  safeLoadMore()
50
48
  }
51
- }, [loadButton, hasMore, safeLoadMore])
49
+ }, [loadButton, hasMore, loadOffset, safeLoadMore])
52
50
 
51
+ // call a function when scrolling through options using keys;
52
+ // loadAhead prop how many options before the end it will be called
53
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
- }
54
+ if (!loadButton && open && hasMore && highlightedIndex >= options.length - loadAhead) {
55
+ safeLoadMore()
61
56
  }
62
- }, [loadButton, highlightedIndex, isOpen, hasMore, options.length, safeLoadMore])
57
+ }, [highlightedIndex, open, hasMore, options.length, loadAhead, loadButton, safeLoadMore])
63
58
 
59
+ // force refocus blocking if the user exits the browser or the page
64
60
  useEffect(() => {
65
- const handleWindowFocus = () => {
66
- lastWindowFocusTime.current = Date.now()
67
- }
61
+ const handleWindowFocus = () => {lastWindowFocusTime.current = Date.now()}
68
62
  window.addEventListener('focus', handleWindowFocus)
69
63
  return () => window.removeEventListener('focus', handleWindowFocus)
70
64
  }, [])
71
65
 
66
+ // set highlighting to the first available option by default unless otherwise selected
72
67
  useEffect(() => {
73
- if (isOpen) {
74
- if (highlightedIndex >= 0 && highlightedIndex < options.length) {
75
- return
76
- }
77
-
78
- let index = -1
79
- if (selected) {
80
- const selectedIndex = options.findIndex(o => o.id === selected.id && !o.disabled)
81
- if (selectedIndex >= 0) index = selectedIndex
82
- }
83
- if (index === -1) {
84
- index = options.findIndex(o => !o.disabled)
85
- }
86
- setHighlightedIndex(index)
87
- } else {
68
+ if (!open) {
88
69
  setHighlightedIndex(-1)
70
+ return
89
71
  }
90
- }, [isOpen, options])
91
72
 
92
- const handleBlur = useCallback((e) => {
93
- if (e.currentTarget.contains(e.relatedTarget)) return
94
- setIsOpen(false)
95
- }, [setIsOpen])
73
+ // blocking the reset of an index if it is already within the array (exmpl after loading)
74
+ if (highlightedIndex >= 0 && highlightedIndex < options.length) return
96
75
 
97
- const handleFocus = useCallback(() => {
98
- if (disabled) return
76
+ let index = -1
77
+ if (selected) {
78
+ index = options.findIndex(o => o.id === selected.id && !o.disabled)
79
+ }
99
80
 
100
- if (document.hidden) return
101
-
102
- const timeSinceWindowFocus = Date.now() - lastWindowFocusTime.current
103
- if (timeSinceWindowFocus < 100) return
104
-
105
- if (!isOpen) {
106
- setIsOpen(true)
107
- justFocused.current = true
108
- setTimeout(() => {
109
- justFocused.current = false
110
- }, 200)
81
+ if (index === -1) {
82
+ index = options.findIndex(o => !o.disabled)
111
83
  }
112
- }, [disabled, isOpen, setIsOpen])
113
-
114
- const handleToggle = useCallback((e) => {
115
- if (disabled) return
116
- if (e.target.closest && e.target.closest('.rac-select-cancel')) return
117
- if (justFocused.current) return
118
-
119
- setIsOpen(!isOpen)
120
- }, [disabled, isOpen, setIsOpen])
84
+ setHighlightedIndex(index)
85
+ }, [open, options, selected])
121
86
 
122
- const getNextIndex = (current, direction) => {
87
+ // find the next available option to switch to using the keyboard
88
+ const getNextIndex = useCallback((current, direction) => {
123
89
  const isNavigable = (opt) => opt && !opt.disabled && !opt.loading
124
-
125
- if (!options.some(isNavigable)) return -1
90
+ const len = options.length
91
+ if (len === 0) return -1
126
92
 
127
93
  let next = current
128
- let loops = 0
129
-
130
- do {
131
- next += direction
132
-
133
- if (next >= options.length) {
134
- if (hasMore && !loadButton) return current
135
- next = 0
94
+ // я не шарю нихуя в математике
95
+ for (let i = 0; i < len; i++) {
96
+ next = (next + direction + len) % len
97
+
98
+ // if autoloading is active but loadButton is inactive, then infinite scrolling is blocked
99
+ if (!loadButton && hasMore) {
100
+ if (direction > 0 && next === 0) return current
101
+ if (direction < 0 && next === len - 1) return current
136
102
  }
137
103
 
138
- if (next < 0) {
139
- if (hasMore && !loadButton) return current
140
- next = options.length - 1
141
- }
104
+ if (isNavigable(options[next])) return next
105
+ }
106
+ return current
107
+ }, [options, hasMore, loadButton])
142
108
 
143
- loops++
144
- } while (!isNavigable(options[next]) && loops <= options.length)
109
+ // closing the selector if focus is lost
110
+ const handleBlur = useCallback((e) => {
111
+ if (!e.currentTarget.contains(e.relatedTarget)) setOpen(false)
112
+ }, [setOpen])
145
113
 
146
- return next
147
- }
114
+ // opening the selector when receiving focus
115
+ const handleFocus = useCallback(() => {
116
+ if (disabled || document.hidden || (Date.now() - lastWindowFocusTime.current < 100)) return
117
+
118
+ if (!open) {
119
+ setOpen(true)
120
+ justFocused.current = true
121
+ setTimeout(() => {justFocused.current = false}, 200)
122
+ }
123
+ }, [disabled, open, setOpen])
148
124
 
125
+ // processing toggle click on select
126
+ const handleToggle = useCallback((e) => {
127
+ if (disabled || e.target.closest('.rac-select-cancel') || justFocused.current) return
128
+ setOpen(!open)
129
+ }, [disabled, open, setOpen])
149
130
 
131
+ // hotkey processing
150
132
  const handleKeyDown = useCallback((e) => {
151
133
  if (disabled) return
152
134
 
@@ -154,51 +136,34 @@ function useSelect({
154
136
  case 'Enter':
155
137
  case ' ':
156
138
  e.preventDefault()
157
- if (isOpen) {
139
+ if (open) {
158
140
  if (highlightedIndex !== -1 && options[highlightedIndex]) {
159
141
  selectOption(options[highlightedIndex], e)
160
142
  }
161
- } else {
162
- setIsOpen(true)
163
- }
143
+ } else setOpen(true)
164
144
  break
165
145
  case 'Escape':
166
146
  e.preventDefault()
167
- setIsOpen(false)
147
+ setOpen(false)
168
148
  break
169
149
  case 'ArrowDown':
170
150
  e.preventDefault()
171
- if (!isOpen) {
172
- setIsOpen(true)
173
- } else {
174
- setHighlightedIndex(prev => getNextIndex(prev, 1))
175
- }
151
+ open ? setHighlightedIndex(prev => getNextIndex(prev, 1)) : setOpen(true)
176
152
  break
177
153
  case 'ArrowUp':
178
154
  e.preventDefault()
179
- if (!isOpen) {
180
- setIsOpen(true)
181
- } else {
182
- setHighlightedIndex(prev => getNextIndex(prev, -1))
183
- }
155
+ open ? setHighlightedIndex(prev => getNextIndex(prev, -1)) : setOpen(true)
184
156
  break
185
157
  case 'Tab':
186
- if (isOpen) setIsOpen(false)
187
- break
188
- default:
158
+ if (open) setOpen(false)
189
159
  break
190
160
  }
191
- }, [disabled, isOpen, setIsOpen, highlightedIndex, options, selectOption])
161
+ }, [disabled, open, setOpen, highlightedIndex, options, selectOption, getNextIndex])
192
162
 
193
163
  return useMemo(() => ({
194
- handleBlur,
195
- handleFocus,
196
- handleToggle,
197
- handleKeyDown,
198
- highlightedIndex,
199
- setHighlightedIndex,
200
- handleListScroll
201
- }), [handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, setHighlightedIndex])
164
+ handleBlur, handleFocus, handleToggle, handleKeyDown,
165
+ highlightedIndex, setHighlightedIndex, handleListScroll
166
+ }), [handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, handleListScroll])
202
167
  }
203
168
 
204
169
  export default useSelect