@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
14
14
limitations under the License.
15
15
*/
16
16
17
- import React , { useState , ReactNode , ChangeEvent , KeyboardEvent , useRef } from 'react' ;
17
+ import React , { useState , ReactNode , ChangeEvent , KeyboardEvent , useRef , ReactElement } from 'react' ;
18
18
import classNames from 'classnames' ;
19
19
20
20
import Autocompleter from "../../autocomplete/AutocompleteProvider" ;
@@ -23,15 +23,16 @@ import { ICompletion } from '../../autocomplete/Autocompleter';
23
23
import AccessibleButton from '../../components/views/elements/AccessibleButton' ;
24
24
import { Icon as PillRemoveIcon } from '../../../res/img/icon-pill-remove.svg' ;
25
25
import { Icon as CheckmarkIcon } from '../../../res/img/element-icons/roomlist/checkmark.svg' ;
26
+ import useFocus from "../../hooks/useFocus" ;
26
27
27
28
interface AutocompleteInputProps {
28
29
provider : Autocompleter ;
29
30
placeholder : string ;
30
31
selection : ICompletion [ ] ;
31
32
onSelectionChange : ( selection : ICompletion [ ] ) => void ;
32
33
maxSuggestions ?: number ;
33
- renderSuggestion ?: ( s : ICompletion ) => ReactNode ;
34
- renderSelection ?: ( m : ICompletion ) => ReactNode ;
34
+ renderSuggestion ?: ( s : ICompletion ) => ReactElement ;
35
+ renderSelection ?: ( m : ICompletion ) => ReactElement ;
35
36
additionalFilter ?: ( suggestion : ICompletion ) => boolean ;
36
37
}
37
38
@@ -47,9 +48,9 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
47
48
} ) => {
48
49
const [ query , setQuery ] = useState < string > ( '' ) ;
49
50
const [ suggestions , setSuggestions ] = useState < ICompletion [ ] > ( [ ] ) ;
50
- const [ isFocused , setFocused ] = useState < boolean > ( false ) ;
51
- const editorContainerRef = useRef < HTMLDivElement > ( ) ;
52
- const editorRef = useRef < HTMLInputElement > ( ) ;
51
+ const [ isFocused , onFocusChangeHandlerFunctions ] = useFocus ( ) ;
52
+ const editorContainerRef = useRef < HTMLDivElement > ( null ) ;
53
+ const editorRef = useRef < HTMLInputElement > ( null ) ;
53
54
54
55
const focusEditor = ( ) => {
55
56
editorRef ?. current ?. focus ( ) ;
@@ -111,72 +112,6 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
111
112
}
112
113
} ;
113
114
114
- const _renderSuggestion = ( completion : ICompletion ) : ReactNode => {
115
- const isSelected = selection . findIndex ( selection => selection . completionId === completion . completionId ) >= 0 ;
116
- const classes = classNames ( {
117
- 'mx_AutocompleteInput_suggestion' : true ,
118
- 'mx_AutocompleteInput_suggestion--selected' : isSelected ,
119
- } ) ;
120
-
121
- const withContainer = ( children : ReactNode ) : ReactNode => (
122
- < div className = { classes }
123
- onMouseDown = { ( e ) => {
124
- e . preventDefault ( ) ;
125
- e . stopPropagation ( ) ;
126
-
127
- toggleSelection ( completion ) ;
128
- } }
129
- key = { completion . completionId }
130
- data-testid = { `autocomplete-suggestion-item-${ completion . completionId } ` }
131
- >
132
- < div >
133
- { children }
134
- </ div >
135
- { isSelected && < CheckmarkIcon height = { 16 } width = { 16 } /> }
136
- </ div >
137
- ) ;
138
-
139
- if ( renderSuggestion ) {
140
- return withContainer ( renderSuggestion ( completion ) ) ;
141
- }
142
-
143
- return withContainer (
144
- < >
145
- < span className = 'mx_AutocompleteInput_suggestion_title' > { completion . completion } </ span >
146
- < span className = 'mx_AutocompleteInput_suggestion_description' > { completion . completionId } </ span >
147
- </ > ,
148
- ) ;
149
- } ;
150
-
151
- const _renderSelection = ( s : ICompletion ) : ReactNode => {
152
- const withContainer = ( children : ReactNode ) : ReactNode => (
153
- < span
154
- className = 'mx_AutocompleteInput_editor_selection'
155
- key = { s . completionId }
156
- data-testid = { `autocomplete-selection-item-${ s . completionId } ` }
157
- >
158
- < span className = 'mx_AutocompleteInput_editor_selection_pill' >
159
- { children }
160
- </ span >
161
- < AccessibleButton
162
- className = 'mx_AutocompleteInput_editor_selection_remove'
163
- onClick = { ( ) => removeSelection ( s ) }
164
- data-testid = { `autocomplete-selection-remove-button-${ s . completionId } ` }
165
- >
166
- < PillRemoveIcon width = { 8 } height = { 8 } />
167
- </ AccessibleButton >
168
- </ span >
169
- ) ;
170
-
171
- if ( renderSelection ) {
172
- return withContainer ( renderSelection ( s ) ) ;
173
- }
174
-
175
- return withContainer (
176
- < span className = 'mx_AutocompleteInput_editor_selection_text' > { s . completion } </ span > ,
177
- ) ;
178
- } ;
179
-
180
115
const hasPlaceholder = ( ) : boolean => selection . length === 0 && query . length === 0 ;
181
116
182
117
return (
@@ -187,18 +122,26 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
187
122
onClick = { onClickInputArea }
188
123
data-testid = "autocomplete-editor"
189
124
>
190
- { selection . map ( s => _renderSelection ( s ) ) }
125
+ {
126
+ selection . map ( item => (
127
+ < SelectionItem
128
+ key = { item . completionId }
129
+ item = { item }
130
+ onClick = { removeSelection }
131
+ render = { renderSelection }
132
+ />
133
+ ) )
134
+ }
191
135
< input
192
136
ref = { editorRef }
193
137
type = "text"
194
138
onKeyDown = { onKeyDown }
195
139
onChange = { onQueryChange }
196
140
value = { query }
197
141
autoComplete = "off"
198
- placeholder = { hasPlaceholder ( ) ? placeholder : null }
199
- onFocus = { ( ) => setFocused ( true ) }
200
- onBlur = { ( ) => setFocused ( false ) }
142
+ placeholder = { hasPlaceholder ( ) ? placeholder : undefined }
201
143
data-testid = "autocomplete-input"
144
+ { ...onFocusChangeHandlerFunctions }
202
145
/>
203
146
</ div >
204
147
{
@@ -209,11 +152,96 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
209
152
data-testid = "autocomplete-matches"
210
153
>
211
154
{
212
- suggestions . map ( ( s ) => _renderSuggestion ( s ) )
155
+ suggestions . map ( ( item ) => (
156
+ < SuggestionItem
157
+ key = { item . completionId }
158
+ item = { item }
159
+ selection = { selection }
160
+ onClick = { toggleSelection }
161
+ render = { renderSuggestion }
162
+ />
163
+ ) )
213
164
}
214
165
</ div >
215
166
) : null
216
167
}
217
168
</ div >
218
169
) ;
219
170
} ;
171
+
172
+ type SelectionItemProps = {
173
+ item : ICompletion ;
174
+ onClick : ( completion : ICompletion ) => void ;
175
+ render ?: ( completion : ICompletion ) => ReactElement ;
176
+ } ;
177
+
178
+ const SelectionItem : React . FC < SelectionItemProps > = ( { item, onClick, render } ) => {
179
+ const withContainer = ( children : ReactNode ) : ReactElement => (
180
+ < span
181
+ className = 'mx_AutocompleteInput_editor_selection'
182
+ data-testid = { `autocomplete-selection-item-${ item . completionId } ` }
183
+ >
184
+ < span className = 'mx_AutocompleteInput_editor_selection_pill' >
185
+ { children }
186
+ </ span >
187
+ < AccessibleButton
188
+ className = 'mx_AutocompleteInput_editor_selection_remove'
189
+ onClick = { ( ) => onClick ( item ) }
190
+ data-testid = { `autocomplete-selection-remove-button-${ item . completionId } ` }
191
+ >
192
+ < PillRemoveIcon width = { 8 } height = { 8 } />
193
+ </ AccessibleButton >
194
+ </ span >
195
+ ) ;
196
+
197
+ if ( render ) {
198
+ return withContainer ( render ( item ) ) ;
199
+ }
200
+
201
+ return withContainer (
202
+ < span className = 'mx_AutocompleteInput_editor_selection_text' > { item . completion } </ span > ,
203
+ ) ;
204
+ } ;
205
+
206
+ type SuggestionItemProps = {
207
+ item : ICompletion ;
208
+ selection : ICompletion [ ] ;
209
+ onClick : ( completion : ICompletion ) => void ;
210
+ render ?: ( completion : ICompletion ) => ReactElement ;
211
+ } ;
212
+
213
+ const SuggestionItem : React . FC < SuggestionItemProps > = ( { item, selection, onClick, render } ) => {
214
+ const isSelected = selection . some ( selection => selection . completionId === item . completionId ) ;
215
+ const classes = classNames ( {
216
+ 'mx_AutocompleteInput_suggestion' : true ,
217
+ 'mx_AutocompleteInput_suggestion--selected' : isSelected ,
218
+ } ) ;
219
+
220
+ const withContainer = ( children : ReactNode ) : ReactElement => (
221
+ < div
222
+ className = { classes }
223
+ // `onClick` cannot be used here as it would lead to focus loss and closing the suggestion list.
224
+ onMouseDown = { ( event ) => {
225
+ event . preventDefault ( ) ;
226
+ onClick ( item ) ;
227
+ } }
228
+ data-testid = { `autocomplete-suggestion-item-${ item . completionId } ` }
229
+ >
230
+ < div >
231
+ { children }
232
+ </ div >
233
+ { isSelected && < CheckmarkIcon height = { 16 } width = { 16 } /> }
234
+ </ div >
235
+ ) ;
236
+
237
+ if ( render ) {
238
+ return withContainer ( render ( item ) ) ;
239
+ }
240
+
241
+ return withContainer (
242
+ < >
243
+ < span className = 'mx_AutocompleteInput_suggestion_title' > { item . completion } </ span >
244
+ < span className = 'mx_AutocompleteInput_suggestion_description' > { item . completionId } </ span >
245
+ </ > ,
246
+ ) ;
247
+ } ;
0 commit comments