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 048de42

Browse files
authored
feat(cdk-experimental/toolbar): add toolbar directive and demo (#31676)
* feat(cdk-experimental/toolbar): add toolbar directive and demo * fix(cdk-experimental/toolbar): lint correction * fix(cdk-experimental/toolbar): focus mode removal * fix(cdk-experimental/toolbar): skip disabled default false * fix(cdk-experimental/toolbar): uregister wording
1 parent 9a81066 commit 048de42

File tree

19 files changed

+685
-4
lines changed

19 files changed

+685
-4
lines changed

‎src/cdk-experimental/config.bzl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [
1010
"scrolling",
1111
"selection",
1212
"tabs",
13+
"toolbar",
1314
"tree",
1415
"table-scroll-container",
1516
"ui-patterns",

‎src/cdk-experimental/radio-group/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ng_project(
1010
),
1111
deps = [
1212
"//:node_modules/@angular/core",
13+
"//src/cdk-experimental/toolbar",
1314
"//src/cdk-experimental/ui-patterns",
1415
"//src/cdk/a11y",
1516
"//src/cdk/bidi",

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import {
1919
model,
2020
signal,
2121
WritableSignal,
22+
OnDestroy,
2223
} from '@angular/core';
2324
import {RadioButtonPattern, RadioGroupPattern} from '../ui-patterns';
2425
import {Directionality} from '@angular/cdk/bidi';
2526
import {_IdGenerator} from '@angular/cdk/a11y';
27+
import {CdkToolbar} from '../toolbar';
2628

2729
// TODO: Move mapSignal to it's own file so it can be reused across components.
2830

@@ -97,6 +99,12 @@ export class CdkRadioGroup<V> {
9799
/** A signal wrapper for directionality. */
98100
protected textDirection = inject(Directionality).valueSignal;
99101

102+
/** A signal wrapper for toolbar. */
103+
toolbar = inject(CdkToolbar, {optional: true});
104+
105+
/** Toolbar pattern if applicable */
106+
private readonly _toolbarPattern = computed(() => this.toolbar?.pattern);
107+
100108
/** The RadioButton UIPatterns of the child CdkRadioButtons. */
101109
protected items = computed(() => this._cdkRadioButtons().map(radio => radio.pattern));
102110

@@ -131,7 +139,9 @@ export class CdkRadioGroup<V> {
131139
value: this._value,
132140
activeItem: signal(undefined),
133141
textDirection: this.textDirection,
134-
toolbar: signal(undefined), // placeholder until Toolbar CDK is added
142+
toolbar: this._toolbarPattern,
143+
focusMode: this._toolbarPattern()?.inputs.focusMode ?? this.focusMode,
144+
skipDisabled: this._toolbarPattern()?.inputs.skipDisabled ?? this.skipDisabled,
135145
});
136146

137147
/** Whether the radio group has received focus yet. */
@@ -148,15 +158,35 @@ export class CdkRadioGroup<V> {
148158
});
149159

150160
afterRenderEffect(() => {
151-
if (!this._hasFocused()) {
161+
if (!this._hasFocused()&&!this.toolbar) {
152162
this.pattern.setDefaultState();
153163
}
154164
});
165+
166+
// TODO: Refactor to be handled within list behavior
167+
afterRenderEffect(() => {
168+
if (this.toolbar) {
169+
const radioButtons = this._cdkRadioButtons();
170+
// If the group is disabled and the toolbar is set to skip disabled items,
171+
// the radio buttons should not be part of the toolbar's navigation.
172+
if (this.disabled() && this.toolbar.skipDisabled()) {
173+
radioButtons.forEach(radio => this.toolbar!.unregister(radio));
174+
} else {
175+
radioButtons.forEach(radio => this.toolbar!.register(radio));
176+
}
177+
}
178+
});
155179
}
156180

157181
onFocus() {
158182
this._hasFocused.set(true);
159183
}
184+
185+
toolbarButtonUnregister(radio: CdkRadioButton<V>) {
186+
if (this.toolbar) {
187+
this.toolbar.unregister(radio);
188+
}
189+
}
160190
}
161191

162192
/** A selectable radio button in a CdkRadioGroup. */
@@ -173,7 +203,7 @@ export class CdkRadioGroup<V> {
173203
'[id]': 'pattern.id()',
174204
},
175205
})
176-
export class CdkRadioButton<V> {
206+
export class CdkRadioButton<V> implementsOnDestroy{
177207
/** A reference to the radio button element. */
178208
private readonly _elementRef = inject(ElementRef);
179209

@@ -193,7 +223,7 @@ export class CdkRadioButton<V> {
193223
protected group = computed(() => this._cdkRadioGroup.pattern);
194224

195225
/** A reference to the radio button element to be focused on navigation. */
196-
protectedelement = computed(() => this._elementRef.nativeElement);
226+
element = computed(() => this._elementRef.nativeElement);
197227

198228
/** Whether the radio button is disabled. */
199229
disabled = input(false, {transform: booleanAttribute});
@@ -206,4 +236,10 @@ export class CdkRadioButton<V> {
206236
group: this.group,
207237
element: this.element,
208238
});
239+
240+
ngOnDestroy() {
241+
if (this._cdkRadioGroup.toolbar) {
242+
this._cdkRadioGroup.toolbarButtonUnregister(this);
243+
}
244+
}
209245
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
load("//tools:defaults.bzl", "ng_project")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_project(
6+
name = "toolbar",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//:node_modules/@angular/core",
13+
"//src/cdk-experimental/ui-patterns",
14+
"//src/cdk/a11y",
15+
"//src/cdk/bidi",
16+
],
17+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export * from './public-api';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export {CdkToolbar, CdkToolbarWidget} from './toolbar';
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
afterRenderEffect,
11+
Directive,
12+
ElementRef,
13+
inject,
14+
computed,
15+
input,
16+
booleanAttribute,
17+
signal,
18+
Signal,
19+
OnInit,
20+
OnDestroy,
21+
} from '@angular/core';
22+
import {ToolbarPattern, RadioButtonPattern, ToolbarWidgetPattern} from '../ui-patterns';
23+
import {Directionality} from '@angular/cdk/bidi';
24+
import {_IdGenerator} from '@angular/cdk/a11y';
25+
26+
/** Interface for a radio button that can be used with a toolbar. Based on radio-button in ui-patterns */
27+
interface CdkRadioButtonInterface<V> {
28+
/** The HTML element associated with the radio button. */
29+
element: Signal<HTMLElement>;
30+
/** Whether the radio button is disabled. */
31+
disabled: Signal<boolean>;
32+
33+
pattern: RadioButtonPattern<V>;
34+
}
35+
36+
interface HasElement {
37+
element: Signal<HTMLElement>;
38+
}
39+
40+
/**
41+
* Sort directives by their document order.
42+
*/
43+
function sortDirectives(a: HasElement, b: HasElement) {
44+
return (a.element().compareDocumentPosition(b.element()) & Node.DOCUMENT_POSITION_PRECEDING) > 0
45+
? 1
46+
: -1;
47+
}
48+
49+
/**
50+
* A toolbar widget container.
51+
*
52+
* Widgets such as radio groups or buttons are nested within a toolbar to allow for a single
53+
* place of reference for focus and navigation. The CdkToolbar is meant to be used in conjunction
54+
* with CdkToolbarWidget and CdkRadioGroup as follows:
55+
*
56+
* ```html
57+
* <div cdkToolbar>
58+
* <button cdkToolbarWidget>Button</button>
59+
* <div cdkRadioGroup>
60+
* <label cdkRadioButton value="1">Option 1</label>
61+
* <label cdkRadioButton value="2">Option 2</label>
62+
* <label cdkRadioButton value="3">Option 3</label>
63+
* </div>
64+
* </div>
65+
* ```
66+
*/
67+
@Directive({
68+
selector: '[cdkToolbar]',
69+
exportAs: 'cdkToolbar',
70+
host: {
71+
'role': 'toolbar',
72+
'class': 'cdk-toolbar',
73+
'[attr.tabindex]': 'pattern.tabindex()',
74+
'[attr.aria-disabled]': 'pattern.disabled()',
75+
'[attr.aria-orientation]': 'pattern.orientation()',
76+
'[attr.aria-activedescendant]': 'pattern.activedescendant()',
77+
'(keydown)': 'pattern.onKeydown($event)',
78+
'(pointerdown)': 'pattern.onPointerdown($event)',
79+
'(focusin)': 'onFocus()',
80+
},
81+
})
82+
export class CdkToolbar<V> {
83+
/** The CdkTabList nested inside of the container. */
84+
private readonly _cdkWidgets = signal(new Set<CdkRadioButtonInterface<V> | CdkToolbarWidget>());
85+
86+
/** A signal wrapper for directionality. */
87+
textDirection = inject(Directionality).valueSignal;
88+
89+
/** Sorted UIPatterns of the child widgets */
90+
items = computed(() =>
91+
[...this._cdkWidgets()].sort(sortDirectives).map(widget => widget.pattern),
92+
);
93+
94+
/** Whether the toolbar is vertically or horizontally oriented. */
95+
orientation = input<'vertical' | 'horizontal'>('horizontal');
96+
97+
/** Whether disabled items in the group should be skipped when navigating. */
98+
skipDisabled = input(false, {transform: booleanAttribute});
99+
100+
/** Whether the toolbar is disabled. */
101+
disabled = input(false, {transform: booleanAttribute});
102+
103+
/** Whether focus should wrap when navigating. */
104+
readonly wrap = input(true, {transform: booleanAttribute});
105+
106+
/** The toolbar UIPattern. */
107+
pattern: ToolbarPattern<V> = new ToolbarPattern<V>({
108+
...this,
109+
activeItem: signal(undefined),
110+
textDirection: this.textDirection,
111+
focusMode: signal('roving'),
112+
});
113+
114+
/** Whether the toolbar has received focus yet. */
115+
private _hasFocused = signal(false);
116+
117+
onFocus() {
118+
this._hasFocused.set(true);
119+
}
120+
121+
constructor() {
122+
afterRenderEffect(() => {
123+
if (!this._hasFocused()) {
124+
this.pattern.setDefaultState();
125+
}
126+
});
127+
128+
afterRenderEffect(() => {
129+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
130+
const violations = this.pattern.validate();
131+
for (const violation of violations) {
132+
console.error(violation);
133+
}
134+
}
135+
});
136+
}
137+
138+
register(widget: CdkRadioButtonInterface<V> | CdkToolbarWidget) {
139+
const widgets = this._cdkWidgets();
140+
if (!widgets.has(widget)) {
141+
widgets.add(widget);
142+
this._cdkWidgets.set(new Set(widgets));
143+
}
144+
}
145+
146+
unregister(widget: CdkRadioButtonInterface<V> | CdkToolbarWidget) {
147+
const widgets = this._cdkWidgets();
148+
if (widgets.delete(widget)) {
149+
this._cdkWidgets.set(new Set(widgets));
150+
}
151+
}
152+
}
153+
154+
/**
155+
* A widget within a toolbar.
156+
*
157+
* A widget is anything that is within a toolbar. It should be applied to any native HTML element
158+
* that has the purpose of acting as a widget navigatable within a toolbar.
159+
*/
160+
@Directive({
161+
selector: '[cdkToolbarWidget]',
162+
exportAs: 'cdkToolbarWidget',
163+
host: {
164+
'role': 'button',
165+
'class': 'cdk-toolbar-widget',
166+
'[class.cdk-active]': 'pattern.active()',
167+
'[attr.tabindex]': 'pattern.tabindex()',
168+
'[attr.inert]': 'hardDisabled() ? true : null',
169+
'[attr.disabled]': 'hardDisabled() ? true : null',
170+
'[attr.aria-disabled]': 'pattern.disabled()',
171+
'[id]': 'pattern.id()',
172+
},
173+
})
174+
export class CdkToolbarWidget implements OnInit, OnDestroy {
175+
/** A reference to the widget element. */
176+
private readonly _elementRef = inject(ElementRef);
177+
178+
/** The parent CdkToolbar. */
179+
private readonly _cdkToolbar = inject(CdkToolbar);
180+
181+
/** A unique identifier for the widget. */
182+
private readonly _generatedId = inject(_IdGenerator).getId('cdk-toolbar-widget-');
183+
184+
/** A unique identifier for the widget. */
185+
protected id = computed(() => this._generatedId);
186+
187+
/** The parent Toolbar UIPattern. */
188+
protected parentToolbar = computed(() => this._cdkToolbar.pattern);
189+
190+
/** A reference to the widget element to be focused on navigation. */
191+
element = computed(() => this._elementRef.nativeElement);
192+
193+
/** Whether the widget is disabled. */
194+
disabled = input(false, {transform: booleanAttribute});
195+
196+
readonly hardDisabled = computed(
197+
() => this.pattern.disabled() && this._cdkToolbar.skipDisabled(),
198+
);
199+
200+
pattern = new ToolbarWidgetPattern({
201+
...this,
202+
id: this.id,
203+
element: this.element,
204+
disabled: computed(() => this._cdkToolbar.disabled() || this.disabled()),
205+
parentToolbar: this.parentToolbar,
206+
});
207+
208+
ngOnInit() {
209+
this._cdkToolbar.register(this);
210+
}
211+
212+
ngOnDestroy() {
213+
this._cdkToolbar.unregister(this);
214+
}
215+
}

0 commit comments

Comments
(0)

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