@@ -10,8 +10,7 @@ import {
10
10
faCircleXmark ,
11
11
faXmark ,
12
12
} from "@fortawesome/pro-solid-svg-icons" ;
13
- import { useRef } from "react" ;
14
- import { assertIsNode } from "../../utils/type_guards" ;
13
+ import { isNode } from "../../utils/type_guards" ;
15
14
16
15
type Option = {
17
16
label : string ;
@@ -20,8 +19,10 @@ type Option = {
20
19
type Props = {
21
20
options : Option [ ] ;
22
21
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23
- onChange : ( v : any ) => void ;
22
+ value : Option [ ] ;
23
+ onChange : ( v : Option [ ] ) => void ;
24
24
onBlur : ( ) => void ;
25
+ // TODO: Implement disabled state
25
26
disabled ?: boolean ;
26
27
name : string ;
27
28
placeholder ?: string ;
@@ -34,77 +35,66 @@ type Props = {
34
35
*/
35
36
export const MultiSelectSearch = React . forwardRef < HTMLInputElement , Props > (
36
37
function MultiSelectSearch ( props , ref ) {
37
- const { options, onChange, onBlur, placeholder, color } = props ;
38
- const menuRef = useRef < HTMLDivElement | null > ( null ) ;
39
- const inputRef = useRef < HTMLInputElement | null > ( null ) ;
38
+ const { name, options, value, onChange, onBlur, placeholder, color } =
39
+ props ;
40
+ const menuRef = React . useRef < HTMLDivElement | null > ( null ) ;
41
+ const inputRef = React . useRef < HTMLInputElement | null > ( null ) ;
40
42
41
43
const [ isVisible , setIsVisible ] = React . useState ( false ) ;
42
44
const [ search , setSearch ] = React . useState ( "" ) ;
43
- const [ selected , setList ] = React . useState < Option [ ] > ( [ ] ) ;
44
45
const [ filteredOptions , setFilteredOptions ] = React . useState ( options ) ;
45
46
46
47
function add ( incomingOption : Option ) {
47
- if ( ! selected . some ( ( option ) => option . value === incomingOption . value ) ) {
48
- setList ( [ ...selected , incomingOption ] ) ;
48
+ if ( ! value . some ( ( option ) => option . value === incomingOption . value ) ) {
49
+ onChange ( [ ...value , incomingOption ] ) ;
49
50
}
50
51
}
51
52
52
53
function remove ( incomingOption : Option ) {
53
- setList (
54
- selected . filter ( ( option ) => option . value !== incomingOption . value )
55
- ) ;
56
- }
57
-
58
- // Helper function for preventing focusing on input, when clicking on a child.
59
- function handleParentClick (
60
- event : React . MouseEvent | React . KeyboardEvent ,
61
- onClick : ( ) => void
62
- ) {
63
- onClick ( ) ;
64
- event . stopPropagation ( ) ;
54
+ onChange ( value . filter ( ( option ) => option . value !== incomingOption . value ) ) ;
65
55
}
66
56
67
- const closeOpenMenu = React . useCallback (
57
+ const hideMenu = React . useCallback (
68
58
( e : MouseEvent | TouchEvent ) => {
69
59
if (
70
60
menuRef . current &&
71
61
isVisible &&
72
- ! menuRef . current . contains ( assertIsNode ( e . target ) ? e . target : null )
62
+ ! menuRef . current . contains ( isNode ( e . target ) ? e . target : null )
73
63
) {
74
64
setIsVisible ( false ) ;
75
65
onBlur ( ) ;
76
66
}
77
67
} ,
78
- [ setIsVisible , isVisible ]
68
+ [ setIsVisible , isVisible , onBlur , menuRef ]
79
69
) ;
80
70
81
71
// Event listeners for closing menu when clicking or touching outside.
82
72
React . useEffect ( ( ) => {
83
- document . addEventListener ( "mousedown" , closeOpenMenu ) ;
84
- document . addEventListener ( "touchstart" , closeOpenMenu ) ;
73
+ document . addEventListener ( "mousedown" , hideMenu ) ;
74
+ document . addEventListener ( "touchstart" , hideMenu ) ;
85
75
return ( ) => {
86
- document . removeEventListener ( "mousedown" , closeOpenMenu ) ;
87
- document . removeEventListener ( "touchstart" , closeOpenMenu ) ;
76
+ document . removeEventListener ( "mousedown" , hideMenu ) ;
77
+ document . removeEventListener ( "touchstart" , hideMenu ) ;
88
78
} ;
89
79
} ) ;
90
80
91
81
React . useEffect ( ( ) => {
92
- onChange ( selected ) ;
93
82
const updatedOptions = options . filter (
94
- ( option ) => ! selected . includes ( option ) && option . label . includes ( search )
83
+ ( option ) =>
84
+ ! value . includes ( option ) &&
85
+ option . label . toLowerCase ( ) . includes ( search . toLowerCase ( ) )
95
86
) ;
96
87
setFilteredOptions ( updatedOptions ) ;
97
- } , [ selected , search ] ) ;
88
+ } , [ value , search , options , setFilteredOptions ] ) ;
98
89
99
90
return (
100
91
< div className = { styles . root } ref = { ref } >
101
- < div className = { styles . selected } >
102
- { selected . map ( ( option , key ) => {
92
+ < div className = { styles . value } >
93
+ { value . map ( ( option ) => {
103
94
return (
104
95
< Button . Root
105
- key = { key }
96
+ key = { option . value }
106
97
onClick = { ( ) => remove ( option ) }
107
- colorScheme = "default"
108
98
paddingSize = "small"
109
99
style = { { gap : "0.5rem" } }
110
100
>
@@ -116,20 +106,19 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
116
106
) ;
117
107
} ) }
118
108
</ div >
119
- { /* eslint-disable-next-line jsx-a11y/click-events-have-key-events */ }
120
109
< div
121
110
className = { styles . search }
122
- onClick = { ( e ) =>
123
- handleParentClick ( e , ( ) => {
124
- inputRef . current && inputRef . current . focus ( ) ;
125
- } )
126
- }
111
+ onFocus = { ( e ) => {
112
+ e . stopPropagation ( ) ;
113
+ inputRef . current && inputRef . current . focus ( ) ;
114
+ } }
127
115
role = "button"
128
116
tabIndex = { 0 }
129
117
ref = { menuRef }
130
118
>
131
119
< div className = { styles . inputContainer } data-color = { color } >
132
120
< input
121
+ name = { name }
133
122
className = { styles . input }
134
123
value = { search }
135
124
data-color = { color }
@@ -139,7 +128,10 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
139
128
placeholder = { placeholder }
140
129
/>
141
130
< button
142
- onClick = { ( e ) => handleParentClick ( e , ( ) => setList ( [ ] ) ) }
131
+ onClick = { ( e ) => {
132
+ e . stopPropagation ( ) ;
133
+ setSearch ( "" ) ;
134
+ } }
143
135
className = { styles . removeAllButton }
144
136
>
145
137
< Icon inline >
@@ -148,9 +140,10 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
148
140
</ button >
149
141
< div className = { styles . inputButtonDivider } > </ div >
150
142
< button
151
- onClick = { ( e ) =>
152
- handleParentClick ( e , ( ) => setIsVisible ( ! isVisible ) )
153
- }
143
+ onClick = { ( e ) => {
144
+ e . stopPropagation ( ) ;
145
+ setIsVisible ( ! isVisible ) ;
146
+ } }
154
147
className = { styles . openMenuButton }
155
148
>
156
149
< Icon inline >
@@ -164,11 +157,14 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
164
157
isVisible ? styles . visible : null
165
158
) }
166
159
>
167
- { filteredOptions . map ( ( option , key ) => {
160
+ { filteredOptions . map ( ( option ) => {
168
161
return (
169
162
< MultiSelectItem
170
- key = { key }
171
- onClick = { ( e ) => handleParentClick ( e , ( ) => add ( option ) ) }
163
+ key = { option . value }
164
+ onClick = { ( e ) => {
165
+ e . stopPropagation ( ) ;
166
+ add ( option ) ;
167
+ } }
172
168
option = { option }
173
169
/>
174
170
) ;
@@ -183,18 +179,13 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
183
179
MultiSelectSearch . displayName = "MultiSelectSearch" ;
184
180
185
181
const MultiSelectItem = ( props : {
186
- key : number ;
187
182
onClick : ( e : React . MouseEvent | React . KeyboardEvent ) => void ;
188
183
option : Option ;
189
- focus ?: boolean ;
190
184
} ) => {
191
185
return (
192
186
< div
193
- className = { classnames (
194
- styles . multiSelectItemWrapper ,
195
- props . focus ? styles . highlighted : null
196
- ) }
197
- onClick = { ( e ) => props . onClick ( e ) }
187
+ className = { styles . multiSelectItemWrapper }
188
+ onClick = { props . onClick }
198
189
onKeyDown = { ( e ) => ( e . code === "Enter" ? props . onClick ( e ) : null ) }
199
190
tabIndex = { 0 }
200
191
role = "button"
0 commit comments