Skip to content

Commit c2cb82d

Browse files
committed
refactor(cdk-experimental/ui-patterns): add toolbar widget group to decouple toolbar and radio group
1 parent c21dfa3 commit c2cb82d

17 files changed

+1599
-641
lines changed

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

Lines changed: 47 additions & 46 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,23 +96,24 @@ export function mapSignal<T, V>(
9196
'(pointerdown)': 'pattern.onPointerdown($event)',
9297
'(focusin)': 'onFocus()',
9398
},
99+
hostDirectives: [CdkToolbarWidgetGroup],
94100
})
95101
export class CdkRadioGroup<V> {
96102
/** A reference to the radio group element. */
97103
private readonly _elementRef = inject(ElementRef);
98104

105+
/** A reference to the CdkToolbarWidgetGroup, if the radio group is in a toolbar. */
106+
private readonly _cdkToolbarWidgetGroup = inject(CdkToolbarWidgetGroup);
107+
108+
/** Whether the radio group is inside of a CdkToolbar. */
109+
private readonly _hasToolbar = computed(() => !!this._cdkToolbarWidgetGroup.toolbar());
110+
99111
/** The CdkRadioButtons nested inside of the CdkRadioGroup. */
100112
private readonly _cdkRadioButtons = contentChildren(CdkRadioButton, {descendants: true});
101113

102114
/** A signal wrapper for directionality. */
103115
protected textDirection = inject(Directionality).valueSignal;
104116

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-
111117
/** The RadioButton UIPatterns of the child CdkRadioButtons. */
112118
protected items = computed(() => this._cdkRadioButtons().map(radio => radio.pattern));
113119

@@ -136,22 +142,37 @@ export class CdkRadioGroup<V> {
136142
});
137143

138144
/** 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-
});
145+
readonly pattern: RadioGroupPattern<V>;
150146

151147
/** Whether the radio group has received focus yet. */
152148
private _hasFocused = signal(false);
153149

154150
constructor() {
151+
const inputs: RadioGroupInputs<V> | ToolbarRadioGroupInputs<V> = {
152+
...this,
153+
items: this.items,
154+
value: this._value,
155+
activeItem: signal(undefined),
156+
textDirection: this.textDirection,
157+
element: () => this._elementRef.nativeElement,
158+
getItem: e => {
159+
if (!(e.target instanceof HTMLElement)) {
160+
return undefined;
161+
}
162+
const element = e.target.closest('[role="radio"]');
163+
return this.items().find(i => i.element() === element);
164+
},
165+
toolbar: this._cdkToolbarWidgetGroup.toolbar,
166+
};
167+
168+
this.pattern = this._hasToolbar()
169+
? new ToolbarRadioGroupPattern(inputs as ToolbarRadioGroupInputs<V>)
170+
: new RadioGroupPattern(inputs as RadioGroupInputs<V>);
171+
172+
if (this._hasToolbar()) {
173+
this._cdkToolbarWidgetGroup.controls.set(this.pattern as ToolbarRadioGroupPattern<V>);
174+
}
175+
155176
afterRenderEffect(() => {
156177
if (typeof ngDevMode === 'undefined' || ngDevMode) {
157178
const violations = this.pattern.validate();
@@ -162,35 +183,21 @@ export class CdkRadioGroup<V> {
162183
});
163184

164185
afterRenderEffect(() => {
165-
if (!this._hasFocused() && !this.toolbar) {
186+
if (!this._hasFocused() && !this._hasToolbar()) {
166187
this.pattern.setDefaultState();
167188
}
168189
});
169190

170-
// TODO: Refactor to be handled within list behavior
171191
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-
}
192+
if (this._hasToolbar()) {
193+
this._cdkToolbarWidgetGroup.disabled.set(this.disabled());
181194
}
182195
});
183196
}
184197

185198
onFocus() {
186199
this._hasFocused.set(true);
187200
}
188-
189-
toolbarButtonUnregister(radio: CdkRadioButton<V>) {
190-
if (this.toolbar) {
191-
this.toolbar.unregister(radio);
192-
}
193-
}
194201
}
195202

196203
/** A selectable radio button in a CdkRadioGroup. */
@@ -207,7 +214,7 @@ export class CdkRadioGroup<V> {
207214
'[id]': 'pattern.id()',
208215
},
209216
})
210-
export class CdkRadioButton<V> implements OnDestroy {
217+
export class CdkRadioButton<V> {
211218
/** A reference to the radio button element. */
212219
private readonly _elementRef = inject(ElementRef);
213220

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

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

223230
/** The value associated with the radio button. */
224231
readonly value = input.required<V>();
225232

226233
/** The parent RadioGroup UIPattern. */
227-
protected group = computed(() => this._cdkRadioGroup.pattern);
234+
readonly group = computed(() => this._cdkRadioGroup.pattern);
228235

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

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)