Skip to content

Commit 457b33b

Browse files
committed
fixup! feat(cdk-experimental/ui-patterns): listbox ui pattern
1 parent f401a56 commit 457b33b

File tree

11 files changed

+76
-42
lines changed

11 files changed

+76
-42
lines changed

src/cdk-experimental/listbox/listbox.ts

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,40 @@ import {Directionality} from '@angular/cdk/bidi';
2323
import {startWith, takeUntil} from 'rxjs/operators';
2424
import {Subject} from 'rxjs';
2525

26+
/**
27+
* A listbox container.
28+
*
29+
* Listboxes are used to display a list of items for a user to select from. The CdkListbox is meant
30+
* to be used in conjunction with CdkOption as follows:
31+
*
32+
* ```html
33+
* <ul cdkListbox>
34+
* <li cdkOption>Item 1</li>
35+
* <li cdkOption>Item 2</li>
36+
* <li cdkOption>Item 3</li>
37+
* </ul>
38+
* ```
39+
*/
2640
@Directive({
2741
selector: '[cdkListbox]',
2842
exportAs: 'cdkListbox',
2943
host: {
3044
'role': 'listbox',
3145
'class': 'cdk-listbox',
3246
'[attr.tabindex]': 'state.tabindex()',
33-
// '[attr.aria-disabled]': 'state.disabled()',
47+
'[attr.aria-disabled]': 'state.disabled()',
3448
'[attr.aria-multiselectable]': 'state.multiselectable()',
3549
'[attr.aria-activedescendant]': 'state.activedescendant()',
3650
'[attr.aria-orientation]': 'state.orientation()',
3751
'(focusin)': 'state.onFocus()',
3852
'(keydown)': 'state.onKeydown($event)',
3953
'(mousedown)': 'state.onMousedown($event)',
40-
// '(focusout)': '_handleFocusOut($event)',
41-
// '(focusin)': '_handleFocusIn()',
4254
},
4355
})
4456
export class CdkListbox implements ListboxInputs, OnDestroy {
57+
/** The directionality (LTR / RTL) context for the application (or a subtree of it). */
58+
private _dir = inject(Directionality);
59+
4560
/** Whether the list is vertically or horizontally oriented. */
4661
orientation = input<'vertical' | 'horizontal'>('vertical');
4762

@@ -55,13 +70,13 @@ export class CdkListbox implements ListboxInputs, OnDestroy {
5570
skipDisabled = input<boolean>(true);
5671

5772
/** The focus strategy used by the list. */
58-
focusStrategy = input<'roving tabindex' | 'activedescendant'>('roving tabindex');
73+
focusMode = input<'roving' | 'activedescendant'>('roving');
5974

6075
/** The selection strategy used by the list. */
61-
selectionStrategy = input<'follow' | 'explicit'>('follow');
76+
selectionMode = input<'follow' | 'explicit'>('follow');
6277

6378
/** The amount of time before the typeahead search is reset. */
64-
delay = input<number>(0.5);
79+
delay = input<number>(0.5); // Picked arbitrarily.
6580

6681
/** The ids of the current selected items. */
6782
selectedIds = model<string[]>([]);
@@ -75,15 +90,15 @@ export class CdkListbox implements ListboxInputs, OnDestroy {
7590
/** The Option UIPatterns of the child CdkOptions. */
7691
items = computed(() => this._cdkOptions().map(option => option.state));
7792

78-
/** The directionality (LTR / RTL) context for the application (or a subtree of it). */
79-
private _dir = inject(Directionality);
80-
8193
/** A signal wrapper for directionality. */
82-
directionality = signal<'rtl' | 'ltr'>('rtl');
94+
directionality = signal<'ltr' | 'rtl'>('ltr');
8395

8496
/** Emits when the list has been destroyed. */
8597
private readonly _destroyed = new Subject<void>();
8698

99+
/** Whether the listbox is disabled. */
100+
disabled = input<boolean>(false);
101+
87102
/** The Listbox UIPattern. */
88103
state: ListboxPattern = new ListboxPattern(this);
89104

@@ -98,6 +113,7 @@ export class CdkListbox implements ListboxInputs, OnDestroy {
98113
}
99114
}
100115

116+
/** A selectable option in a CdkListbox. */
101117
@Directive({
102118
selector: '[cdkOption]',
103119
exportAs: 'cdkOption',
@@ -107,12 +123,15 @@ export class CdkListbox implements ListboxInputs, OnDestroy {
107123
'[attr.aria-selected]': 'state.selected()',
108124
'[attr.tabindex]': 'state.tabindex()',
109125
'[attr.aria-disabled]': 'state.disabled()',
110-
// '[class.cdk-option-active]': 'isActive()',
111-
// '(click)': '_clicked.next($event)',
112-
// '(focus)': '_handleFocus()',
113126
},
114127
})
115128
export class CdkOption {
129+
/** A reference to the option element. */
130+
private _elementRef = inject(ElementRef);
131+
132+
/** The parent CdkListbox. */
133+
private _cdkListbox = inject(CdkListbox);
134+
116135
/** Whether an item is disabled. */
117136
disabled = input<boolean>(false);
118137

@@ -123,13 +142,8 @@ export class CdkOption {
123142
searchTerm = computed(() => this.label() ?? this.element().textContent);
124143

125144
/** A reference to the option element. */
126-
private _elementRef = inject(ElementRef);
127-
128145
element = computed(() => this._elementRef.nativeElement);
129146

130-
/** The parent CdkListbox. */
131-
private _cdkListbox = inject(CdkListbox);
132-
133147
/** The parent Listbox UIPattern. */
134148
listbox = computed(() => this._cdkListbox.state);
135149

src/cdk-experimental/ui-patterns/behaviors/event-manager/event-manager.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/**
1010
* An event that supports modifier keys.
11+
*
12+
* Matches the native KeyboardEvent, MouseEvent, and TouchEvent.
1113
*/
1214
export interface EventWithModifiers extends Event {
1315
ctrlKey: boolean;
@@ -18,6 +20,8 @@ export interface EventWithModifiers extends Event {
1820

1921
/**
2022
* Options that are applicable to all event handlers.
23+
*
24+
* This library has not yet had a need for stopPropagationImmediate.
2125
*/
2226
export interface EventHandlerOptions {
2327
stopPropagation: boolean;
@@ -44,6 +48,9 @@ export enum ModifierKey {
4448

4549
/**
4650
* Abstract base class for all event managers.
51+
*
52+
* Event managers are designed to normalize how event handlers are authored and create a safety net
53+
* for common event handling gotchas like remembering to call preventDefault or stopPropagation.
4754
*/
4855
export abstract class EventManager<T extends Event> {
4956
private _submanagers: EventManager<T>[] = [];

src/cdk-experimental/ui-patterns/behaviors/list-focus/controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class ListFocusController<T extends ListFocusItem> {
1414

1515
/** Focuses the current active item. */
1616
focus() {
17-
if (this.state.inputs.focusStrategy() === 'activedescendant') {
17+
if (this.state.inputs.focusMode() === 'activedescendant') {
1818
return;
1919
}
2020

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ describe('List Focus', () => {
4848
): ListFocus<T> {
4949
return new ListFocus({
5050
navigation,
51-
focusStrategy: signal('roving tabindex'),
51+
focusMode: signal('roving'),
5252
...args,
5353
});
5454
}
5555

56-
describe('roving tabindex', () => {
56+
describe('roving', () => {
5757
it('should set the list tabindex to -1', () => {
5858
const items = getItems(5);
5959
const nav = getNavigation(items);
@@ -110,7 +110,7 @@ describe('List Focus', () => {
110110
const items = getItems(5);
111111
const nav = getNavigation(items);
112112
const focus = getFocus(nav, {
113-
focusStrategy: signal('activedescendant'),
113+
focusMode: signal('activedescendant'),
114114
});
115115
const tabindex = focus.getListTabindex();
116116
expect(tabindex()).toBe(0);
@@ -120,7 +120,7 @@ describe('List Focus', () => {
120120
const items = getItems(5);
121121
const nav = getNavigation(items);
122122
const focus = getFocus(nav, {
123-
focusStrategy: signal('activedescendant'),
123+
focusMode: signal('activedescendant'),
124124
});
125125
const activeId = focus.getActiveDescendant();
126126
expect(activeId()).toBe(items()[0].id());
@@ -130,7 +130,7 @@ describe('List Focus', () => {
130130
const items = getItems(5);
131131
const nav = getNavigation(items);
132132
const focus = getFocus(nav, {
133-
focusStrategy: signal('activedescendant'),
133+
focusMode: signal('activedescendant'),
134134
});
135135

136136
items().forEach(i => {
@@ -148,7 +148,7 @@ describe('List Focus', () => {
148148
const items = getItems(5);
149149
const nav = getNavigation(items);
150150
const focus = getFocus(nav, {
151-
focusStrategy: signal('activedescendant'),
151+
focusMode: signal('activedescendant'),
152152
});
153153
const activeId = focus.getActiveDescendant();
154154

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface ListFocusItem extends ListNavigationItem {
2222
/** The required inputs for list focus. */
2323
export interface ListFocusInputs<T extends ListFocusItem> {
2424
/** The focus strategy used by the list. */
25-
focusStrategy: Signal<'roving tabindex' | 'activedescendant'>;
25+
focusMode: Signal<'roving' | 'activedescendant'>;
2626
}
2727

2828
/** Controls focus for a list of items. */
@@ -53,7 +53,7 @@ export class ListFocus<T extends ListFocusItem> {
5353
/** Returns the id of the current active item. */
5454
getActiveDescendant(): Signal<string | null> {
5555
return computed(() => {
56-
if (this.inputs.focusStrategy() === 'roving tabindex') {
56+
if (this.inputs.focusMode() === 'roving') {
5757
return null;
5858
}
5959
return this.navigation.inputs.items()[this.navigation.inputs.activeIndex()].id();
@@ -62,13 +62,13 @@ export class ListFocus<T extends ListFocusItem> {
6262

6363
/** Returns a signal that keeps track of the tabindex for the list. */
6464
getListTabindex(): Signal<-1 | 0> {
65-
return computed(() => (this.inputs.focusStrategy() === 'activedescendant' ? 0 : -1));
65+
return computed(() => (this.inputs.focusMode() === 'activedescendant' ? 0 : -1));
6666
}
6767

6868
/** Returns a signal that keeps track of the tabindex for the given item. */
6969
getItemTabindex(item: T): Signal<-1 | 0> {
7070
return computed(() => {
71-
if (this.inputs.focusStrategy() === 'activedescendant') {
71+
if (this.inputs.focusMode() === 'activedescendant') {
7272
return -1;
7373
}
7474
const index = this.navigation.inputs.items().indexOf(item);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('List Selection', () => {
5151
navigation,
5252
selectedIds: signal([]),
5353
multiselectable: signal(true),
54-
selectionStrategy: signal('explicit'),
54+
selectionMode: signal('explicit'),
5555
...args,
5656
});
5757
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface ListSelectionInputs<T extends ListSelectionItem> {
3131
selectedIds: WritableSignal<string[]>;
3232

3333
/** The selection strategy used by the list. */
34-
selectionStrategy: Signal<'follow' | 'explicit'>;
34+
selectionMode: Signal<'follow' | 'explicit'>;
3535
}
3636

3737
/** Controls selection for a list of items. */

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface SelectOptions {
2525

2626
/** Controls selection for a list of items. */
2727
export class ListboxController {
28-
followFocus = computed(() => this.state.inputs.selectionStrategy() === 'follow');
28+
followFocus = computed(() => this.state.inputs.selectionMode() === 'follow');
2929

3030
/** The key used to navigate to the previous item in the list. */
3131
prevKey = computed(() => {
@@ -124,11 +124,15 @@ export class ListboxController {
124124

125125
/** Handles keydown events for the listbox. */
126126
onKeydown(event: KeyboardEvent) {
127-
this.keydown().handle(event);
127+
if (!this.state.disabled()) {
128+
this.keydown().handle(event);
129+
}
128130
}
129131

130132
onMousedown(event: MouseEvent) {
131-
this.mousedown().handle(event);
133+
if (!this.state.disabled()) {
134+
this.mousedown().handle(event);
135+
}
132136
}
133137

134138
/** Navigates to the first option in the listbox. */

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import {ListboxController} from './controller';
1818
export type ListboxInputs = ListNavigationInputs<OptionPattern> &
1919
ListSelectionInputs<OptionPattern> &
2020
ListTypeaheadInputs &
21-
ListFocusInputs<OptionPattern>;
21+
ListFocusInputs<OptionPattern> & {
22+
disabled: Signal<boolean>;
23+
};
2224

2325
/** Controls the state of a listbox. */
2426
export class ListboxPattern {
@@ -37,6 +39,9 @@ export class ListboxPattern {
3739
/** Whether the list is vertically or horizontally oriented. */
3840
orientation: Signal<'vertical' | 'horizontal'>;
3941

42+
/** Whether the listbox is disabled. */
43+
disabled: Signal<boolean>;
44+
4045
/** The tabindex of the listbox. */
4146
tabindex: Signal<-1 | 0>;
4247

@@ -58,6 +63,7 @@ export class ListboxPattern {
5863
private _controller: ListboxController | null = null;
5964

6065
constructor(readonly inputs: ListboxInputs) {
66+
this.disabled = inputs.disabled;
6167
this.orientation = inputs.orientation;
6268
this.multiselectable = inputs.multiselectable;
6369

src/components-examples/cdk-experimental/listbox/cdk-listbox/cdk-listbox-example.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<div class="example-listbox-controls">
22
<mat-checkbox [formControl]="wrap">Wrap</mat-checkbox>
33
<mat-checkbox [formControl]="multi">Multi</mat-checkbox>
4+
<mat-checkbox [formControl]="disabled">Disabled</mat-checkbox>
45
<mat-checkbox [formControl]="skipDisabled">Skip Disabled</mat-checkbox>
56

67
<mat-form-field subscriptSizing="dynamic" appearance="outline">
@@ -13,15 +14,15 @@
1314

1415
<mat-form-field subscriptSizing="dynamic" appearance="outline">
1516
<mat-label>Selection strategy</mat-label>
16-
<mat-select [(value)]="selectionStrategy">
17+
<mat-select [(value)]="selectionMode">
1718
<mat-option value="explicit">Explicit</mat-option>
1819
<mat-option value="follow">Follow Focus</mat-option>
1920
</mat-select>
2021
</mat-form-field>
2122

2223
<mat-form-field subscriptSizing="dynamic" appearance="outline">
2324
<mat-label>Focus strategy</mat-label>
24-
<mat-select [(value)]="focusStrategy">
25+
<mat-select [(value)]="focusMode">
2526
<mat-option value="roving tabindex">Roving Tabindex</mat-option>
2627
<mat-option value="activedescendant">Active Descendant</mat-option>
2728
</mat-select>
@@ -32,11 +33,12 @@
3233
<ul
3334
cdkListbox
3435
[wrap]="wrap.value"
36+
[disabled]="disabled.value"
3537
[multiselectable]="multi.value"
3638
[skipDisabled]="skipDisabled.value"
3739
[orientation]="orientation"
38-
[focusStrategy]="focusStrategy"
39-
[selectionStrategy]="selectionStrategy"
40+
[focusMode]="focusMode"
41+
[selectionMode]="selectionMode"
4042
>
4143
<label id="fruit-example-label">List of Fruits</label>
4244

0 commit comments

Comments
 (0)