Skip to content

Commit 6d912d6

Browse files
committed
fixup! feat(cdk-experimental/ui-patterns): listbox ui pattern
1 parent 4e7e9a7 commit 6d912d6

File tree

9 files changed

+81
-63
lines changed

9 files changed

+81
-63
lines changed

src/cdk-experimental/listbox/listbox.ts

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {
10+
booleanAttribute,
1011
computed,
1112
contentChildren,
1213
Directive,
@@ -18,7 +19,7 @@ import {
1819
signal,
1920
} from '@angular/core';
2021
import {OptionPattern} from '@angular/cdk-experimental/ui-patterns/listbox/option';
21-
import {ListboxInputs, ListboxPattern} from '@angular/cdk-experimental/ui-patterns/listbox/listbox';
22+
import {ListboxPattern} from '@angular/cdk-experimental/ui-patterns/listbox/listbox';
2223
import {Directionality} from '@angular/cdk/bidi';
2324
import {startWith, takeUntil} from 'rxjs/operators';
2425
import {Subject} from 'rxjs';
@@ -43,31 +44,43 @@ import {Subject} from 'rxjs';
4344
host: {
4445
'role': 'listbox',
4546
'class': 'cdk-listbox',
46-
'[attr.tabindex]': 'state.tabindex()',
47-
'[attr.aria-disabled]': 'state.disabled()',
48-
'[attr.aria-multiselectable]': 'state.multiselectable()',
49-
'[attr.aria-activedescendant]': 'state.activedescendant()',
50-
'[attr.aria-orientation]': 'state.orientation()',
51-
'(focusin)': 'state.onFocus()',
52-
'(keydown)': 'state.onKeydown($event)',
53-
'(mousedown)': 'state.onMousedown($event)',
47+
'[attr.tabindex]': 'pattern.tabindex()',
48+
'[attr.aria-disabled]': 'pattern.disabled()',
49+
'[attr.aria-multiselectable]': 'pattern.multiselectable()',
50+
'[attr.aria-activedescendant]': 'pattern.activedescendant()',
51+
'[attr.aria-orientation]': 'pattern.orientation()',
52+
'(focusin)': 'pattern.onFocus()',
53+
'(keydown)': 'pattern.onKeydown($event)',
54+
'(mousedown)': 'pattern.onMousedown($event)',
5455
},
5556
})
56-
export class CdkListbox implements ListboxInputs, OnDestroy {
57+
export class CdkListbox implements OnDestroy {
5758
/** The directionality (LTR / RTL) context for the application (or a subtree of it). */
5859
private _dir = inject(Directionality);
5960

61+
/** A signal wrapper for directionality. */
62+
private directionality = signal<'ltr' | 'rtl'>('ltr');
63+
64+
/** The CdkOptions nested inside of the CdkListbox. */
65+
private _cdkOptions = contentChildren(CdkOption, {descendants: true});
66+
67+
/** The Option UIPatterns of the child CdkOptions. */
68+
private items = computed(() => this._cdkOptions().map(option => option.pattern));
69+
70+
/** Emits when the list has been destroyed. */
71+
private readonly _destroyed = new Subject<void>();
72+
6073
/** Whether the list is vertically or horizontally oriented. */
6174
orientation = input<'vertical' | 'horizontal'>('vertical');
6275

6376
/** Whether multiple items in the list can be selected at once. */
64-
multiselectable = input<boolean>(false);
77+
multiselectable = input(false, {transform: booleanAttribute});
6578

6679
/** Whether focus should wrap when navigating. */
67-
wrap = input<boolean>(true);
80+
wrap = input(true, {transform: booleanAttribute});
6881

6982
/** Whether disabled items in the list should be skipped when navigating. */
70-
skipDisabled = input<boolean>(true);
83+
skipDisabled = input(true, {transform: booleanAttribute});
7184

7285
/** The focus strategy used by the list. */
7386
focusMode = input<'roving' | 'activedescendant'>('roving');
@@ -76,31 +89,23 @@ export class CdkListbox implements ListboxInputs, OnDestroy {
7689
selectionMode = input<'follow' | 'explicit'>('follow');
7790

7891
/** The amount of time before the typeahead search is reset. */
79-
delay = input<number>(0.5); // Picked arbitrarily.
92+
typeaheadDelay = input<number>(0.5); // Picked arbitrarily.
93+
94+
/** Whether the listbox is disabled. */
95+
disabled = input(false, {transform: booleanAttribute});
8096

8197
/** The ids of the current selected items. */
8298
selectedIds = model<string[]>([]);
8399

84100
/** The current index that has been navigated to. */
85101
activeIndex = model<number>(0);
86102

87-
/** The CdkOptions nested inside of the CdkListbox. */
88-
private _cdkOptions = contentChildren(CdkOption, {descendants: true});
89-
90-
/** The Option UIPatterns of the child CdkOptions. */
91-
items = computed(() => this._cdkOptions().map(option => option.state));
92-
93-
/** A signal wrapper for directionality. */
94-
directionality = signal<'ltr' | 'rtl'>('ltr');
95-
96-
/** Emits when the list has been destroyed. */
97-
private readonly _destroyed = new Subject<void>();
98-
99-
/** Whether the listbox is disabled. */
100-
disabled = input<boolean>(false);
101-
102103
/** The Listbox UIPattern. */
103-
state: ListboxPattern = new ListboxPattern(this);
104+
pattern: ListboxPattern = new ListboxPattern({
105+
...this,
106+
items: this.items,
107+
directionality: this.directionality,
108+
});
104109

105110
constructor() {
106111
this._dir.change
@@ -113,16 +118,19 @@ export class CdkListbox implements ListboxInputs, OnDestroy {
113118
}
114119
}
115120

121+
// TODO(wagnermaciel): Figure out how we actually want to do this.
122+
let count = 0;
123+
116124
/** A selectable option in a CdkListbox. */
117125
@Directive({
118126
selector: '[cdkOption]',
119127
exportAs: 'cdkOption',
120128
host: {
121129
'role': 'option',
122130
'class': 'cdk-option',
123-
'[attr.aria-selected]': 'state.selected()',
124-
'[attr.tabindex]': 'state.tabindex()',
125-
'[attr.aria-disabled]': 'state.disabled()',
131+
'[attr.aria-selected]': 'pattern.selected()',
132+
'[attr.tabindex]': 'pattern.tabindex()',
133+
'[attr.aria-disabled]': 'pattern.disabled()',
126134
},
127135
})
128136
export class CdkOption {
@@ -132,21 +140,30 @@ export class CdkOption {
132140
/** The parent CdkListbox. */
133141
private _cdkListbox = inject(CdkListbox);
134142

135-
/** Whether an item is disabled. */
136-
disabled = input<boolean>(false);
143+
/** A unique identifier for the option. */
144+
private id = computed(() => `${count++}`);
137145

138146
/** The text used by the typeahead search. */
139-
label = input<string>();
147+
private searchTerm = computed(() => this.label() ?? this.element().textContent);
140148

141-
/** The text used by the typeahead search. */
142-
searchTerm = computed(() => this.label() ?? this.element().textContent);
149+
/** The parent Listbox UIPattern. */
150+
private listbox = computed(() => this._cdkListbox.pattern);
143151

144-
/** A reference to the option element. */
145-
element = computed(() => this._elementRef.nativeElement);
152+
/** A reference to the option element to be focused on navigation. */
153+
private element = computed(() => this._elementRef.nativeElement);
146154

147-
/** The parent Listbox UIPattern. */
148-
listbox = computed(() => this._cdkListbox.state);
155+
/** Whether an item is disabled. */
156+
disabled = input(false, {transform: booleanAttribute});
157+
158+
/** The text used by the typeahead search. */
159+
label = input<string>();
149160

150161
/** The Option UIPattern. */
151-
state = new OptionPattern(this);
162+
pattern = new OptionPattern({
163+
...this,
164+
id: this.id,
165+
listbox: this.listbox,
166+
element: this.element,
167+
searchTerm: this.searchTerm,
168+
});
152169
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export abstract class EventManager<T extends Event> {
9999
for (const submanager of this._submanagers) {
100100
await submanager.handle(event);
101101
}
102-
for (const config of this.getConfigs(event)) {
102+
for (const config of this.getHandlersForKey(event)) {
103103
await config.handler(event);
104104
if (config.stopPropagation) {
105105
event.stopPropagation();
@@ -139,13 +139,15 @@ export abstract class EventManager<T extends Event> {
139139
/**
140140
* Gets all of the handler configs that are applicable to the given event.
141141
*/
142-
protected abstract getConfigs(event: T): EventHandlerConfig<T>[];
142+
protected abstract getHandlersForKey(event: T): EventHandlerConfig<T>[];
143143

144144
/**
145145
* Checks whether this event manager is confugred to handle the given event.
146146
*/
147147
protected isHandled(event: T): boolean {
148-
return this.getConfigs(event).length > 0 || this._submanagers.some(sm => sm.isHandled(event));
148+
return (
149+
this.getHandlersForKey(event).length > 0 || this._submanagers.some(sm => sm.isHandled(event))
150+
);
149151
}
150152
}
151153

@@ -164,7 +166,7 @@ export class GenericEventManager<T extends Event> extends EventManager<T> {
164166
return this;
165167
}
166168

167-
getConfigs(_event: T): EventHandlerConfig<T>[] {
169+
getHandlersForKey(_event: T): EventHandlerConfig<T>[] {
168170
return this.configs;
169171
}
170172
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ export class KeyboardEventManager extends EventManager<KeyboardEvent> {
8282
return this;
8383
}
8484

85-
getConfigs(event: KeyboardEvent) {
86-
return this.configs.filter(config => this._checkKey(config, event));
85+
getHandlersForKey(event: KeyboardEvent) {
86+
return this.configs.filter(config => this._isKeyMatch(config, event));
8787
}
8888

8989
// TODO: Make modifiers accept a signal as well.
9090

91-
private _checkKey(config: KeyboardEventHandlerConfig, event: KeyboardEvent) {
91+
private _isKeyMatch(config: KeyboardEventHandlerConfig, event: KeyboardEvent) {
9292
if (config.key instanceof RegExp) {
9393
return config.key.test(event.key);
9494
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export class MouseEventManager extends EventManager<MouseEvent> {
125125
};
126126
}
127127

128-
getConfigs(event: MouseEvent) {
128+
getHandlersForKey(event: MouseEvent) {
129129
const configs: MouseEventHandlerConfig[] = [];
130130
for (const config of this.configs) {
131131
if (config.button === (event.button ?? 0) && hasModifiers(event, config.modifiers)) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class ListTypeaheadController<T extends ListTypeaheadItem> {
4343
this.timeout = setTimeout(() => {
4444
this.query.set('');
4545
this.anchorIndex.set(null);
46-
}, this.state.inputs.delay() * 1000);
46+
}, this.state.inputs.typeaheadDelay() * 1000);
4747
}
4848

4949
/**

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('List Typeahead', () => {
4040
});
4141
const typeahead = new ListTypeahead({
4242
navigation,
43-
delay: signal(0.5),
43+
typeaheadDelay: signal(0.5),
4444
});
4545

4646
await typeahead.search('i');
@@ -67,7 +67,7 @@ describe('List Typeahead', () => {
6767
});
6868
const typeahead = new ListTypeahead({
6969
navigation,
70-
delay: signal(0.5),
70+
typeaheadDelay: signal(0.5),
7171
});
7272

7373
await typeahead.search('i');
@@ -92,7 +92,7 @@ describe('List Typeahead', () => {
9292
});
9393
const typeahead = new ListTypeahead({
9494
navigation,
95-
delay: signal(0.5),
95+
typeaheadDelay: signal(0.5),
9696
});
9797
items()[1].disabled.set(true);
9898

@@ -113,7 +113,7 @@ describe('List Typeahead', () => {
113113
});
114114
const typeahead = new ListTypeahead({
115115
navigation,
116-
delay: signal(0.5),
116+
typeaheadDelay: signal(0.5),
117117
});
118118
items()[1].disabled.set(true);
119119

@@ -134,7 +134,7 @@ describe('List Typeahead', () => {
134134
});
135135
const typeahead = new ListTypeahead({
136136
navigation,
137-
delay: signal(0.5),
137+
typeaheadDelay: signal(0.5),
138138
});
139139

140140
await typeahead.search('i');

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface ListTypeaheadItem extends ListNavigationItem {
1919
/** The required inputs for list typeahead. */
2020
export interface ListTypeaheadInputs {
2121
/** The amount of time before the typeahead search is reset. */
22-
delay: Signal<number>;
22+
typeaheadDelay: Signal<number>;
2323
}
2424

2525
/** Controls typeahead for a list of items. */

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@ export interface OptionInputs
2222
listbox: Signal<ListboxPattern>;
2323
}
2424

25-
let count = 0;
26-
2725
/** An option in a listbox. */
2826
export class OptionPattern {
2927
/** A unique identifier for the option. */
30-
id = signal(`${count++}`);
28+
id: Signal<string>;
3129

3230
/** The position of the option in the list. */
3331
index = computed(
@@ -55,7 +53,8 @@ export class OptionPattern {
5553
/** The html element that should receive focus. */
5654
element: Signal<HTMLElement>;
5755

58-
constructor(args: Omit<OptionInputs, 'id'>) {
56+
constructor(args: OptionInputs) {
57+
this.id = args.id;
5958
this.listbox = args.listbox;
6059
this.element = args.element;
6160
this.disabled = args.disabled;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<label id="fruit-example-label">List of Fruits</label>
4444

4545
@for (fruit of fruits; track fruit) {
46-
@let checked = option.state.selected() ? 'checked' : 'unchecked';
46+
@let checked = option.pattern.selected() ? 'checked' : 'unchecked';
4747

4848
<li cdkOption #option="cdkOption">
4949
<mat-pseudo-checkbox [state]="checked"></mat-pseudo-checkbox>

0 commit comments

Comments
 (0)