Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /