diff --git a/src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.spec.ts b/src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.spec.ts index 1be9ab8d0745..4f82a8f15027 100644 --- a/src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.spec.ts +++ b/src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.spec.ts @@ -27,117 +27,65 @@ describe('List Typeahead', () => { ); } + let items: SignalLike; + let typeahead: ListTypeahead; + let navigation: ListNavigation; + + beforeEach(() => { + items = getItems(5); + navigation = new ListNavigation({ + items, + wrap: signal(false), + activeIndex: signal(0), + skipDisabled: signal(false), + textDirection: signal('ltr'), + orientation: signal('vertical'), + }); + typeahead = new ListTypeahead({ + navigation, + typeaheadDelay: signal(0.5), + }); + }); + describe('#search', () => { it('should navigate to an item', () => { - const items = getItems(5); - const activeIndex = signal(0); - const navigation = new ListNavigation({ - items, - activeIndex, - wrap: signal(false), - skipDisabled: signal(false), - textDirection: signal('ltr'), - orientation: signal('vertical'), - }); - const typeahead = new ListTypeahead({ - navigation, - typeaheadDelay: signal(0.5), - }); - typeahead.search('i'); - expect(activeIndex()).toBe(1); + expect(navigation.inputs.activeIndex()).toBe(1); typeahead.search('t'); typeahead.search('e'); typeahead.search('m'); typeahead.search(' '); typeahead.search('3'); - expect(activeIndex()).toBe(3); + expect(navigation.inputs.activeIndex()).toBe(3); }); it('should reset after a delay', fakeAsync(() => { - const items = getItems(5); - const activeIndex = signal(0); - const navigation = new ListNavigation({ - items, - activeIndex, - wrap: signal(false), - skipDisabled: signal(false), - textDirection: signal('ltr'), - orientation: signal('vertical'), - }); - const typeahead = new ListTypeahead({ - navigation, - typeaheadDelay: signal(0.5), - }); - typeahead.search('i'); - expect(activeIndex()).toBe(1); + expect(navigation.inputs.activeIndex()).toBe(1); tick(500); typeahead.search('i'); - expect(activeIndex()).toBe(2); + expect(navigation.inputs.activeIndex()).toBe(2); })); it('should skip disabled items', () => { - const items = getItems(5); - const activeIndex = signal(0); - const navigation = new ListNavigation({ - items, - activeIndex, - wrap: signal(false), - skipDisabled: signal(true), - textDirection: signal('ltr'), - orientation: signal('vertical'), - }); - const typeahead = new ListTypeahead({ - navigation, - typeaheadDelay: signal(0.5), - }); items()[1].disabled.set(true); - + (navigation.inputs.skipDisabled as WritableSignalLike).set(true); typeahead.search('i'); - expect(activeIndex()).toBe(2); + console.log(typeahead.inputs.navigation.inputs.items().map(i => i.disabled())); + expect(navigation.inputs.activeIndex()).toBe(2); }); it('should not skip disabled items', () => { - const items = getItems(5); - const activeIndex = signal(0); - const navigation = new ListNavigation({ - items, - activeIndex, - wrap: signal(false), - skipDisabled: signal(false), - textDirection: signal('ltr'), - orientation: signal('vertical'), - }); - const typeahead = new ListTypeahead({ - navigation, - typeaheadDelay: signal(0.5), - }); items()[1].disabled.set(true); - + (navigation.inputs.skipDisabled as WritableSignalLike).set(false); typeahead.search('i'); - expect(activeIndex()).toBe(1); + expect(navigation.inputs.activeIndex()).toBe(1); }); it('should ignore keys like shift', () => { - const items = getItems(5); - const activeIndex = signal(0); - const navigation = new ListNavigation({ - items, - activeIndex, - wrap: signal(false), - skipDisabled: signal(false), - textDirection: signal('ltr'), - orientation: signal('vertical'), - }); - const typeahead = new ListTypeahead({ - navigation, - typeaheadDelay: signal(0.5), - }); - typeahead.search('i'); typeahead.search('t'); typeahead.search('e'); @@ -147,7 +95,18 @@ describe('List Typeahead', () => { typeahead.search('m'); typeahead.search(' '); typeahead.search('2'); - expect(activeIndex()).toBe(2); + expect(navigation.inputs.activeIndex()).toBe(2); + }); + + it('should not allow a query to begin with a space', () => { + typeahead.search(' '); + typeahead.search('i'); + typeahead.search('t'); + typeahead.search('e'); + typeahead.search('m'); + typeahead.search(' '); + typeahead.search('3'); + expect(navigation.inputs.activeIndex()).toBe(3); }); }); }); diff --git a/src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.ts b/src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.ts index aaa2350d6d63..36d3a65898b3 100644 --- a/src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.ts +++ b/src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {signal} from '@angular/core'; +import {computed, signal} from '@angular/core'; import {SignalLike} from '../signal-like/signal-like'; import {ListNavigationItem, ListNavigation} from '../list-navigation/list-navigation'; @@ -36,6 +36,9 @@ export class ListTypeahead { /** The navigation controller of the parent list. */ navigation: ListNavigation; + /** Whether the user is actively typing a typeahead search query. */ + isTyping = computed(() => this._query().length > 0); + /** Keeps track of the characters that typeahead search is being called with. */ private _query = signal(''); @@ -52,6 +55,10 @@ export class ListTypeahead { return; } + if (!this.isTyping() && char === ' ') { + return; + } + if (this._startIndex() === undefined) { this._startIndex.set(this.navigation.inputs.activeIndex()); } diff --git a/src/cdk-experimental/ui-patterns/listbox/listbox.ts b/src/cdk-experimental/ui-patterns/listbox/listbox.ts index bba0f2444ba6..1e9425248980 100644 --- a/src/cdk-experimental/ui-patterns/listbox/listbox.ts +++ b/src/cdk-experimental/ui-patterns/listbox/listbox.ts @@ -91,6 +91,9 @@ export class ListboxPattern { return this.inputs.textDirection() === 'rtl' ? 'ArrowLeft' : 'ArrowRight'; }); + /** Represents the space key. Does nothing when the user is actively using typeahead. */ + dynamicSpaceKey = computed(() => (this.typeahead.isTyping() ? '' : ' ')); + /** The regexp used to decide if a key should trigger typeahead. */ typeaheadRegexp = /^.$/; // TODO: Ignore spaces? @@ -127,22 +130,24 @@ export class ListboxPattern { if (this.inputs.multi()) { manager - .on(Modifier.Shift, ' ', () => this._updateSelection({selectFromAnchor: true})) .on(Modifier.Shift, 'Enter', () => this._updateSelection({selectFromAnchor: true})) .on(Modifier.Shift, this.prevKey, () => this.prev({toggle: true})) .on(Modifier.Shift, this.nextKey, () => this.next({toggle: true})) .on(Modifier.Ctrl | Modifier.Shift, 'Home', () => this.first({selectFromActive: true})) .on(Modifier.Ctrl | Modifier.Shift, 'End', () => this.last({selectFromActive: true})) - .on(Modifier.Ctrl, 'A', () => this._updateSelection({selectAll: true})); + .on(Modifier.Ctrl, 'A', () => this._updateSelection({selectAll: true})) + .on(Modifier.Shift, this.dynamicSpaceKey, () => + this._updateSelection({selectFromAnchor: true}), + ); } if (!this.followFocus() && this.inputs.multi()) { - manager.on(' ', () => this._updateSelection({toggle: true})); + manager.on(this.dynamicSpaceKey, () => this._updateSelection({toggle: true})); manager.on('Enter', () => this._updateSelection({toggle: true})); } if (!this.followFocus() && !this.inputs.multi()) { - manager.on(' ', () => this._updateSelection({toggleOne: true})); + manager.on(this.dynamicSpaceKey, () => this._updateSelection({toggleOne: true})); manager.on('Enter', () => this._updateSelection({toggleOne: true})); }