Skip to content

Commit 2620e55

Browse files
committed
refactor(cdk-experimental/ui-patterns): switch radio-group to use the list behavior
1 parent ca46fb9 commit 2620e55

File tree

3 files changed

+44
-100
lines changed

3 files changed

+44
-100
lines changed

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ ts_project(
1111
deps = [
1212
"//:node_modules/@angular/core",
1313
"//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",
14+
"//src/cdk-experimental/ui-patterns/behaviors/list",
1715
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
1816
],
1917
)

src/cdk-experimental/ui-patterns/radio-group/radio-button.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,20 @@
77
*/
88

99
import {computed} from '@angular/core';
10-
import {ListSelection, ListSelectionItem} from '../behaviors/list-selection/list-selection';
11-
import {ListNavigation, ListNavigationItem} from '../behaviors/list-navigation/list-navigation';
12-
import {ListFocus, ListFocusItem} from '../behaviors/list-focus/list-focus';
1310
import {SignalLike} from '../behaviors/signal-like/signal-like';
11+
import {List, ListItem} from '../behaviors/list/list';
1412

1513
/**
1614
* Represents the properties exposed by a radio group that need to be accessed by a radio button.
1715
* This exists to avoid circular dependency errors between the radio group and radio button.
1816
*/
1917
interface RadioGroupLike<V> {
20-
focusManager: ListFocus<RadioButtonPattern<V>>;
21-
selection: ListSelection<RadioButtonPattern<V>, V>;
22-
navigation: ListNavigation<RadioButtonPattern<V>>;
18+
/** The list behavior for the radio group. */
19+
listBehavior: List<RadioButtonPattern<V>, V>;
2320
}
2421

2522
/** Represents the required inputs for a radio button in a radio group. */
26-
export interface RadioButtonInputs<V>
27-
extends ListNavigationItem,
28-
ListSelectionItem<V>,
29-
ListFocusItem {
23+
export interface RadioButtonInputs<V> extends Omit<ListItem<V>, 'searchTerm'> {
3024
/** A reference to the parent radio group. */
3125
group: SignalLike<RadioGroupLike<V> | undefined>;
3226
}
@@ -43,16 +37,16 @@ export class RadioButtonPattern<V> {
4337
index = computed(
4438
() =>
4539
this.group()
46-
?.navigation.inputs.items()
40+
?.listBehavior.inputs.items()
4741
.findIndex(i => i.id() === this.id()) ?? -1,
4842
);
4943

5044
/** Whether the radio button is currently the active one (focused). */
51-
active = computed(() => this.group()?.focusManager.activeItem() === this);
45+
active = computed(() => this.group()?.listBehavior.activeItem() === this);
5246

5347
/** Whether the radio button is selected. */
5448
selected: SignalLike<boolean> = computed(
55-
() => !!this.group()?.selection.inputs.value().includes(this.value()),
49+
() => !!this.group()?.listBehavior.inputs.value().includes(this.value()),
5650
);
5751

5852
/** Whether the radio button is disabled. */
@@ -62,11 +56,14 @@ export class RadioButtonPattern<V> {
6256
group: SignalLike<RadioGroupLike<V> | undefined>;
6357

6458
/** The tabindex of the radio button. */
65-
tabindex = computed(() => this.group()?.focusManager.getItemTabindex(this));
59+
tabindex = computed(() => this.group()?.listBehavior.getItemTabindex(this));
6660

6761
/** The HTML element associated with the radio button. */
6862
element: SignalLike<HTMLElement>;
6963

64+
/** The search term for typeahead. */
65+
readonly searchTerm = () => ''; // Radio groups do not support typeahead.
66+
7067
constructor(readonly inputs: RadioButtonInputs<V>) {
7168
this.id = inputs.id;
7269
this.value = inputs.value;

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

Lines changed: 32 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -8,56 +8,44 @@
88

99
import {computed} from '@angular/core';
1010
import {KeyboardEventManager, PointerEventManager} from '../behaviors/event-manager';
11-
import {ListFocus, ListFocusInputs} from '../behaviors/list-focus/list-focus';
12-
import {ListNavigation, ListNavigationInputs} from '../behaviors/list-navigation/list-navigation';
13-
import {ListSelection, ListSelectionInputs} from '../behaviors/list-selection/list-selection';
11+
import {List, ListInputs} from '../behaviors/list/list';
1412
import {SignalLike} from '../behaviors/signal-like/signal-like';
1513
import {RadioButtonPattern} from './radio-button';
1614

17-
/** The selection operations that the radio group can perform. */
18-
interface SelectOptions {
19-
selectOne?: boolean;
20-
}
21-
2215
/** Represents the required inputs for a radio group. */
23-
export type RadioGroupInputs<V> = Omit<ListNavigationInputs<RadioButtonPattern<V>>, 'wrap'> &
24-
// Radio groups are always single-select.
25-
Omit<ListSelectionInputs<RadioButtonPattern<V>, V>, 'multi' | 'selectionMode'> &
26-
ListFocusInputs<RadioButtonPattern<V>> & {
27-
/** Whether the radio group is disabled. */
28-
disabled: SignalLike<boolean>;
29-
/** Whether the radio group is readonly. */
30-
readonly: SignalLike<boolean>;
31-
};
16+
export type RadioGroupInputs<V> = Omit<
17+
ListInputs<RadioButtonPattern<V>, V>,
18+
'multi' | 'selectionMode' | 'wrap' | 'typeaheadDelay'
19+
> & {
20+
/** Whether the radio group is disabled. */
21+
disabled: SignalLike<boolean>;
22+
23+
/** Whether the radio group is readonly. */
24+
readonly: SignalLike<boolean>;
25+
};
3226

3327
/** Controls the state of a radio group. */
3428
export class RadioGroupPattern<V> {
35-
/** Controls navigation for the radio group. */
36-
navigation: ListNavigation<RadioButtonPattern<V>>;
37-
38-
/** Controls selection for the radio group. */
39-
selection: ListSelection<RadioButtonPattern<V>, V>;
40-
41-
/** Controls focus for the radio group. */
42-
focusManager: ListFocus<RadioButtonPattern<V>>;
29+
/** The list behavior for the radio group. */
30+
readonly listBehavior: List<RadioButtonPattern<V>, V>;
4331

4432
/** Whether the radio group is vertically or horizontally oriented. */
4533
orientation: SignalLike<'vertical' | 'horizontal'>;
4634

4735
/** Whether the radio group is disabled. */
48-
disabled = computed(() => this.inputs.disabled() || this.focusManager.isListDisabled());
36+
disabled = computed(() => this.inputs.disabled() || this.listBehavior.disabled());
4937

5038
/** The currently selected radio button. */
51-
selectedItem = computed(() => this.selection.selectedItems()[0]);
39+
selectedItem = computed(() => this.listBehavior.selectionBehavior.selectedItems()[0]);
5240

5341
/** Whether the radio group is readonly. */
5442
readonly = computed(() => this.selectedItem()?.disabled() || this.inputs.readonly());
5543

5644
/** The tabindex of the radio group (if using activedescendant). */
57-
tabindex = computed(() => this.focusManager.getListTabindex());
45+
tabindex = computed(() => this.listBehavior.tabindex());
5846

5947
/** The id of the current active radio button (if using activedescendant). */
60-
activedescendant = computed(() => this.focusManager.getActiveDescendant());
48+
activedescendant = computed(() => this.listBehavior.activedescendant());
6149

6250
/** The key used to navigate to the previous radio button. */
6351
prevKey = computed(() => {
@@ -82,21 +70,21 @@ export class RadioGroupPattern<V> {
8270
// Readonly mode allows navigation but not selection changes.
8371
if (this.readonly()) {
8472
return manager
85-
.on(this.prevKey, () => this.prev())
86-
.on(this.nextKey, () => this.next())
87-
.on('Home', () => this.first())
88-
.on('End', () => this.last());
73+
.on(this.prevKey, () => this.listBehavior.prev())
74+
.on(this.nextKey, () => this.listBehavior.next())
75+
.on('Home', () => this.listBehavior.first())
76+
.on('End', () => this.listBehavior.last());
8977
}
9078

9179
// Default behavior: navigate and select on arrow keys, home, end.
9280
// Space/Enter also select the focused item.
9381
return manager
94-
.on(this.prevKey, () => this.prev({selectOne: true}))
95-
.on(this.nextKey, () => this.next({selectOne: true}))
96-
.on('Home', () => this.first({selectOne: true}))
97-
.on('End', () => this.last({selectOne: true}))
98-
.on(' ', () => this.selection.selectOne())
99-
.on('Enter', () => this.selection.selectOne());
82+
.on(this.prevKey, () => this.listBehavior.prev({selectOne: true}))
83+
.on(this.nextKey, () => this.listBehavior.next({selectOne: true}))
84+
.on('Home', () => this.listBehavior.first({selectOne: true}))
85+
.on('End', () => this.listBehavior.last({selectOne: true}))
86+
.on(' ', () => this.listBehavior.selectOne())
87+
.on('Enter', () => this.listBehavior.selectOne());
10088
});
10189

10290
/** The pointerdown event manager for the radio group. */
@@ -105,27 +93,22 @@ export class RadioGroupPattern<V> {
10593

10694
if (this.readonly()) {
10795
// Navigate focus only in readonly mode.
108-
return manager.on(e => this.goto(e));
96+
return manager.on(e => this.listBehavior.goto(this._getItem(e)!));
10997
}
11098

11199
// Default behavior: navigate and select on click.
112-
return manager.on(e => this.goto(e, {selectOne: true}));
100+
return manager.on(e => this.listBehavior.goto(this._getItem(e)!, {selectOne: true}));
113101
});
114102

115103
constructor(readonly inputs: RadioGroupInputs<V>) {
116104
this.orientation = inputs.orientation;
117105

118-
this.focusManager = new ListFocus(inputs);
119-
this.navigation = new ListNavigation({
106+
this.listBehavior = new List({
120107
...inputs,
121108
wrap: () => false,
122-
focusManager: this.focusManager,
123-
});
124-
this.selection = new ListSelection({
125-
...inputs,
126109
multi: () => false,
127110
selectionMode: () => 'follow',
128-
focusManager: this.focusManager,
111+
typeaheadDelay: () => 0, // Radio groups do not support typeahead.
129112
});
130113
}
131114

@@ -143,32 +126,6 @@ export class RadioGroupPattern<V> {
143126
}
144127
}
145128

146-
/** Navigates to the first enabled radio button in the group. */
147-
first(opts?: SelectOptions) {
148-
this._navigate(opts, () => this.navigation.first());
149-
}
150-
151-
/** Navigates to the last enabled radio button in the group. */
152-
last(opts?: SelectOptions) {
153-
this._navigate(opts, () => this.navigation.last());
154-
}
155-
156-
/** Navigates to the next enabled radio button in the group. */
157-
next(opts?: SelectOptions) {
158-
this._navigate(opts, () => this.navigation.next());
159-
}
160-
161-
/** Navigates to the previous enabled radio button in the group. */
162-
prev(opts?: SelectOptions) {
163-
this._navigate(opts, () => this.navigation.prev());
164-
}
165-
166-
/** Navigates to the radio button associated with the given pointer event. */
167-
goto(event: PointerEvent, opts?: SelectOptions) {
168-
const item = this._getItem(event);
169-
this._navigate(opts, () => this.navigation.goto(item));
170-
}
171-
172129
/**
173130
* Sets the radio group to its default initial state.
174131
*
@@ -179,7 +136,7 @@ export class RadioGroupPattern<V> {
179136
let firstItem: RadioButtonPattern<V> | null = null;
180137

181138
for (const item of this.inputs.items()) {
182-
if (this.focusManager.isFocusable(item)) {
139+
if (this.listBehavior.isFocusable(item)) {
183140
if (!firstItem) {
184141
firstItem = item;
185142
}
@@ -208,14 +165,6 @@ export class RadioGroupPattern<V> {
208165
return violations;
209166
}
210167

211-
/** Safely performs a navigation operation and updates selection if needed. */
212-
private _navigate(opts: SelectOptions = {}, operation: () => boolean) {
213-
const moved = operation();
214-
if (moved && opts.selectOne) {
215-
this.selection.selectOne();
216-
}
217-
}
218-
219168
/** Finds the RadioButtonPattern associated with a pointer event target. */
220169
private _getItem(e: PointerEvent): RadioButtonPattern<V> | undefined {
221170
if (!(e.target instanceof HTMLElement)) {

0 commit comments

Comments
 (0)