@@ -15,11 +15,14 @@ import styles from './Combobox.styles.css'
1515class AsyncOptionsInfo extends Error { }
1616
1717export type ComboboxOptionData = {
18- value : string ;
19- label : string | TemplateResult ;
18+ value : unknown ;
19+ label : string ;
20+ template ?: TemplateResult ;
2021 selectable ?: boolean ;
2122}
2223
24+ export type ComboboxChangeEvent = CustomEvent < { option : ComboboxOptionData } >
25+
2326export type AsyncComboboxOptionsProvider = ( query : string ) => Promise < ComboboxOptionData [ ] >
2427
2528export function defineAsyncComboboxOptionsProvider < T extends AsyncComboboxOptionsProvider > ( provider : T ) : T {
@@ -37,7 +40,7 @@ export default class Combobox extends WebComponent {
3740 @property ( { type : String , reflect : true } )
3841 accessor name = ''
3942
40- @property ( { type : String } )
43+ @property ( )
4144 accessor value = ''
4245
4346 @property ( { type : String , reflect : true } )
@@ -64,15 +67,15 @@ export default class Combobox extends WebComponent {
6467 @property ( { type : Function } )
6568 accessor asyncOptionsProvider : AsyncComboboxOptionsProvider | null = null
6669
70+ @property ( { type : Array } )
71+ accessor optionsFallback : ComboboxOptionData [ ] | null = null
72+
6773 @query ( 'input' )
6874 private accessor inputElement : HTMLInputElement | null = null
6975
7076 @state ( )
7177 private accessor filter = ''
7278
73- @state ( )
74- private accessor debouncedFilter = ''
75-
7679 @state ( )
7780 private accessor displayValue = ''
7881
@@ -95,29 +98,23 @@ export default class Combobox extends WebComponent {
9598 new InputTrait ( this , {
9699 getInputElement : ( ) => this . inputElement ,
97100 getInternals : ( ) => this . getInternals ( ) ,
98- onValueChanged : ( value ) => {
99- this . filter = value . toLowerCase ( )
100- this . displayValue = value
101-
102- this . updateFilter ( value . toLowerCase ( ) )
103-
104- const options = this . getFilteredOptions ( )
105-
106- if ( options . length === 0 ) {
107- this . hide ( )
108- return
109- }
110-
111- if ( this . open ) {
112- this . activeIndex = this . getInitialActiveIndex ( options )
113- }
114- } ,
115101 } )
116102 )
117103
118104 this . listboxId = `listbox-${ this . inputTrait . inputId } `
119105 }
120106
107+ get selectedOption ( ) : ComboboxOptionData | undefined {
108+ if ( ! this . value ) {
109+ return
110+ }
111+
112+ return {
113+ label : this . displayValue ,
114+ value : this . value ,
115+ }
116+ }
117+
121118 disconnectedCallback ( ) {
122119 super . disconnectedCallback ( )
123120
@@ -127,13 +124,21 @@ export default class Combobox extends WebComponent {
127124 protected willUpdate ( changedProperties : Map < string , unknown > ) {
128125 super . willUpdate ( changedProperties )
129126
130- if ( changedProperties . has ( 'value' ) && this . displayValue === '' ) {
131- this . displayValue = this . value
132- }
133-
134127 if ( changedProperties . has ( 'asyncOptionsUrl' ) || changedProperties . has ( 'asyncOptionsProvider' ) ) {
135128 this . updateAsyncOptionsTask ( )
136129 }
130+
131+ if ( changedProperties . has ( 'value' ) ) {
132+ const options = this . getFilteredOptions ( )
133+ const option = options . find ( ( option ) => option . value === this . value ) ?? this . optionsFallback ?. find ( ( option ) => option . value === this . value )
134+ const optionLabel = option ?. selectable !== false && option ?. label
135+
136+ this . updateDisplayValue ( optionLabel || this . value )
137+
138+ if ( this . open ) {
139+ this . activeIndex = this . getInitialActiveIndex ( options )
140+ }
141+ }
137142 }
138143
139144 protected render ( ) {
@@ -176,7 +181,7 @@ export default class Combobox extends WebComponent {
176181 .value=${ this . displayValue }
177182 @keydown=${ this . onInputKeyDown }
178183 @focus=${ this . onInputFocus }
179- @input=${ ( ) => this . selectOnly ? this . updateFilter ( this . inputElement ?. value ?? '' ) : this . inputTrait . onInput ( ) }
184+ @input=${ ( ) => this . selectOnly ? this . updateDisplayValue ( this . inputElement ?. value ?? '' ) : this . inputTrait . onInput ( ) }
180185 />
181186 < icon-lucide-chevron-down > </ icon-lucide-chevron-down >
182187 </ div >
@@ -201,9 +206,9 @@ export default class Combobox extends WebComponent {
201206 data-active=${ index === this . activeIndex || nothing }
202207 @mousemove=${ ( ) => this . setActiveIndex ( index ) }
203208 >
204- ${ option . label }
209+ ${ option . template ?? option . label }
205210 </ div > `
206- : option . label
211+ : option . template ?? option . label
207212 } ) }
208213 </ div >
209214 </ wa-popup >
@@ -212,12 +217,14 @@ export default class Combobox extends WebComponent {
212217
213218 private getFilteredOptions ( ) : ComboboxOptionData [ ] {
214219 if ( this . asyncOptionsTask ) {
215- const options = this . asyncOptionsTask . render ( {
220+ return this . asyncOptionsTask . render ( {
221+ initial : ( ) => this . optionsFallback ?? [ ] ,
216222 complete : ( options ) => options ,
217223 pending : ( ) => [
218224 {
219225 value : '' ,
220- label : html `< div class ="loading-async-options "> < icon-svg-spinners-3-dots-fade > </ icon-svg-spinners-3-dots-fade > </ div > ` ,
226+ label : 'Loading...' ,
227+ template : html `< div class ="loading-async-options "> < icon-svg-spinners-3-dots-fade > </ icon-svg-spinners-3-dots-fade > </ div > ` ,
221228 selectable : false
222229 } as const ,
223230 ] ,
@@ -228,14 +235,13 @@ export default class Combobox extends WebComponent {
228235 return [
229236 {
230237 value : '' ,
231- label : html `< div class ="async-options-message async-options-message-- ${ isError ? 'error' : 'info' } "> ${ message } </ div > ` ,
238+ label : message ,
239+ template : html `< div class ="async-options-message async-options-message-- ${ isError ? 'error' : 'info' } "> ${ message } </ div > ` ,
232240 selectable : false
233241 } as const ,
234242 ]
235243 } ,
236244 } )
237-
238- return options ?? [ ]
239245 }
240246
241247 return this . getOptionsFromDOM ( ) . filter ( ( option ) =>
@@ -284,13 +290,17 @@ export default class Combobox extends WebComponent {
284290 this . activeIndex = index
285291 }
286292
287- private updateFilter ( value : string , options : { debounce ?: boolean } = { } ) {
288- this . filter = value . toLowerCase ( )
293+ private updateDisplayValue ( value : unknown ) {
294+ this . displayValue = String ( value )
295+
296+ if ( this . open ) {
297+ const filter = this . displayValue . toLowerCase ( )
289298
290- if ( options . debounce ?? true ) {
291- this . updateDebouncedFilter ( this . filter )
292- } else {
293- this . debouncedFilter = this . filter
299+ if ( this . asyncOptionsTask ) {
300+ this . updateDebouncedFilter ( filter )
301+ } else {
302+ this . filter = filter
303+ }
294304 }
295305 }
296306
@@ -333,10 +343,10 @@ export default class Combobox extends WebComponent {
333343
334344 return results . map ( ( result ) => ( {
335345 label : String ( Object ( result ) [ labelField ] ) ,
336- value : String ( Object ( result ) [ valueField ] ) ,
346+ value : Object ( result ) [ valueField ] ,
337347 } ) )
338348 } ,
339- ( ) => [ this . debouncedFilter ]
349+ ( ) => [ this . filter ]
340350 )
341351 }
342352
@@ -365,20 +375,24 @@ export default class Combobox extends WebComponent {
365375 return
366376 }
367377
378+ this . filter = ''
368379 this . open = false
369380 this . activeIndex = - 1
370381 this . removeOpenListeners ( )
371- this . updateFilter ( '' , { debounce : false } )
382+ this . updateDebouncedFilter . cancel ( )
372383 }
373384
374385 private selectOption ( option : ComboboxOptionData ) {
375- this . inputTrait . setValue ( option . value )
386+ const previousValue = this . value
376387
377- this . displayValue =
378- typeof option . label === 'string' ? option . label : option . value
379388 this . hide ( )
389+ this . inputTrait . setValue ( option . value )
380390 this . inputElement ?. focus ( { preventScroll : true } )
381- this . dispatchEvent ( new Event ( 'change' , { bubbles : true , composed : true } ) )
391+ this . dispatchEvent ( new CustomEvent ( 'change' , { bubbles : true , composed : true , detail : { option } } ) )
392+
393+ if ( previousValue === this . value ) {
394+ this . updateDisplayValue ( option . label )
395+ }
382396 }
383397
384398 private scrollActiveOptionIntoView ( ) {
@@ -442,6 +456,10 @@ export default class Combobox extends WebComponent {
442456 }
443457
444458 private onAnchorMouseDown ( event : MouseEvent ) {
459+ if ( event . target === this . inputElement ) {
460+ return
461+ }
462+
445463 event . preventDefault ( )
446464 this . inputElement ?. focus ( { preventScroll : true } )
447465 }
@@ -500,6 +518,10 @@ export default class Combobox extends WebComponent {
500518 case 'Tab' :
501519 this . hide ( )
502520 break
521+ default :
522+ if ( ! this . open ) {
523+ this . show ( )
524+ }
503525 }
504526 }
505527
0 commit comments