Skip to content

Commit d02338b

Browse files
authored
refactor(cdk-experimental/ui-patterns): add toolbar widget group to decouple toolbar and radio group (#31840)
1 parent a2b2b9b commit d02338b

19 files changed

+1665
-653
lines changed

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

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@ import {
1919
model,
2020
signal,
2121
WritableSignal,
22-
OnDestroy,
2322
} from '@angular/core';
24-
import {RadioButtonPattern, RadioGroupPattern} from '../ui-patterns';
23+
import {
24+
RadioButtonPattern,
25+
RadioGroupInputs,
26+
RadioGroupPattern,
27+
ToolbarRadioGroupInputs,
28+
ToolbarRadioGroupPattern,
29+
} from '../ui-patterns';
2530
import {Directionality} from '@angular/cdk/bidi';
2631
import {_IdGenerator} from '@angular/cdk/a11y';
27-
import {CdkToolbar} from '../toolbar';
32+
import {CdkToolbarWidgetGroup} from '@angular/cdk-experimental/toolbar';
2833

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

@@ -91,43 +96,49 @@ export function mapSignal<T, V>(
9196
'(pointerdown)': 'pattern.onPointerdown($event)',
9297
'(focusin)': 'onFocus()',
9398
},
99+
hostDirectives: [
100+
{
101+
directive: CdkToolbarWidgetGroup,
102+
inputs: ['disabled'],
103+
},
104+
],
94105
})
95106
export class CdkRadioGroup<V> {
96107
/** A reference to the radio group element. */
97108
private readonly _elementRef = inject(ElementRef);
98109

110+
/** A reference to the CdkToolbarWidgetGroup, if the radio group is in a toolbar. */
111+
private readonly _cdkToolbarWidgetGroup = inject(CdkToolbarWidgetGroup);
112+
113+
/** Whether the radio group is inside of a CdkToolbar. */
114+
private readonly _hasToolbar = computed(() => !!this._cdkToolbarWidgetGroup.toolbar());
115+
99116
/** The CdkRadioButtons nested inside of the CdkRadioGroup. */
100117
private readonly _cdkRadioButtons = contentChildren(CdkRadioButton, {descendants: true});
101118

102119
/** A signal wrapper for directionality. */
103120
protected textDirection = inject(Directionality).valueSignal;
104121

105-
/** A signal wrapper for toolbar. */
106-
toolbar = inject(CdkToolbar, {optional: true});
107-
108-
/** Toolbar pattern if applicable */
109-
private readonly _toolbarPattern = computed(() => this.toolbar?.pattern);
110-
111122
/** The RadioButton UIPatterns of the child CdkRadioButtons. */
112123
protected items = computed(() => this._cdkRadioButtons().map(radio => radio.pattern));
113124

114125
/** Whether the radio group is vertically or horizontally oriented. */
115-
orientation = input<'vertical' | 'horizontal'>('vertical');
126+
readonly orientation = input<'vertical' | 'horizontal'>('vertical');
116127

117128
/** Whether disabled items in the group should be skipped when navigating. */
118-
skipDisabled = input(true, {transform: booleanAttribute});
129+
readonly skipDisabled = input(true, {transform: booleanAttribute});
119130

120131
/** The focus strategy used by the radio group. */
121-
focusMode = input<'roving' | 'activedescendant'>('roving');
132+
readonly focusMode = input<'roving' | 'activedescendant'>('roving');
122133

123134
/** Whether the radio group is disabled. */
124-
disabled = input(false, {transform: booleanAttribute});
135+
readonly disabled = input(false, {transform: booleanAttribute});
125136

126137
/** Whether the radio group is readonly. */
127-
readonly = input(false, {transform: booleanAttribute});
138+
readonly readonly = input(false, {transform: booleanAttribute});
128139

129140
/** The value of the currently selected radio button. */
130-
value = model<V | null>(null);
141+
readonly value = model<V | null>(null);
131142

132143
/** The internal selection state for the radio group. */
133144
private readonly _value = mapSignal<V | null, V[]>(this.value, {
@@ -136,22 +147,37 @@ export class CdkRadioGroup<V> {
136147
});
137148

138149
/** The RadioGroup UIPattern. */
139-
pattern: RadioGroupPattern<V> = new RadioGroupPattern<V>({
140-
...this,
141-
items: this.items,
142-
value: this._value,
143-
activeItem: signal(undefined),
144-
textDirection: this.textDirection,
145-
toolbar: this._toolbarPattern,
146-
element: () => this._elementRef.nativeElement,
147-
focusMode: this._toolbarPattern()?.inputs.focusMode ?? this.focusMode,
148-
skipDisabled: this._toolbarPattern()?.inputs.skipDisabled ?? this.skipDisabled,
149-
});
150+
readonly pattern: RadioGroupPattern<V>;
150151

151152
/** Whether the radio group has received focus yet. */
152153
private _hasFocused = signal(false);
153154

154155
constructor() {
156+
const inputs: RadioGroupInputs<V> | ToolbarRadioGroupInputs<V> = {
157+
...this,
158+
items: this.items,
159+
value: this._value,
160+
activeItem: signal(undefined),
161+
textDirection: this.textDirection,
162+
element: () => this._elementRef.nativeElement,
163+
getItem: e => {
164+
if (!(e.target instanceof HTMLElement)) {
165+
return undefined;
166+
}
167+
const element = e.target.closest('[role="radio"]');
168+
return this.items().find(i => i.element() === element);
169+
},
170+
toolbar: this._cdkToolbarWidgetGroup.toolbar,
171+
};
172+
173+
this.pattern = this._hasToolbar()
174+
? new ToolbarRadioGroupPattern<V>(inputs as ToolbarRadioGroupInputs<V>)
175+
: new RadioGroupPattern<V>(inputs as RadioGroupInputs<V>);
176+
177+
if (this._hasToolbar()) {
178+
this._cdkToolbarWidgetGroup.controls.set(this.pattern as ToolbarRadioGroupPattern<V>);
179+
}
180+
155181
afterRenderEffect(() => {
156182
if (typeof ngDevMode === 'undefined' || ngDevMode) {
157183
const violations = this.pattern.validate();
@@ -162,35 +188,15 @@ export class CdkRadioGroup<V> {
162188
});
163189

164190
afterRenderEffect(() => {
165-
if (!this._hasFocused() && !this.toolbar) {
191+
if (!this._hasFocused() && !this._hasToolbar()) {
166192
this.pattern.setDefaultState();
167193
}
168194
});
169-
170-
// TODO: Refactor to be handled within list behavior
171-
afterRenderEffect(() => {
172-
if (this.toolbar) {
173-
const radioButtons = this._cdkRadioButtons();
174-
// If the group is disabled and the toolbar is set to skip disabled items,
175-
// the radio buttons should not be part of the toolbar's navigation.
176-
if (this.disabled() && this.toolbar.skipDisabled()) {
177-
radioButtons.forEach(radio => this.toolbar!.unregister(radio));
178-
} else {
179-
radioButtons.forEach(radio => this.toolbar!.register(radio));
180-
}
181-
}
182-
});
183195
}
184196

185197
onFocus() {
186198
this._hasFocused.set(true);
187199
}
188-
189-
toolbarButtonUnregister(radio: CdkRadioButton<V>) {
190-
if (this.toolbar) {
191-
this.toolbar.unregister(radio);
192-
}
193-
}
194200
}
195201

196202
/** A selectable radio button in a CdkRadioGroup. */
@@ -207,7 +213,7 @@ export class CdkRadioGroup<V> {
207213
'[id]': 'pattern.id()',
208214
},
209215
})
210-
export class CdkRadioButton<V> implements OnDestroy {
216+
export class CdkRadioButton<V> {
211217
/** A reference to the radio button element. */
212218
private readonly _elementRef = inject(ElementRef);
213219

@@ -218,13 +224,13 @@ export class CdkRadioButton<V> implements OnDestroy {
218224
private readonly _generatedId = inject(_IdGenerator).getId('cdk-radio-button-');
219225

220226
/** A unique identifier for the radio button. */
221-
protected id = computed(() => this._generatedId);
227+
readonly id = computed(() => this._generatedId);
222228

223229
/** The value associated with the radio button. */
224230
readonly value = input.required<V>();
225231

226232
/** The parent RadioGroup UIPattern. */
227-
protected group = computed(() => this._cdkRadioGroup.pattern);
233+
readonly group = computed(() => this._cdkRadioGroup.pattern);
228234

229235
/** A reference to the radio button element to be focused on navigation. */
230236
element = computed(() => this._elementRef.nativeElement);
@@ -240,10 +246,4 @@ export class CdkRadioButton<V> implements OnDestroy {
240246
group: this.group,
241247
element: this.element,
242248
});
243-
244-
ngOnDestroy() {
245-
if (this._cdkRadioGroup.toolbar) {
246-
this._cdkRadioGroup.toolbarButtonUnregister(this);
247-
}
248-
}
249249
}

src/cdk-experimental/toolbar/BUILD.bazel

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//tools:defaults.bzl", "ng_project")
1+
load("//tools:defaults.bzl", "ng_project", "ng_web_test_suite", "ts_project")
22

33
package(default_visibility = ["//visibility:public"])
44

@@ -15,3 +15,22 @@ ng_project(
1515
"//src/cdk/bidi",
1616
],
1717
)
18+
19+
ts_project(
20+
name = "unit_test_sources",
21+
testonly = True,
22+
srcs = [
23+
"toolbar.spec.ts",
24+
],
25+
deps = [
26+
":toolbar",
27+
"//:node_modules/@angular/core",
28+
"//:node_modules/@angular/platform-browser",
29+
"//src/cdk/testing/private",
30+
],
31+
)
32+
33+
ng_web_test_suite(
34+
name = "unit_tests",
35+
deps = [":unit_test_sources"],
36+
)

src/cdk-experimental/toolbar/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export {CdkToolbar, CdkToolbarWidget} from './toolbar';
9+
export {CdkToolbar, CdkToolbarWidget, CdkToolbarWidgetGroup} from './toolbar';

0 commit comments

Comments
 (0)