Skip to content

Commit 647985f

Browse files
committed
feat(cdk-experimental/ui-patterns): toolbar and toolbar widget
1 parent 8da079d commit 647985f

File tree

9 files changed

+353
-10
lines changed

9 files changed

+353
-10
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/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+
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 (if using activedescendant or if in toolbar). */
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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-focus",
14+
"//src/cdk-experimental/ui-patterns/behaviors/list-navigation",
15+
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
16+
"//src/cdk-experimental/ui-patterns/radio-group",
17+
"//src/cdk/bidi",
18+
],
19+
)

0 commit comments

Comments
 (0)