Skip to content

Commit 5f65d22

Browse files
committed
feat(cdk-experimental/radio): create radio group and button directives
1 parent 9f249d0 commit 5f65d22

File tree

6 files changed

+215
-0
lines changed

6 files changed

+215
-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',

src/cdk-experimental/config.bzl

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

0 commit comments

Comments
 (0)