Skip to content

Commit 15e81c9

Browse files
committed
feat(cdk-experimental/radio): create radio group and button
* Create new UI Patterns: RadioGroupPattern and RadioButtonPattern. * Create new experimental Cdk directives: CdkRadioGroup and CdkRadioButton. * Add a new demo for the new Cdk directives under /cdk-experimental-radio.
1 parent 7791972 commit 15e81c9

File tree

21 files changed

+753
-0
lines changed

21 files changed

+753
-0
lines changed

.ng-dev/commit-message.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const commitMessage: CommitMessageConfig = {
1313
'cdk-experimental/combobox',
1414
'cdk-experimental/listbox',
1515
'cdk-experimental/popover-edit',
16+
'cdk-experimental/radio',
1617
'cdk-experimental/scrolling',
1718
'cdk-experimental/selection',
1819
'cdk-experimental/table-scroll-container',
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 = "radio",
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+
)

src/cdk-experimental/radio/index.ts

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 {CdkRadioGroup, CdkRadioButton} from './radio';

src/cdk-experimental/radio/radio.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
AfterViewInit,
11+
booleanAttribute,
12+
computed,
13+
contentChildren,
14+
Directive,
15+
effect,
16+
ElementRef,
17+
inject,
18+
input,
19+
model,
20+
signal,
21+
} from '@angular/core';
22+
import {RadioButtonPattern, RadioGroupPattern} from '../ui-patterns';
23+
import {Directionality} from '@angular/cdk/bidi';
24+
import {toSignal} from '@angular/core/rxjs-interop';
25+
import {_IdGenerator} from '@angular/cdk/a11y';
26+
27+
/**
28+
* A radio button group container.
29+
*
30+
* Radio groups are used to group multiple radio buttons or radio group labels so they function as
31+
* a single form control. The CdkRadioGroup is meant to be used in conjunction with CdkRadioButton
32+
* as follows:
33+
*
34+
* ```html
35+
* <div cdkRadioGroup>
36+
* <label cdkRadioButton value="1">Option 1</label>
37+
* <label cdkRadioButton value="2">Option 2</label>
38+
* <label cdkRadioButton value="3">Option 3</label>
39+
* </div>
40+
* ```
41+
*/
42+
@Directive({
43+
selector: '[cdkRadioGroup]',
44+
exportAs: 'cdkRadioGroup',
45+
host: {
46+
'role': 'radiogroup',
47+
'class': 'cdk-radio-group',
48+
'[attr.tabindex]': 'pattern.tabindex()',
49+
'[attr.aria-readonly]': 'pattern.readonly()',
50+
'[attr.aria-disabled]': 'pattern.disabled()',
51+
'[attr.aria-orientation]': 'pattern.orientation()',
52+
'[attr.aria-activedescendant]': 'pattern.activedescendant()',
53+
'(keydown)': 'pattern.onKeydown($event)',
54+
'(pointerdown)': 'pattern.onPointerdown($event)',
55+
'(focusin)': 'onFocus()',
56+
},
57+
})
58+
export class CdkRadioGroup<V> implements AfterViewInit {
59+
/** The directionality (LTR / RTL) context for the application (or a subtree of it). */
60+
private readonly _directionality = inject(Directionality);
61+
62+
/** The CdkRadioButtons nested inside of the CdkRadioGroup. */
63+
private readonly _cdkRadioButtons = contentChildren(CdkRadioButton, {descendants: true});
64+
65+
/** A signal wrapper for directionality. */
66+
protected textDirection = toSignal(this._directionality.change, {
67+
initialValue: this._directionality.value,
68+
});
69+
70+
/** The RadioButton UIPatterns of the child CdkRadioButtons. */
71+
protected items = computed(() => this._cdkRadioButtons().map(radio => radio.pattern));
72+
73+
/** Whether the radio group is vertically or horizontally oriented. */
74+
orientation = input<'vertical' | 'horizontal'>('vertical');
75+
76+
/** Whether focus should wrap when navigating. */
77+
wrap = input(false, {transform: booleanAttribute}); // Radio groups typically don't wrap
78+
79+
/** Whether disabled items in the group should be skipped when navigating. */
80+
skipDisabled = input(true, {transform: booleanAttribute});
81+
82+
/** The focus strategy used by the radio group. */
83+
focusMode = input<'roving' | 'activedescendant'>('roving');
84+
85+
/** Whether the radio group is disabled. */
86+
disabled = input(false, {transform: booleanAttribute});
87+
88+
/** Whether the radio group is readonly. */
89+
readonly = input(false, {transform: booleanAttribute});
90+
91+
/** The value of the currently selected radio button. */
92+
value = model<V[]>([]); // TODO: Change this to just be model(V|null).
93+
94+
/** The current index that has been navigated to. */
95+
activeIndex = model<number>(0);
96+
97+
/** The RadioGroup UIPattern. */
98+
pattern: RadioGroupPattern<V> = new RadioGroupPattern<V>({
99+
...this,
100+
items: this.items,
101+
textDirection: this.textDirection,
102+
});
103+
104+
/** Whether the radio group has received focus yet. */
105+
private _hasFocused = signal(false);
106+
107+
/** Whether the radio buttons in the group have been initialized. */
108+
private _isViewInitialized = signal(false);
109+
110+
constructor() {
111+
effect(() => {
112+
if (this._isViewInitialized() && !this._hasFocused()) {
113+
this.pattern.setDefaultState();
114+
}
115+
});
116+
}
117+
118+
ngAfterViewInit() {
119+
this._isViewInitialized.set(true);
120+
}
121+
122+
onFocus() {
123+
this._hasFocused.set(true);
124+
}
125+
}
126+
127+
/** A selectable radio button in a CdkRadioGroup. */
128+
@Directive({
129+
selector: '[cdkRadioButton]',
130+
exportAs: 'cdkRadioButton',
131+
host: {
132+
'role': 'radio',
133+
'class': 'cdk-radio-button',
134+
'[class.cdk-active]': 'pattern.active()',
135+
'[attr.tabindex]': 'pattern.tabindex()',
136+
'[attr.aria-checked]': 'pattern.selected()',
137+
'[attr.aria-disabled]': 'pattern.disabled()',
138+
},
139+
})
140+
export class CdkRadioButton<V> {
141+
/** A reference to the radio button element. */
142+
private readonly _elementRef = inject(ElementRef);
143+
144+
/** The parent CdkRadioGroup. */
145+
private readonly _cdkRadioGroup = inject(CdkRadioGroup);
146+
147+
/** A unique identifier for the radio button. */
148+
private readonly _generatedId = inject(_IdGenerator).getId('cdk-radio-button-');
149+
150+
/** A unique identifier for the radio button. */
151+
protected id = computed(() => this._generatedId);
152+
153+
/** The value associated with the radio button. */
154+
protected value = input.required<V>();
155+
156+
/** The parent RadioGroup UIPattern. */
157+
protected group = computed(() => this._cdkRadioGroup.pattern);
158+
159+
/** A reference to the radio button element to be focused on navigation. */
160+
protected element = computed(() => this._elementRef.nativeElement);
161+
162+
/** Whether the radio button is disabled. */
163+
disabled = input(false, {transform: booleanAttribute});
164+
165+
/** The RadioButton UIPattern. */
166+
pattern = new RadioButtonPattern<V>({
167+
...this,
168+
id: this.id,
169+
value: this.value,
170+
group: this.group,
171+
element: this.element,
172+
});
173+
}

src/cdk-experimental/ui-patterns/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_project(
1212
"//:node_modules/@angular/core",
1313
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
1414
"//src/cdk-experimental/ui-patterns/listbox",
15+
"//src/cdk-experimental/ui-patterns/radio",
1516
"//src/cdk-experimental/ui-patterns/tabs",
1617
],
1718
)

src/cdk-experimental/ui-patterns/public-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88

99
export * from './listbox/listbox';
1010
export * from './listbox/option';
11+
export * from './radio/radio-group';
12+
export * from './radio/radio';
1113
export * from './behaviors/signal-like/signal-like';
1214
export * from './tabs/tabs';
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 = "radio",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//:node_modules/@angular/core",
13+
"//src/cdk-experimental/ui-patterns/behaviors/event-manager",
14+
"//src/cdk-experimental/ui-patterns/behaviors/list-focus",
15+
"//src/cdk-experimental/ui-patterns/behaviors/list-navigation",
16+
"//src/cdk-experimental/ui-patterns/behaviors/list-selection",
17+
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
18+
],
19+
)

0 commit comments

Comments
 (0)