react-animated-select 0.2.9 → 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,84 +1,134 @@
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
- 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)
20
+ const loadingTriggered = useRef(false)
13
21
  const [highlightedIndex, setHighlightedIndex] = useState(-1)
14
22
 
23
+ // loading state synchronization
15
24
  useEffect(() => {
16
- const handleWindowFocus = () => {
17
- lastWindowFocusTime.current = Date.now()
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)
30
+ }
31
+ }, [options.length, hasMore, loadButton, loadButtonText, setLoadingTitle])
32
+
33
+ // safely call loadMore prop
34
+ const safeLoadMore = useCallback(() => {
35
+ if (!hasMore || loadingTriggered.current) return
36
+ loadingTriggered.current = true
37
+ loadMore()
38
+ }, [hasMore, loadMore])
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
42
+ const handleListScroll = useCallback((e) => {
43
+ if (loadButton || !hasMore || loadingTriggered.current) return
44
+
45
+ const {scrollTop, scrollHeight, clientHeight} = e.currentTarget
46
+ if (scrollHeight - scrollTop <= clientHeight + loadOffset) {
47
+ safeLoadMore()
48
+ }
49
+ }, [loadButton, hasMore, loadOffset, safeLoadMore])
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
+ useEffect(() => {
54
+ if (!loadButton && open && hasMore && highlightedIndex >= options.length - loadAhead) {
55
+ safeLoadMore()
18
56
  }
57
+ }, [highlightedIndex, open, hasMore, options.length, loadAhead, loadButton, safeLoadMore])
58
+
59
+ // force refocus blocking if the user exits the browser or the page
60
+ useEffect(() => {
61
+ const handleWindowFocus = () => {lastWindowFocusTime.current = Date.now()}
19
62
  window.addEventListener('focus', handleWindowFocus)
20
63
  return () => window.removeEventListener('focus', handleWindowFocus)
21
64
  }, [])
22
65
 
66
+ // set highlighting to the first available option by default unless otherwise selected
23
67
  useEffect(() => {
24
- if (isOpen) {
25
- let index = -1
26
- if (selected) {
27
- const selectedIndex = options.findIndex(o => o.id === selected.id && !o.disabled)
28
- if (selectedIndex >= 0) index = selectedIndex
29
- }
30
- if (index === -1) {
31
- index = options.findIndex(o => !o.disabled)
32
- }
33
- setHighlightedIndex(index)
34
- } else {
68
+ if (!open) {
35
69
  setHighlightedIndex(-1)
70
+ return
36
71
  }
37
- }, [isOpen, selected, options])
38
72
 
39
- const handleBlur = useCallback((e) => {
40
- if (e.currentTarget.contains(e.relatedTarget)) return
41
- setIsOpen(false)
42
- }, [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
43
75
 
44
- const handleFocus = useCallback(() => {
45
- if (disabled) return
76
+ let index = -1
77
+ if (selected) {
78
+ index = options.findIndex(o => o.id === selected.id && !o.disabled)
79
+ }
46
80
 
47
- if (document.hidden) return
81
+ if (index === -1) {
82
+ index = options.findIndex(o => !o.disabled)
83
+ }
84
+ setHighlightedIndex(index)
85
+ }, [open, options, selected])
48
86
 
49
- const timeSinceWindowFocus = Date.now() - lastWindowFocusTime.current
50
- if (timeSinceWindowFocus < 100) return
87
+ // find the next available option to switch to using the keyboard
88
+ const getNextIndex = useCallback((current, direction) => {
89
+ const isNavigable = (opt) => opt && !opt.disabled && !opt.loading
90
+ const len = options.length
91
+ if (len === 0) return -1
51
92
 
52
- if (!isOpen) {
53
- setIsOpen(true)
54
- justFocused.current = true
55
- setTimeout(() => {
56
- justFocused.current = false
57
- }, 200)
93
+ let next = current
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
102
+ }
103
+
104
+ if (isNavigable(options[next])) return next
58
105
  }
59
- }, [disabled, isOpen, setIsOpen])
106
+ return current
107
+ }, [options, hasMore, loadButton])
60
108
 
61
- const handleToggle = useCallback((e) => {
62
- if (disabled) return
63
- if (e.target.closest('.rac-select-cancel')) return
64
- if (justFocused.current) return
109
+ // closing the selector if focus is lost
110
+ const handleBlur = useCallback((e) => {
111
+ if (!e.currentTarget.contains(e.relatedTarget)) setOpen(false)
112
+ }, [setOpen])
65
113
 
66
- setIsOpen(!isOpen)
67
- }, [disabled, isOpen, setIsOpen])
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])
68
124
 
69
- const getNextIndex = (current, direction) => {
70
- if (options.every(o => o.disabled)) return -1
71
- let next = current
72
- let loops = 0
73
- do {
74
- next += direction
75
- if (next < 0) next = options.length - 1
76
- if (next >= options.length) next = 0
77
- loops++
78
- } while (options[next]?.disabled && loops <= options.length)
79
- return next
80
- }
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])
81
130
 
131
+ // hotkey processing
82
132
  const handleKeyDown = useCallback((e) => {
83
133
  if (disabled) return
84
134
 
@@ -86,50 +136,34 @@ function useSelect({
86
136
  case 'Enter':
87
137
  case ' ':
88
138
  e.preventDefault()
89
- if (isOpen) {
139
+ if (open) {
90
140
  if (highlightedIndex !== -1 && options[highlightedIndex]) {
91
141
  selectOption(options[highlightedIndex], e)
92
142
  }
93
- } else {
94
- setIsOpen(true)
95
- }
143
+ } else setOpen(true)
96
144
  break
97
145
  case 'Escape':
98
146
  e.preventDefault()
99
- setIsOpen(false)
147
+ setOpen(false)
100
148
  break
101
149
  case 'ArrowDown':
102
150
  e.preventDefault()
103
- if (!isOpen) {
104
- setIsOpen(true)
105
- } else {
106
- setHighlightedIndex(prev => getNextIndex(prev, 1))
107
- }
151
+ open ? setHighlightedIndex(prev => getNextIndex(prev, 1)) : setOpen(true)
108
152
  break
109
153
  case 'ArrowUp':
110
154
  e.preventDefault()
111
- if (!isOpen) {
112
- setIsOpen(true)
113
- } else {
114
- setHighlightedIndex(prev => getNextIndex(prev, -1))
115
- }
155
+ open ? setHighlightedIndex(prev => getNextIndex(prev, -1)) : setOpen(true)
116
156
  break
117
157
  case 'Tab':
118
- if (isOpen) setIsOpen(false)
119
- break
120
- default:
158
+ if (open) setOpen(false)
121
159
  break
122
160
  }
123
- }, [disabled, isOpen, setIsOpen, highlightedIndex, options, selectOption])
161
+ }, [disabled, open, setOpen, highlightedIndex, options, selectOption, getNextIndex])
124
162
 
125
163
  return useMemo(() => ({
126
- handleBlur,
127
- handleFocus,
128
- handleToggle,
129
- handleKeyDown,
130
- highlightedIndex,
131
- setHighlightedIndex
132
- }), [handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, setHighlightedIndex])
164
+ handleBlur, handleFocus, handleToggle, handleKeyDown,
165
+ highlightedIndex, setHighlightedIndex, handleListScroll
166
+ }), [handleBlur, handleFocus, handleToggle, handleKeyDown, highlightedIndex, handleListScroll])
133
167
  }
134
168
 
135
169
  export default useSelect