Skip to content

Commit faa75cc

Browse files
committed
refactor(cdk-experimental/ui-patterns): switch listbox to use the list behavior
1 parent 803f72d commit faa75cc

File tree

4 files changed

+66
-191
lines changed

4 files changed

+66
-191
lines changed

src/cdk-experimental/ui-patterns/listbox/BUILD.bazel

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ ts_project(
1111
deps = [
1212
"//:node_modules/@angular/core",
1313
"//src/cdk-experimental/ui-patterns/behaviors/event-manager",
14-
"//src/cdk-experimental/ui-patterns/behaviors/list-focus",
15-
"//src/cdk-experimental/ui-patterns/behaviors/list-navigation",
16-
"//src/cdk-experimental/ui-patterns/behaviors/list-selection",
17-
"//src/cdk-experimental/ui-patterns/behaviors/list-typeahead",
14+
"//src/cdk-experimental/ui-patterns/behaviors/list",
1815
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
1916
],
2017
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ describe('Listbox Pattern', () => {
474474
});
475475

476476
it('should not allow wrapping while Shift is held down', () => {
477-
listbox.selection.deselectAll();
477+
listbox.listBehavior.deselectAll();
478478
listbox.onKeydown(shift());
479479
listbox.onKeydown(up({shift: true}));
480480
expect(listbox.inputs.value()).toEqual([]);

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

Lines changed: 56 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -8,63 +8,39 @@
88

99
import {OptionPattern} from './option';
1010
import {KeyboardEventManager, PointerEventManager, Modifier} from '../behaviors/event-manager';
11-
import {ListSelection, ListSelectionInputs} from '../behaviors/list-selection/list-selection';
12-
import {ListTypeahead, ListTypeaheadInputs} from '../behaviors/list-typeahead/list-typeahead';
13-
import {ListNavigation, ListNavigationInputs} from '../behaviors/list-navigation/list-navigation';
14-
import {ListFocus, ListFocusInputs} from '../behaviors/list-focus/list-focus';
1511
import {computed, signal} from '@angular/core';
1612
import {SignalLike} from '../behaviors/signal-like/signal-like';
17-
18-
/** The selection operations that the listbox can perform. */
19-
interface SelectOptions {
20-
toggle?: boolean;
21-
selectOne?: boolean;
22-
selectRange?: boolean;
23-
anchor?: boolean;
24-
}
13+
import {List, ListInputs} from '../behaviors/list/list';
2514

2615
/** Represents the required inputs for a listbox. */
27-
export type ListboxInputs<V> = ListNavigationInputs<OptionPattern<V>> &
28-
ListSelectionInputs<OptionPattern<V>, V> &
29-
ListTypeaheadInputs<OptionPattern<V>> &
30-
ListFocusInputs<OptionPattern<V>> & {
31-
readonly: SignalLike<boolean>;
32-
};
16+
export type ListboxInputs<V> = ListInputs<OptionPattern<V>, V> & {
17+
readonly: SignalLike<boolean>;
18+
};
3319

3420
/** Controls the state of a listbox. */
3521
export class ListboxPattern<V> {
36-
/** Controls navigation for the listbox. */
37-
navigation: ListNavigation<OptionPattern<V>>;
38-
39-
/** Controls selection for the listbox. */
40-
selection: ListSelection<OptionPattern<V>, V>;
41-
42-
/** Controls typeahead for the listbox. */
43-
typeahead: ListTypeahead<OptionPattern<V>>;
44-
45-
/** Controls focus for the listbox. */
46-
focusManager: ListFocus<OptionPattern<V>>;
22+
listBehavior: List<OptionPattern<V>, V>;
4723

4824
/** Whether the list is vertically or horizontally oriented. */
4925
orientation: SignalLike<'vertical' | 'horizontal'>;
5026

5127
/** Whether the listbox is disabled. */
52-
disabled = computed(() => this.focusManager.isListDisabled());
28+
disabled = computed(() => this.listBehavior.disabled());
5329

5430
/** Whether the listbox is readonly. */
5531
readonly: SignalLike<boolean>;
5632

5733
/** The tabindex of the listbox. */
58-
tabindex = computed(() => this.focusManager.getListTabindex());
34+
tabindex = computed(() => this.listBehavior.tabindex());
5935

6036
/** The id of the current active item. */
61-
activedescendant = computed(() => this.focusManager.getActiveDescendant());
37+
activedescendant = computed(() => this.listBehavior.activedescendant());
6238

6339
/** Whether multiple items in the list can be selected at once. */
6440
multi: SignalLike<boolean>;
6541

6642
/** The number of items in the listbox. */
67-
setsize = computed(() => this.navigation.inputs.items().length);
43+
setsize = computed(() => this.inputs.items().length);
6844

6945
/** Whether the listbox selection follows focus. */
7046
followFocus = computed(() => this.inputs.selectionMode() === 'follow');
@@ -89,98 +65,84 @@ export class ListboxPattern<V> {
8965
});
9066

9167
/** Represents the space key. Does nothing when the user is actively using typeahead. */
92-
dynamicSpaceKey = computed(() => (this.typeahead.isTyping() ? '' : ' '));
68+
dynamicSpaceKey = computed(() => (this.listBehavior.isTyping() ? '' : ' '));
9369

9470
/** The regexp used to decide if a key should trigger typeahead. */
9571
typeaheadRegexp = /^.$/; // TODO: Ignore spaces?
9672

97-
/**
98-
* The uncommitted index for selecting a range of options.
99-
*
100-
* NOTE: This is subtly distinct from the "rangeStartIndex" in the ListSelection behavior.
101-
* The anchorIndex does not necessarily represent the start of a range, but represents the most
102-
* recent index where the user showed intent to begin a range selection. Usually, this is wherever
103-
* the user most recently pressed the "Shift" key, but if the user presses shift + space to select
104-
* from the anchor, the user is not intending to start a new range from this index.
105-
*
106-
* In other words, "rangeStartIndex" is only set when a user commits to starting a range selection
107-
* while "anchorIndex" is set whenever a user indicates they may be starting a range selection.
108-
*/
109-
anchorIndex = signal(0);
110-
11173
/** The keydown event manager for the listbox. */
11274
keydown = computed(() => {
11375
const manager = new KeyboardEventManager();
11476

11577
if (this.readonly()) {
11678
return manager
117-
.on(this.prevKey, () => this.prev())
118-
.on(this.nextKey, () => this.next())
119-
.on('Home', () => this.first())
120-
.on('End', () => this.last())
121-
.on(this.typeaheadRegexp, e => this.search(e.key));
79+
.on(this.prevKey, () => this.listBehavior.prev())
80+
.on(this.nextKey, () => this.listBehavior.next())
81+
.on('Home', () => this.listBehavior.first())
82+
.on('End', () => this.listBehavior.last())
83+
.on(this.typeaheadRegexp, e => this.listBehavior.search(e.key));
12284
}
12385

12486
if (!this.followFocus()) {
12587
manager
126-
.on(this.prevKey, () => this.prev())
127-
.on(this.nextKey, () => this.next())
128-
.on('Home', () => this.first())
129-
.on('End', () => this.last())
130-
.on(this.typeaheadRegexp, e => this.search(e.key));
88+
.on(this.prevKey, () => this.listBehavior.prev())
89+
.on(this.nextKey, () => this.listBehavior.next())
90+
.on('Home', () => this.listBehavior.first())
91+
.on('End', () => this.listBehavior.last())
92+
.on(this.typeaheadRegexp, e => this.listBehavior.search(e.key));
13193
}
13294

13395
if (this.followFocus()) {
13496
manager
135-
.on(this.prevKey, () => this.prev({selectOne: true}))
136-
.on(this.nextKey, () => this.next({selectOne: true}))
137-
.on('Home', () => this.first({selectOne: true}))
138-
.on('End', () => this.last({selectOne: true}))
139-
.on(this.typeaheadRegexp, e => this.search(e.key, {selectOne: true}));
97+
.on(this.prevKey, () => this.listBehavior.prev({selectOne: true}))
98+
.on(this.nextKey, () => this.listBehavior.next({selectOne: true}))
99+
.on('Home', () => this.listBehavior.first({selectOne: true}))
100+
.on('End', () => this.listBehavior.last({selectOne: true}))
101+
.on(this.typeaheadRegexp, e => this.listBehavior.search(e.key, {selectOne: true}));
140102
}
141103

142104
if (this.inputs.multi()) {
143105
manager
144-
.on(Modifier.Any, 'Shift', () => this.anchorIndex.set(this.inputs.activeIndex()))
145-
.on(Modifier.Shift, this.prevKey, () => this.prev({selectRange: true}))
146-
.on(Modifier.Shift, this.nextKey, () => this.next({selectRange: true}))
106+
.on(Modifier.Any, 'Shift', () => this.listBehavior.anchor(this.inputs.activeIndex()))
107+
.on(Modifier.Shift, this.prevKey, () => this.listBehavior.prev({selectRange: true}))
108+
.on(Modifier.Shift, this.nextKey, () => this.listBehavior.next({selectRange: true}))
147109
.on([Modifier.Ctrl | Modifier.Shift, Modifier.Meta | Modifier.Shift], 'Home', () =>
148-
this.first({selectRange: true, anchor: false}),
110+
this.listBehavior.first({selectRange: true, anchor: false}),
149111
)
150112
.on([Modifier.Ctrl | Modifier.Shift, Modifier.Meta | Modifier.Shift], 'End', () =>
151-
this.last({selectRange: true, anchor: false}),
113+
this.listBehavior.last({selectRange: true, anchor: false}),
152114
)
153115
.on(Modifier.Shift, 'Enter', () =>
154-
this._updateSelection({selectRange: true, anchor: false}),
116+
this.listBehavior.updateSelection({selectRange: true, anchor: false}),
155117
)
156118
.on(Modifier.Shift, this.dynamicSpaceKey, () =>
157-
this._updateSelection({selectRange: true, anchor: false}),
119+
this.listBehavior.updateSelection({selectRange: true, anchor: false}),
158120
);
159121
}
160122

161123
if (!this.followFocus() && this.inputs.multi()) {
162124
manager
163-
.on(this.dynamicSpaceKey, () => this.selection.toggle())
164-
.on('Enter', () => this.selection.toggle())
165-
.on([Modifier.Ctrl, Modifier.Meta], 'A', () => this.selection.toggleAll());
125+
.on(this.dynamicSpaceKey, () => this.listBehavior.toggle())
126+
.on('Enter', () => this.listBehavior.toggle())
127+
.on([Modifier.Ctrl, Modifier.Meta], 'A', () => this.listBehavior.toggleAll());
166128
}
167129

168130
if (!this.followFocus() && !this.inputs.multi()) {
169-
manager.on(this.dynamicSpaceKey, () => this.selection.toggleOne());
170-
manager.on('Enter', () => this.selection.toggleOne());
131+
manager.on(this.dynamicSpaceKey, () => this.listBehavior.toggleOne());
132+
manager.on('Enter', () => this.listBehavior.toggleOne());
171133
}
172134

173135
if (this.inputs.multi() && this.followFocus()) {
174136
manager
175-
.on([Modifier.Ctrl, Modifier.Meta], this.prevKey, () => this.prev())
176-
.on([Modifier.Ctrl, Modifier.Meta], this.nextKey, () => this.next())
177-
.on([Modifier.Ctrl, Modifier.Meta], ' ', () => this.selection.toggle())
178-
.on([Modifier.Ctrl, Modifier.Meta], 'Enter', () => this.selection.toggle())
179-
.on([Modifier.Ctrl, Modifier.Meta], 'Home', () => this.first())
180-
.on([Modifier.Ctrl, Modifier.Meta], 'End', () => this.last())
137+
.on([Modifier.Ctrl, Modifier.Meta], this.prevKey, () => this.listBehavior.prev())
138+
.on([Modifier.Ctrl, Modifier.Meta], this.nextKey, () => this.listBehavior.next())
139+
.on([Modifier.Ctrl, Modifier.Meta], ' ', () => this.listBehavior.toggle())
140+
.on([Modifier.Ctrl, Modifier.Meta], 'Enter', () => this.listBehavior.toggle())
141+
.on([Modifier.Ctrl, Modifier.Meta], 'Home', () => this.listBehavior.first())
142+
.on([Modifier.Ctrl, Modifier.Meta], 'End', () => this.listBehavior.last())
181143
.on([Modifier.Ctrl, Modifier.Meta], 'A', () => {
182-
this.selection.toggleAll();
183-
this.selection.select(); // Ensure the currect option remains selected.
144+
this.listBehavior.toggleAll();
145+
this.listBehavior.select(); // Ensure the currect option remains selected.
184146
});
185147
}
186148

@@ -192,29 +154,31 @@ export class ListboxPattern<V> {
192154
const manager = new PointerEventManager();
193155

194156
if (this.readonly()) {
195-
return manager.on(e => this.goto(e));
157+
return manager.on(e => this.listBehavior.goto(this._getItem(e)!));
196158
}
197159

198160
if (this.multi()) {
199-
manager.on(Modifier.Shift, e => this.goto(e, {selectRange: true}));
161+
manager.on(Modifier.Shift, e =>
162+
this.listBehavior.goto(this._getItem(e)!, {selectRange: true}),
163+
);
200164
}
201165

202166
if (!this.multi() && this.followFocus()) {
203-
return manager.on(e => this.goto(e, {selectOne: true}));
167+
return manager.on(e => this.listBehavior.goto(this._getItem(e)!, {selectOne: true}));
204168
}
205169

206170
if (!this.multi() && !this.followFocus()) {
207-
return manager.on(e => this.goto(e, {toggle: true}));
171+
return manager.on(e => this.listBehavior.goto(this._getItem(e)!, {toggle: true}));
208172
}
209173

210174
if (this.multi() && this.followFocus()) {
211175
return manager
212-
.on(e => this.goto(e, {selectOne: true}))
213-
.on(Modifier.Ctrl, e => this.goto(e, {toggle: true}));
176+
.on(e => this.listBehavior.goto(this._getItem(e)!, {selectOne: true}))
177+
.on(Modifier.Ctrl, e => this.listBehavior.goto(this._getItem(e)!, {toggle: true}));
214178
}
215179

216180
if (this.multi() && !this.followFocus()) {
217-
return manager.on(e => this.goto(e, {toggle: true}));
181+
return manager.on(e => this.listBehavior.goto(this._getItem(e)!, {toggle: true}));
218182
}
219183

220184
return manager;
@@ -225,14 +189,7 @@ export class ListboxPattern<V> {
225189
this.orientation = inputs.orientation;
226190
this.multi = inputs.multi;
227191

228-
this.focusManager = new ListFocus(inputs);
229-
this.selection = new ListSelection({...inputs, focusManager: this.focusManager});
230-
this.typeahead = new ListTypeahead({...inputs, focusManager: this.focusManager});
231-
this.navigation = new ListNavigation({
232-
...inputs,
233-
focusManager: this.focusManager,
234-
wrap: computed(() => this.wrap() && this.inputs.wrap()),
235-
});
192+
this.listBehavior = new List(inputs);
236193
}
237194

238195
/** Returns a set of violations */
@@ -270,37 +227,6 @@ export class ListboxPattern<V> {
270227
}
271228
}
272229

273-
/** Navigates to the first option in the listbox. */
274-
first(opts?: SelectOptions) {
275-
this._navigate(opts, () => this.navigation.first());
276-
}
277-
278-
/** Navigates to the last option in the listbox. */
279-
last(opts?: SelectOptions) {
280-
this._navigate(opts, () => this.navigation.last());
281-
}
282-
283-
/** Navigates to the next option in the listbox. */
284-
next(opts?: SelectOptions) {
285-
this._navigate(opts, () => this.navigation.next());
286-
}
287-
288-
/** Navigates to the previous option in the listbox. */
289-
prev(opts?: SelectOptions) {
290-
this._navigate(opts, () => this.navigation.prev());
291-
}
292-
293-
/** Navigates to the given item in the listbox. */
294-
goto(event: PointerEvent, opts?: SelectOptions) {
295-
const item = this._getItem(event);
296-
this._navigate(opts, () => this.navigation.goto(item));
297-
}
298-
299-
/** Handles typeahead search navigation for the listbox. */
300-
search(char: string, opts?: SelectOptions) {
301-
this._navigate(opts, () => this.typeahead.search(char));
302-
}
303-
304230
/**
305231
* Sets the listbox to it's default initial state.
306232
*
@@ -315,7 +241,7 @@ export class ListboxPattern<V> {
315241
let firstItem: OptionPattern<V> | null = null;
316242

317243
for (const item of this.inputs.items()) {
318-
if (this.focusManager.isFocusable(item)) {
244+
if (this.listBehavior.isFocusable(item)) {
319245
if (!firstItem) {
320246
firstItem = item;
321247
}
@@ -331,46 +257,6 @@ export class ListboxPattern<V> {
331257
}
332258
}
333259

334-
/**
335-
* Safely performs a navigation operation.
336-
*
337-
* Handles conditionally disabling wrapping for when a navigation
338-
* operation is occurring while the user is selecting a range of options.
339-
*
340-
* Handles boilerplate calling of focus & selection operations. Also ensures these
341-
* additional operations are only called if the navigation operation moved focus to a new option.
342-
*/
343-
private _navigate(opts: SelectOptions = {}, operation: () => boolean) {
344-
if (opts?.selectRange) {
345-
this.wrap.set(false);
346-
this.selection.rangeStartIndex.set(this.anchorIndex());
347-
}
348-
349-
const moved = operation();
350-
351-
if (moved) {
352-
this._updateSelection(opts);
353-
}
354-
355-
this.wrap.set(true);
356-
}
357-
358-
/** Handles updating selection for the listbox. */
359-
private _updateSelection(opts: SelectOptions = {anchor: true}) {
360-
if (opts.toggle) {
361-
this.selection.toggle();
362-
}
363-
if (opts.selectOne) {
364-
this.selection.selectOne();
365-
}
366-
if (opts.selectRange) {
367-
this.selection.selectRange();
368-
}
369-
if (!opts.anchor) {
370-
this.anchorIndex.set(this.selection.rangeStartIndex());
371-
}
372-
}
373-
374260
private _getItem(e: PointerEvent) {
375261
if (!(e.target instanceof HTMLElement)) {
376262
return;

0 commit comments

Comments
 (0)