Skip to content

Commit 184ceee

Browse files
authored
fix(cdk-experimental/listbox): ignore spaces during typeahead (#30766)
* fix(cdk-experimental/listbox): ignore spaces during typeahead * fixup! fix(cdk-experimental/listbox): ignore spaces during typeahead * fixup! fix(cdk-experimental/listbox): ignore spaces during typeahead
1 parent dac7bc8 commit 184ceee

File tree

3 files changed

+58
-87
lines changed

3 files changed

+58
-87
lines changed

src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.spec.ts

+41-82
Original file line numberDiff line numberDiff line change
@@ -27,117 +27,65 @@ describe('List Typeahead', () => {
2727
);
2828
}
2929

30+
let items: SignalLike<TestItem[]>;
31+
let typeahead: ListTypeahead<TestItem>;
32+
let navigation: ListNavigation<TestItem>;
33+
34+
beforeEach(() => {
35+
items = getItems(5);
36+
navigation = new ListNavigation({
37+
items,
38+
wrap: signal(false),
39+
activeIndex: signal(0),
40+
skipDisabled: signal(false),
41+
textDirection: signal('ltr'),
42+
orientation: signal('vertical'),
43+
});
44+
typeahead = new ListTypeahead({
45+
navigation,
46+
typeaheadDelay: signal(0.5),
47+
});
48+
});
49+
3050
describe('#search', () => {
3151
it('should navigate to an item', () => {
32-
const items = getItems(5);
33-
const activeIndex = signal(0);
34-
const navigation = new ListNavigation({
35-
items,
36-
activeIndex,
37-
wrap: signal(false),
38-
skipDisabled: signal(false),
39-
textDirection: signal('ltr'),
40-
orientation: signal('vertical'),
41-
});
42-
const typeahead = new ListTypeahead({
43-
navigation,
44-
typeaheadDelay: signal(0.5),
45-
});
46-
4752
typeahead.search('i');
48-
expect(activeIndex()).toBe(1);
53+
expect(navigation.inputs.activeIndex()).toBe(1);
4954

5055
typeahead.search('t');
5156
typeahead.search('e');
5257
typeahead.search('m');
5358
typeahead.search(' ');
5459
typeahead.search('3');
55-
expect(activeIndex()).toBe(3);
60+
expect(navigation.inputs.activeIndex()).toBe(3);
5661
});
5762

5863
it('should reset after a delay', fakeAsync(() => {
59-
const items = getItems(5);
60-
const activeIndex = signal(0);
61-
const navigation = new ListNavigation({
62-
items,
63-
activeIndex,
64-
wrap: signal(false),
65-
skipDisabled: signal(false),
66-
textDirection: signal('ltr'),
67-
orientation: signal('vertical'),
68-
});
69-
const typeahead = new ListTypeahead({
70-
navigation,
71-
typeaheadDelay: signal(0.5),
72-
});
73-
7464
typeahead.search('i');
75-
expect(activeIndex()).toBe(1);
65+
expect(navigation.inputs.activeIndex()).toBe(1);
7666

7767
tick(500);
7868

7969
typeahead.search('i');
80-
expect(activeIndex()).toBe(2);
70+
expect(navigation.inputs.activeIndex()).toBe(2);
8171
}));
8272

8373
it('should skip disabled items', () => {
84-
const items = getItems(5);
85-
const activeIndex = signal(0);
86-
const navigation = new ListNavigation({
87-
items,
88-
activeIndex,
89-
wrap: signal(false),
90-
skipDisabled: signal(true),
91-
textDirection: signal('ltr'),
92-
orientation: signal('vertical'),
93-
});
94-
const typeahead = new ListTypeahead({
95-
navigation,
96-
typeaheadDelay: signal(0.5),
97-
});
9874
items()[1].disabled.set(true);
99-
75+
(navigation.inputs.skipDisabled as WritableSignalLike<boolean>).set(true);
10076
typeahead.search('i');
101-
expect(activeIndex()).toBe(2);
77+
console.log(typeahead.inputs.navigation.inputs.items().map(i => i.disabled()));
78+
expect(navigation.inputs.activeIndex()).toBe(2);
10279
});
10380

10481
it('should not skip disabled items', () => {
105-
const items = getItems(5);
106-
const activeIndex = signal(0);
107-
const navigation = new ListNavigation({
108-
items,
109-
activeIndex,
110-
wrap: signal(false),
111-
skipDisabled: signal(false),
112-
textDirection: signal('ltr'),
113-
orientation: signal('vertical'),
114-
});
115-
const typeahead = new ListTypeahead({
116-
navigation,
117-
typeaheadDelay: signal(0.5),
118-
});
11982
items()[1].disabled.set(true);
120-
83+
(navigation.inputs.skipDisabled as WritableSignalLike<boolean>).set(false);
12184
typeahead.search('i');
122-
expect(activeIndex()).toBe(1);
85+
expect(navigation.inputs.activeIndex()).toBe(1);
12386
});
12487

12588
it('should ignore keys like shift', () => {
126-
const items = getItems(5);
127-
const activeIndex = signal(0);
128-
const navigation = new ListNavigation({
129-
items,
130-
activeIndex,
131-
wrap: signal(false),
132-
skipDisabled: signal(false),
133-
textDirection: signal('ltr'),
134-
orientation: signal('vertical'),
135-
});
136-
const typeahead = new ListTypeahead({
137-
navigation,
138-
typeaheadDelay: signal(0.5),
139-
});
140-
14189
typeahead.search('i');
14290
typeahead.search('t');
14391
typeahead.search('e');
@@ -147,7 +95,18 @@ describe('List Typeahead', () => {
14795
typeahead.search('m');
14896
typeahead.search(' ');
14997
typeahead.search('2');
150-
expect(activeIndex()).toBe(2);
98+
expect(navigation.inputs.activeIndex()).toBe(2);
99+
});
100+
101+
it('should not allow a query to begin with a space', () => {
102+
typeahead.search(' ');
103+
typeahead.search('i');
104+
typeahead.search('t');
105+
typeahead.search('e');
106+
typeahead.search('m');
107+
typeahead.search(' ');
108+
typeahead.search('3');
109+
expect(navigation.inputs.activeIndex()).toBe(3);
151110
});
152111
});
153112
});

src/cdk-experimental/ui-patterns/behaviors/list-typeahead/list-typeahead.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {signal} from '@angular/core';
9+
import {computed, signal} from '@angular/core';
1010
import {SignalLike} from '../signal-like/signal-like';
1111
import {ListNavigationItem, ListNavigation} from '../list-navigation/list-navigation';
1212

@@ -36,6 +36,9 @@ export class ListTypeahead<T extends ListTypeaheadItem> {
3636
/** The navigation controller of the parent list. */
3737
navigation: ListNavigation<T>;
3838

39+
/** Whether the user is actively typing a typeahead search query. */
40+
isTyping = computed(() => this._query().length > 0);
41+
3942
/** Keeps track of the characters that typeahead search is being called with. */
4043
private _query = signal('');
4144

@@ -52,6 +55,10 @@ export class ListTypeahead<T extends ListTypeaheadItem> {
5255
return;
5356
}
5457

58+
if (!this.isTyping() && char === ' ') {
59+
return;
60+
}
61+
5562
if (this._startIndex() === undefined) {
5663
this._startIndex.set(this.navigation.inputs.activeIndex());
5764
}

src/cdk-experimental/ui-patterns/listbox/listbox.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ export class ListboxPattern<V> {
9191
return this.inputs.textDirection() === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
9292
});
9393

94+
/** Represents the space key. Does nothing when the user is actively using typeahead. */
95+
dynamicSpaceKey = computed(() => (this.typeahead.isTyping() ? '' : ' '));
96+
9497
/** The regexp used to decide if a key should trigger typeahead. */
9598
typeaheadRegexp = /^.$/; // TODO: Ignore spaces?
9699

@@ -127,22 +130,24 @@ export class ListboxPattern<V> {
127130

128131
if (this.inputs.multi()) {
129132
manager
130-
.on(Modifier.Shift, ' ', () => this._updateSelection({selectFromAnchor: true}))
131133
.on(Modifier.Shift, 'Enter', () => this._updateSelection({selectFromAnchor: true}))
132134
.on(Modifier.Shift, this.prevKey, () => this.prev({toggle: true}))
133135
.on(Modifier.Shift, this.nextKey, () => this.next({toggle: true}))
134136
.on(Modifier.Ctrl | Modifier.Shift, 'Home', () => this.first({selectFromActive: true}))
135137
.on(Modifier.Ctrl | Modifier.Shift, 'End', () => this.last({selectFromActive: true}))
136-
.on(Modifier.Ctrl, 'A', () => this._updateSelection({selectAll: true}));
138+
.on(Modifier.Ctrl, 'A', () => this._updateSelection({selectAll: true}))
139+
.on(Modifier.Shift, this.dynamicSpaceKey, () =>
140+
this._updateSelection({selectFromAnchor: true}),
141+
);
137142
}
138143

139144
if (!this.followFocus() && this.inputs.multi()) {
140-
manager.on(' ', () => this._updateSelection({toggle: true}));
145+
manager.on(this.dynamicSpaceKey, () => this._updateSelection({toggle: true}));
141146
manager.on('Enter', () => this._updateSelection({toggle: true}));
142147
}
143148

144149
if (!this.followFocus() && !this.inputs.multi()) {
145-
manager.on(' ', () => this._updateSelection({toggleOne: true}));
150+
manager.on(this.dynamicSpaceKey, () => this._updateSelection({toggleOne: true}));
146151
manager.on('Enter', () => this._updateSelection({toggleOne: true}));
147152
}
148153

0 commit comments

Comments
 (0)