@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414limitations under the License.
1515*/
1616
17- import React , { useState , ReactNode , ChangeEvent , KeyboardEvent , useRef } from 'react' ;
17+ import React , { useState , ReactNode , ChangeEvent , KeyboardEvent , useRef , ReactElement } from 'react' ;
1818import classNames from 'classnames' ;
1919
2020import Autocompleter from "../../autocomplete/AutocompleteProvider" ;
@@ -23,15 +23,16 @@ import { ICompletion } from '../../autocomplete/Autocompleter';
2323import AccessibleButton from '../../components/views/elements/AccessibleButton' ;
2424import { Icon as PillRemoveIcon } from '../../../res/img/icon-pill-remove.svg' ;
2525import { Icon as CheckmarkIcon } from '../../../res/img/element-icons/roomlist/checkmark.svg' ;
26+ import useFocus from "../../hooks/useFocus" ;
2627
2728interface AutocompleteInputProps {
2829 provider : Autocompleter ;
2930 placeholder : string ;
3031 selection : ICompletion [ ] ;
3132 onSelectionChange : ( selection : ICompletion [ ] ) => void ;
3233 maxSuggestions ?: number ;
33- renderSuggestion ?: ( s : ICompletion ) => ReactNode ;
34- renderSelection ?: ( m : ICompletion ) => ReactNode ;
34+ renderSuggestion ?: ( s : ICompletion ) => ReactElement ;
35+ renderSelection ?: ( m : ICompletion ) => ReactElement ;
3536 additionalFilter ?: ( suggestion : ICompletion ) => boolean ;
3637}
3738
@@ -47,9 +48,9 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
4748} ) => {
4849 const [ query , setQuery ] = useState < string > ( '' ) ;
4950 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 ) ;
5354
5455 const focusEditor = ( ) => {
5556 editorRef ?. current ?. focus ( ) ;
@@ -111,72 +112,6 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
111112 }
112113 } ;
113114
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-
180115 const hasPlaceholder = ( ) : boolean => selection . length === 0 && query . length === 0 ;
181116
182117 return (
@@ -187,18 +122,26 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
187122 onClick = { onClickInputArea }
188123 data-testid = "autocomplete-editor"
189124 >
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+ }
191135 < input
192136 ref = { editorRef }
193137 type = "text"
194138 onKeyDown = { onKeyDown }
195139 onChange = { onQueryChange }
196140 value = { query }
197141 autoComplete = "off"
198- placeholder = { hasPlaceholder ( ) ? placeholder : null }
199- onFocus = { ( ) => setFocused ( true ) }
200- onBlur = { ( ) => setFocused ( false ) }
142+ placeholder = { hasPlaceholder ( ) ? placeholder : undefined }
201143 data-testid = "autocomplete-input"
144+ { ...onFocusChangeHandlerFunctions }
202145 />
203146 </ div >
204147 {
@@ -209,11 +152,96 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
209152 data-testid = "autocomplete-matches"
210153 >
211154 {
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+ ) )
213164 }
214165 </ div >
215166 ) : null
216167 }
217168 </ div >
218169 ) ;
219170} ;
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