Skip to content

Commit 32d8271

Browse files
committed
fixup! fix(cdk-experimental/listbox): ignore spaces during typeahead
1 parent 46f44fa commit 32d8271

File tree

3 files changed

+59
-92
lines changed

3 files changed

+59
-92
lines changed

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

Lines changed: 41 additions & 82 deletions
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

Lines changed: 11 additions & 5 deletions
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,8 +36,10 @@ export class ListTypeahead<T extends ListTypeaheadItem> {
3636
/** The navigation controller of the parent list. */
3737
navigation: ListNavigation<T>;
3838

39+
isTyping = computed(() => this._query().length > 0);
40+
3941
/** Keeps track of the characters that typeahead search is being called with. */
40-
query = signal('');
42+
private _query = signal('');
4143

4244
/** The index where that the typeahead search was initiated from. */
4345
private _startIndex = signal<number | undefined>(undefined);
@@ -52,20 +54,24 @@ export class ListTypeahead<T extends ListTypeaheadItem> {
5254
return;
5355
}
5456

57+
if (!this.isTyping() && char === ' ') {
58+
return;
59+
}
60+
5561
if (this._startIndex() === undefined) {
5662
this._startIndex.set(this.navigation.inputs.activeIndex());
5763
}
5864

5965
clearTimeout(this.timeout);
60-
this.query.update(q => q + char.toLowerCase());
66+
this._query.update(q => q + char.toLowerCase());
6167
const item = this._getItem();
6268

6369
if (item) {
6470
this.navigation.goto(item);
6571
}
6672

6773
this.timeout = setTimeout(() => {
68-
this.query.set('');
74+
this._query.set('');
6975
this._startIndex.set(undefined);
7076
}, this.inputs.typeaheadDelay() * 1000);
7177
}
@@ -88,6 +94,6 @@ export class ListTypeahead<T extends ListTypeaheadItem> {
8894
}
8995
}
9096

91-
return focusableItems.find(i => i.searchTerm().toLowerCase().startsWith(this.query()));
97+
return focusableItems.find(i => i.searchTerm().toLowerCase().startsWith(this._query()));
9298
}
9399
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export class ListboxPattern<V> {
9292
});
9393

9494
/** Represents the space key. Does nothing when the user is actively using typeahead. */
95-
spaceKey = computed(() => (this.typeahead.query().length ? '' : ' '));
95+
dynamicSpaceKey = computed(() => (this.typeahead.isTyping() ? '' : ' '));
9696

9797
/** The regexp used to decide if a key should trigger typeahead. */
9898
typeaheadRegexp = /^.$/; // TODO: Ignore spaces?
@@ -130,22 +130,24 @@ export class ListboxPattern<V> {
130130

131131
if (this.inputs.multi()) {
132132
manager
133-
.on(Modifier.Shift, this.spaceKey, () => this._updateSelection({selectFromAnchor: true}))
134133
.on(Modifier.Shift, 'Enter', () => this._updateSelection({selectFromAnchor: true}))
135134
.on(Modifier.Shift, this.prevKey, () => this.prev({toggle: true}))
136135
.on(Modifier.Shift, this.nextKey, () => this.next({toggle: true}))
137136
.on(Modifier.Ctrl | Modifier.Shift, 'Home', () => this.first({selectFromActive: true}))
138137
.on(Modifier.Ctrl | Modifier.Shift, 'End', () => this.last({selectFromActive: true}))
139-
.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+
);
140142
}
141143

142144
if (!this.followFocus() && this.inputs.multi()) {
143-
manager.on(this.spaceKey, () => this._updateSelection({toggle: true}));
145+
manager.on(this.dynamicSpaceKey, () => this._updateSelection({toggle: true}));
144146
manager.on('Enter', () => this._updateSelection({toggle: true}));
145147
}
146148

147149
if (!this.followFocus() && !this.inputs.multi()) {
148-
manager.on(this.spaceKey, () => this._updateSelection({toggleOne: true}));
150+
manager.on(this.dynamicSpaceKey, () => this._updateSelection({toggleOne: true}));
149151
manager.on('Enter', () => this._updateSelection({toggleOne: true}));
150152
}
151153

0 commit comments

Comments
 (0)