Skip to content

Commit a6f37ea

Browse files
committed
fix(aria/combobox): typeahead in readonly mode
1 parent 76cb94c commit a6f37ea

File tree

6 files changed

+77
-7
lines changed

6 files changed

+77
-7
lines changed

src/aria/private/behaviors/list-typeahead/list-typeahead.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,13 @@ export class ListTypeahead<T extends ListTypeaheadItem> {
8585
*/
8686
private _getItem() {
8787
let items = this.focusManager.inputs.items();
88-
const after = items.slice(this._startIndex()! + 1);
89-
const before = items.slice(0, this._startIndex()!);
90-
items = after.concat(before);
91-
items.push(this.inputs.items()[this._startIndex()!]);
88+
89+
if (this._startIndex() !== -1) {
90+
const after = items.slice(this._startIndex()! + 1);
91+
const before = items.slice(0, this._startIndex()!);
92+
items = after.concat(before);
93+
items.push(this.inputs.items()[this._startIndex()!]);
94+
}
9295

9396
const focusableItems = [];
9497
for (const item of items) {

src/aria/private/combobox/combobox.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,36 @@ describe('Combobox with Listbox Pattern', () => {
613613
expect(listbox.getSelectedItem()).toBe(listbox.inputs.items()[2]);
614614
expect(listbox.inputs.value()).toEqual(['Banana']);
615615
});
616+
617+
it('should navigate on typeahead', () => {
618+
const {combobox, listbox} = getPatterns({readonly: true});
619+
expect(listbox.inputs.activeItem()).toBe(undefined);
620+
combobox.onKeydown(createKeyboardEvent('keydown', 67, 'C'));
621+
expect(listbox.inputs.activeItem()).toBe(listbox.inputs.items()[5]);
622+
});
623+
624+
it('should select on typeahead and filter mode is auto-select', () => {
625+
const {combobox, listbox, inputEl} = getPatterns({readonly: true, filterMode: 'auto-select'});
626+
combobox.onKeydown(createKeyboardEvent('keydown', 67, 'C'));
627+
expect(listbox.getSelectedItem()).toBe(listbox.inputs.items()[5]);
628+
expect(listbox.inputs.value()).toEqual(['Cantaloupe']);
629+
expect(inputEl.value).toBe('');
630+
});
631+
632+
it('should NOT select on typeahead when filter mode is manual', () => {
633+
const {combobox, listbox, inputEl} = getPatterns({readonly: true, filterMode: 'manual'});
634+
combobox.onKeydown(createKeyboardEvent('keydown', 67, 'C'));
635+
expect(listbox.getSelectedItem()).toBe(undefined);
636+
expect(listbox.inputs.value()).toEqual([]);
637+
expect(inputEl.value).toBe('');
638+
});
639+
640+
it('should expand on typeahead', () => {
641+
const {combobox} = getPatterns({readonly: true});
642+
expect(combobox.expanded()).toBe(false);
643+
combobox.onKeydown(createKeyboardEvent('keydown', 67, 'C'));
644+
expect(combobox.expanded()).toBe(true);
645+
});
616646
});
617647
});
618648

src/aria/private/combobox/combobox.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ export interface ComboboxListboxControls<T extends ListItem<V>, V> {
8787

8888
/** Sets the value of the combobox based on the selected item. */
8989
setValue: (value: V | undefined) => void; // For re-setting the value if the popup was destroyed.
90+
91+
/** Handles typeahead search for the popup. */
92+
search: (char: string, opts?: {selectOne?: boolean}) => void;
93+
94+
/** Whether the user is currently typing for typeahead purposes. */
95+
isTyping: SignalLike<boolean>;
9096
}
9197

9298
export interface ComboboxTreeControls<T extends ListItem<V>, V>
@@ -147,6 +153,12 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
147153
/** Whether the combobox is read-only. */
148154
readonly = computed(() => this.inputs.readonly() || null);
149155

156+
/** Represents the space key. Does nothing when the user is actively using typeahead. */
157+
dynamicSpaceKey = computed(() => (this.inputs.popupControls()?.isTyping() ? '' : ' '));
158+
159+
/** The regexp used to decide if a key should trigger typeahead. */
160+
typeaheadRegexp = /^.$/;
161+
150162
/** The keydown event manager for the combobox. */
151163
keydown = computed(() => {
152164
if (!this.expanded()) {
@@ -157,8 +169,9 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
157169

158170
if (this.readonly()) {
159171
manager
172+
.on(' ', () => this.open({selected: true}))
160173
.on('Enter', () => this.open({selected: true}))
161-
.on(' ', () => this.open({selected: true}));
174+
.on(this.typeaheadRegexp, e => this.search(e.key));
162175
}
163176

164177
return manager;
@@ -179,7 +192,9 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
179192
.on('Enter', () => this.select({commit: true, close: true}));
180193

181194
if (this.readonly()) {
182-
manager.on(' ', () => this.select({commit: true, close: true}));
195+
manager
196+
.on(this.typeaheadRegexp, e => this.search(e.key))
197+
.on(this.dynamicSpaceKey, () => this.select({commit: true, close: true}));
183198
}
184199

185200
if (popupControls.role() === 'tree') {
@@ -351,6 +366,16 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
351366
}
352367
}
353368

369+
/** Handles typeahead search for the combobox. */
370+
search(char: string) {
371+
if (!this.expanded()) {
372+
this.open();
373+
}
374+
375+
const selectOne = this.inputs.filterMode() !== 'manual';
376+
this.inputs.popupControls()?.search(char, {selectOne});
377+
}
378+
354379
/** Highlights the currently selected item in the combobox. */
355380
highlight() {
356381
const inputEl = this.inputs.inputEl();

src/aria/private/listbox/combobox-listbox.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,10 @@ export class ComboboxListboxPattern<V>
8787

8888
/** Sets the value of the combobox listbox. */
8989
setValue = (value: V | undefined) => this.inputs.value.set(value ? [value] : []);
90+
91+
/** Whether the user is currently typing for typeahead. */
92+
isTyping = () => this.listBehavior.isTyping();
93+
94+
/** Handles typeahead search for the listbox. */
95+
search = (char: string, opts?: {selectOne?: boolean}) => this.listBehavior.search(char, opts);
9096
}

src/aria/private/tree/combobox-tree.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,10 @@ export class ComboboxTreePattern<V>
104104

105105
/** Collapses all of the tree items. */
106106
collapseAll = () => this.items().forEach(item => item.expansion.close());
107+
108+
/** Whether the user is currently typing for typeahead. */
109+
isTyping = () => this.listBehavior.isTyping();
110+
111+
/** Handles typeahead search for the tree. */
112+
search = (char: string, opts?: {selectOne?: boolean}) => this.listBehavior.search(char, opts);
107113
}

src/components-examples/aria/combobox/combobox-readonly/combobox-readonly-example.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div ngCombobox #combobox="ngCombobox" class="example-combobox-container" [readonly]="true">
1+
<div ngCombobox #combobox="ngCombobox" class="example-combobox-container" [readonly]="true" filterMode="auto-select">
22
<div class="example-combobox-input-container">
33
<input
44
ngComboboxInput

0 commit comments

Comments
 (0)