Skip to content

Commit d124104

Browse files
feat(combo-box): autocomplete with typeahead (#21008)
* feat(combo-box): autocomplete with typeahead * chore: cleanup * fix: should-filter-item attribute * chore: cleanup and docs --------- Co-authored-by: Heloise Lui <71858203+heloiselui@users.noreply.github.com>
1 parent 0b6d65c commit d124104

File tree

2 files changed

+93
-7
lines changed

2 files changed

+93
-7
lines changed

packages/web-components/src/components/combo-box/combo-box.stories.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,22 @@ export const WithAILabel = {
318318
},
319319
};
320320

321+
export const AutocompleteWithTypeahead = {
322+
render: () => html`
323+
<cds-combo-box
324+
title-text="ComboBox title"
325+
helper-text="Combobox helper text"
326+
typeahead>
327+
<cds-combo-box-item value="apple">Apple</cds-combo-box-item>
328+
<cds-combo-box-item value="apricot">Apricot</cds-combo-box-item>
329+
<cds-combo-box-item value="avocado">Avocado</cds-combo-box-item>
330+
<cds-combo-box-item value="banana">Banana</cds-combo-box-item>
331+
<cds-combo-box-item value="blackberry">Blackberry</cds-combo-box-item>
332+
<cds-combo-box-item value="blueberry">Blueberry</cds-combo-box-item>
333+
<cds-combo-box-item value="cantaloupe">Cantaloupe</cds-combo-box-item>
334+
</cds-combo-box>
335+
`,
336+
};
321337
export const WithLayer = {
322338
argTypes: controls,
323339
args: {

packages/web-components/src/components/combo-box/combo-box.ts

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,18 @@ class CDSComboBox extends CDSDropdown {
9595
);
9696
}
9797

98+
connectedCallback() {
99+
super.connectedCallback();
100+
if (this.typeahead) {
101+
this.shouldFilterItem = true;
102+
this.setAttribute('should-filter-item', '');
103+
}
104+
}
105+
98106
/**
99107
* Handles `input` event on the `<input>` for filtering.
100108
*/
101-
protected _handleInput() {
109+
protected _handleInput(event: InputEvent) {
102110
const rawQueryText = this._filterInputNode.value;
103111
const queryText = rawQueryText.trim().toLowerCase();
104112

@@ -118,13 +126,49 @@ class CDSComboBox extends CDSDropdown {
118126
if (highlightedItem) {
119127
this._scrollItemIntoView(highlightedItem as HTMLElement);
120128
}
121-
}
122129

130+
if (this.typeahead && event?.inputType?.startsWith('insert')) {
131+
const suggestedItem = highlightedItem.textContent?.trim() ?? '';
132+
if (
133+
suggestedItem.toLowerCase().startsWith(rawQueryText.toLowerCase()) &&
134+
suggestedItem.length > rawQueryText.length
135+
) {
136+
const suggestionText =
137+
rawQueryText + suggestedItem.slice(rawQueryText.length);
138+
139+
this._filterInputNode.value = suggestionText;
140+
this._filterInputNode.setSelectionRange(
141+
rawQueryText.length,
142+
suggestionText.length
143+
);
144+
145+
this._filterInputValue = suggestionText;
146+
this.open = true;
147+
this.requestUpdate();
148+
return;
149+
}
150+
}
151+
}
123152
this._filterInputValue = rawQueryText;
124153
this.open = true;
125154
this.requestUpdate();
126155
}
127156

157+
// removes the autocomplete suggestion
158+
protected _removeAutoCompleteSuggestion() {
159+
if (!this._filterInputNode) return;
160+
const { selectionStart, selectionEnd, value } = this._filterInputNode;
161+
if (selectionStart && selectionEnd && selectionEnd > selectionStart) {
162+
const cleanInput = value.slice(0, selectionStart);
163+
this._filterInputNode.value = cleanInput;
164+
this._filterInputNode.setSelectionRange(
165+
cleanInput.length,
166+
cleanInput.length
167+
);
168+
return;
169+
}
170+
}
171+
128172
// Applies filtering/highlighting to all slotted items.
129173
protected _filterItems(
130174
items: NodeListOf<Element>,
@@ -141,9 +185,9 @@ class CDSComboBox extends CDSDropdown {
141185
comboItem.highlighted = false;
142186
return;
143187
}
144-
const matches = (comboItem.textContent || '')
145-
.toLowerCase()
146-
.includes(queryText);
188+
const matches = this.typeahead
189+
? (comboItem.textContent || '').toLowerCase().startsWith(queryText)
190+
: (comboItem.textContent || '').toLowerCase().includes(queryText);
147191
const filterFunction =
148192
typeof this.shouldFilterItem === 'function'
149193
? this.shouldFilterItem
@@ -198,6 +242,13 @@ class CDSComboBox extends CDSDropdown {
198242

199243
// Clear the query and selection when Escape is pressed.
200244
protected _handleInputKeydown(event: KeyboardEvent) {
245+
// remove the autocomplete suggestion when navigating away from the suggested item
246+
if (
247+
this.typeahead &&
248+
(event.key === 'ArrowDown' || event.key === 'ArrowUp')
249+
) {
250+
this._removeAutoCompleteSuggestion();
251+
}
201252
if (event.key !== 'Escape') {
202253
return;
203254
}
@@ -282,6 +333,13 @@ class CDSComboBox extends CDSDropdown {
282333
itemToSelect.setAttribute('aria-selected', 'true');
283334
}
284335
this._handleUserInitiatedToggle(false);
336+
337+
if (this.typeahead && this._filterInputNode) {
338+
this._filterInputValue = itemToSelect?.textContent?.trim() ?? '';
339+
340+
const length = this._filterInputValue.length;
341+
this._filterInputNode.setSelectionRange(length, length);
342+
}
285343
}
286344

287345
protected _renderLabel(): TemplateResult {
@@ -375,7 +433,8 @@ class CDSComboBox extends CDSDropdown {
375433
itemMatches!: (item: CDSComboBoxItem, queryText: string) => boolean;
376434

377435
/**
378-
* Provide custom filtering behavior.
436+
* Provide custom filtering behavior. This attribute will be ignored if
437+
* `typeahead` is enabled and will default to `true`
379438
*/
380439
@property({
381440
attribute: 'should-filter-item',
@@ -385,6 +444,12 @@ class CDSComboBox extends CDSDropdown {
385444
})
386445
shouldFilterItem: boolean | ShouldFilterItem = false;
387446

447+
/**
448+
* **Experimental**: will enable autocomplete and typeahead for the input field
449+
*/
450+
@property({ type: Boolean })
451+
typeahead = false;
452+
388453
shouldUpdate(changedProperties) {
389454
super.shouldUpdate(changedProperties);
390455
const { _selectedItemContent: selectedItemContent } = this;
@@ -398,9 +463,14 @@ class CDSComboBox extends CDSDropdown {
398463
super.updated(changedProperties);
399464
if (changedProperties.has('open')) {
400465
if (this.open && this._filterInputNode) {
401-
this._handleInput();
466+
this._handleInput(changedProperties);
402467
} else if (!this.open) {
468+
// remove the autocomplete suggestion when closing the combobox
469+
this._removeAutoCompleteSuggestion();
403470
this._resetFilteredItems();
471+
if (this._filterInputNode.value == '') {
472+
this.value = '';
473+
}
404474
}
405475
}
406476
const { _listBoxNode: listBoxNode } = this;

0 commit comments

Comments
 (0)