Skip to content

Commit 8281276

Browse files
authored
feat(cdk-experimental/ui-patterns): toolbar and toolbar widget (#31670)
* feat(cdk-experimental/ui-patterns): toolbar and toolbar widget * fix(cdk-experimental/radio-group): adding toolbar property to radio group * fix(cdk-experimental/ui-patterns): toolbar cleanup
1 parent 223e114 commit 8281276

File tree

10 files changed

+381
-11
lines changed

10 files changed

+381
-11
lines changed

.ng-dev/commit-message.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const commitMessage: CommitMessageConfig = {
1919
'cdk-experimental/selection',
2020
'cdk-experimental/table-scroll-container',
2121
'cdk-experimental/tabs',
22+
'cdk-experimental/toolbar',
2223
'cdk-experimental/tree',
2324
'cdk-experimental/ui-patterns',
2425
'cdk/a11y',

src/cdk-experimental/radio-group/radio-group.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export class CdkRadioGroup<V> {
131131
value: this._value,
132132
activeItem: signal(undefined),
133133
textDirection: this.textDirection,
134+
toolbar: signal(undefined), // placeholder until Toolbar CDK is added
134135
});
135136

136137
/** Whether the radio group has received focus yet. */

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ ts_project(
1515
"//src/cdk-experimental/ui-patterns/listbox",
1616
"//src/cdk-experimental/ui-patterns/radio-group",
1717
"//src/cdk-experimental/ui-patterns/tabs",
18+
"//src/cdk-experimental/ui-patterns/toolbar",
1819
"//src/cdk-experimental/ui-patterns/tree",
1920
],
2021
)

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,14 @@ export class ListFocus<T extends ListFocusItem> {
4848
prevActiveItem = signal<T | undefined>(undefined);
4949

5050
/** The index of the last item that was active. */
51-
prevActiveIndex = computed(() => this.prevActiveItem()?.index() ?? -1);
51+
prevActiveIndex = computed(() => {
52+
return this.prevActiveItem() ? this.inputs.items().indexOf(this.prevActiveItem()!) : -1;
53+
});
5254

5355
/** The current active index in the list. */
54-
activeIndex = computed(() => this.inputs.activeItem()?.index() ?? -1);
56+
activeIndex = computed(() => {
57+
return this.inputs.activeItem() ? this.inputs.items().indexOf(this.inputs.activeItem()!) : -1;
58+
});
5559

5660
constructor(readonly inputs: ListFocusInputs<T>) {}
5761

src/cdk-experimental/ui-patterns/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './radio-group/radio-button';
1313
export * from './behaviors/signal-like/signal-like';
1414
export * from './tabs/tabs';
1515
export * from './accordion/accordion';
16+
export * from './toolbar/toolbar';

src/cdk-experimental/ui-patterns/radio-group/radio-button.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,30 @@ import {computed} from '@angular/core';
1010
import {SignalLike} from '../behaviors/signal-like/signal-like';
1111
import {List, ListItem} from '../behaviors/list/list';
1212

13+
/**
14+
* Represents the properties exposed by a toolbar widget that need to be accessed by a radio group.
15+
* This exists to avoid circular dependency errors between the toolbar and radio button.
16+
*/
17+
type ToolbarWidgetLike = {
18+
id: SignalLike<string>;
19+
index: SignalLike<number>;
20+
element: SignalLike<HTMLElement>;
21+
disabled: SignalLike<boolean>;
22+
searchTerm: SignalLike<any>;
23+
value: SignalLike<any>;
24+
};
25+
1326
/**
1427
* Represents the properties exposed by a radio group that need to be accessed by a radio button.
1528
* This exists to avoid circular dependency errors between the radio group and radio button.
1629
*/
1730
interface RadioGroupLike<V> {
1831
/** The list behavior for the radio group. */
19-
listBehavior: List<RadioButtonPattern<V>, V>;
32+
listBehavior: List<RadioButtonPattern<V> | ToolbarWidgetLike, V>;
33+
/** Whether the list is readonly */
34+
readonly: SignalLike<boolean>;
35+
/** Whether the radio group is disabled. */
36+
disabled: SignalLike<boolean>;
2037
}
2138

2239
/** Represents the required inputs for a radio button in a radio group. */
@@ -34,7 +51,9 @@ export class RadioButtonPattern<V> {
3451
value: SignalLike<V>;
3552

3653
/** The position of the radio button within the group. */
37-
index = computed(() => this.group()?.listBehavior.inputs.items().indexOf(this) ?? -1);
54+
index: SignalLike<number> = computed(
55+
() => this.group()?.listBehavior.inputs.items().indexOf(this) ?? -1,
56+
);
3857

3958
/** Whether the radio button is currently the active one (focused). */
4059
active = computed(() => this.group()?.listBehavior.inputs.activeItem() === this);

src/cdk-experimental/ui-patterns/radio-group/radio-group.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,37 @@ export type RadioGroupInputs<V> = Omit<
2222

2323
/** Whether the radio group is readonly. */
2424
readonly: SignalLike<boolean>;
25+
/** Parent toolbar of radio group */
26+
toolbar: SignalLike<ToolbarLike<V> | undefined>;
2527
};
2628

29+
/**
30+
* Represents the properties exposed by a toolbar widget that need to be accessed by a radio group.
31+
* This exists to avoid circular dependency errors between the toolbar and radio button.
32+
*/
33+
type ToolbarWidgetLike = {
34+
id: SignalLike<string>;
35+
index: SignalLike<number>;
36+
element: SignalLike<HTMLElement>;
37+
disabled: SignalLike<boolean>;
38+
searchTerm: SignalLike<any>;
39+
value: SignalLike<any>;
40+
};
41+
42+
/**
43+
* Represents the properties exposed by a toolbar that need to be accessed by a radio group.
44+
* This exists to avoid circular dependency errors between the toolbar and radio button.
45+
*/
46+
export interface ToolbarLike<V> {
47+
listBehavior: List<RadioButtonPattern<V> | ToolbarWidgetLike, V>;
48+
orientation: SignalLike<'vertical' | 'horizontal'>;
49+
disabled: SignalLike<boolean>;
50+
}
51+
2752
/** Controls the state of a radio group. */
2853
export class RadioGroupPattern<V> {
2954
/** The list behavior for the radio group. */
30-
readonly listBehavior: List<RadioButtonPattern<V>, V>;
55+
readonly listBehavior: List<RadioButtonPattern<V> | ToolbarWidgetLike, V>;
3156

3257
/** Whether the radio group is vertically or horizontally oriented. */
3358
orientation: SignalLike<'vertical' | 'horizontal'>;
@@ -41,8 +66,8 @@ export class RadioGroupPattern<V> {
4166
/** Whether the radio group is readonly. */
4267
readonly = computed(() => this.selectedItem()?.disabled() || this.inputs.readonly());
4368

44-
/** The tabindex of the radio group (if using activedescendant). */
45-
tabindex = computed(() => this.listBehavior.tabindex());
69+
/** The tabindex of the radio group. */
70+
tabindex = computed(() => (this.inputs.toolbar() ? -1 : this.listBehavior.tabindex()));
4671

4772
/** The id of the current active radio button (if using activedescendant). */
4873
activedescendant = computed(() => this.listBehavior.activedescendant());
@@ -67,6 +92,11 @@ export class RadioGroupPattern<V> {
6792
keydown = computed(() => {
6893
const manager = new KeyboardEventManager();
6994

95+
// When within a toolbar relinquish keyboard control
96+
if (this.inputs.toolbar()) {
97+
return manager;
98+
}
99+
70100
// Readonly mode allows navigation but not selection changes.
71101
if (this.readonly()) {
72102
return manager
@@ -91,6 +121,11 @@ export class RadioGroupPattern<V> {
91121
pointerdown = computed(() => {
92122
const manager = new PointerEventManager();
93123

124+
// When within a toolbar relinquish pointer control
125+
if (this.inputs.toolbar()) {
126+
return manager;
127+
}
128+
94129
if (this.readonly()) {
95130
// Navigate focus only in readonly mode.
96131
return manager.on(e => this.listBehavior.goto(this._getItem(e)!));
@@ -101,13 +136,15 @@ export class RadioGroupPattern<V> {
101136
});
102137

103138
constructor(readonly inputs: RadioGroupInputs<V>) {
104-
this.orientation = inputs.orientation;
139+
this.orientation =
140+
inputs.toolbar() !== undefined ? inputs.toolbar()!.orientation : inputs.orientation;
105141

106142
this.listBehavior = new List({
107143
...inputs,
108-
wrap: () => false,
144+
activeItem: inputs.toolbar()?.listBehavior.inputs.activeItem ?? inputs.activeItem,
145+
wrap: () => !!inputs.toolbar(),
109146
multi: () => false,
110-
selectionMode: () => 'follow',
147+
selectionMode: () => (inputs.toolbar() ? 'explicit' : 'follow'),
111148
typeaheadDelay: () => 0, // Radio groups do not support typeahead.
112149
});
113150
}

src/cdk-experimental/ui-patterns/radio-group/radio.spec.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {signal, WritableSignal} from '@angular/core';
10-
import {RadioGroupInputs, RadioGroupPattern} from './radio-group';
10+
import {RadioGroupInputs, RadioGroupPattern, ToolbarLike} from './radio-group';
1111
import {RadioButtonPattern} from './radio-button';
1212
import {createKeyboardEvent} from '@angular/cdk/testing/private';
1313
import {ModifierKeys} from '@angular/cdk/testing';
@@ -39,6 +39,7 @@ describe('RadioGroup Pattern', () => {
3939
focusMode: inputs.focusMode ?? signal('roving'),
4040
textDirection: inputs.textDirection ?? signal('ltr'),
4141
orientation: inputs.orientation ?? signal('vertical'),
42+
toolbar: inputs.toolbar ?? signal(undefined),
4243
});
4344
}
4445

@@ -303,4 +304,49 @@ describe('RadioGroup Pattern', () => {
303304
expect(violations.length).toBe(1);
304305
});
305306
});
307+
308+
describe('toolbar', () => {
309+
let radioGroup: TestRadioGroup;
310+
let radioButtons: TestRadio[];
311+
let toolbar: ToolbarLike<string>;
312+
313+
beforeEach(() => {
314+
const patterns = getDefaultPatterns();
315+
radioGroup = patterns.radioGroup;
316+
radioButtons = patterns.radioButtons;
317+
toolbar = {
318+
listBehavior: radioGroup.listBehavior,
319+
orientation: radioGroup.orientation,
320+
disabled: radioGroup.disabled,
321+
};
322+
radioGroup.inputs.toolbar = signal(toolbar);
323+
});
324+
325+
it('should ignore keyboard navigation when within a toolbar', () => {
326+
const initialActive = radioGroup.inputs.activeItem();
327+
radioGroup.onKeydown(down());
328+
expect(radioGroup.inputs.activeItem()).toBe(initialActive);
329+
});
330+
331+
it('should ignore keyboard selection when within a toolbar', () => {
332+
expect(radioGroup.inputs.value()).toEqual([]);
333+
radioGroup.onKeydown(space());
334+
expect(radioGroup.inputs.value()).toEqual([]);
335+
radioGroup.onKeydown(enter());
336+
expect(radioGroup.inputs.value()).toEqual([]);
337+
});
338+
339+
it('should ignore pointer events when within a toolbar', () => {
340+
const initialActive = radioGroup.inputs.activeItem();
341+
expect(radioGroup.inputs.value()).toEqual([]);
342+
343+
const clickEvent = {
344+
target: radioButtons[1].element(),
345+
} as unknown as PointerEvent;
346+
radioGroup.onPointerdown(clickEvent);
347+
348+
expect(radioGroup.inputs.activeItem()).toBe(initialActive);
349+
expect(radioGroup.inputs.value()).toEqual([]);
350+
});
351+
});
306352
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
load("//tools:defaults.bzl", "ts_project")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_project(
6+
name = "toolbar",
7+
srcs = [
8+
"toolbar.ts",
9+
],
10+
deps = [
11+
"//:node_modules/@angular/core",
12+
"//src/cdk-experimental/ui-patterns/behaviors/event-manager",
13+
"//src/cdk-experimental/ui-patterns/behaviors/list",
14+
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
15+
"//src/cdk-experimental/ui-patterns/radio-group",
16+
],
17+
)

0 commit comments

Comments
 (0)