Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 56 additions & 56 deletions src/cdk-experimental/radio-group/radio-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ import {
model,
signal,
WritableSignal,
OnDestroy,
} from '@angular/core';
import {RadioButtonPattern, RadioGroupPattern} from '../ui-patterns';
import {
RadioButtonPattern,
RadioGroupInputs,
RadioGroupPattern,
ToolbarRadioGroupInputs,
ToolbarRadioGroupPattern,
} from '../ui-patterns';
import {Directionality} from '@angular/cdk/bidi';
import {_IdGenerator} from '@angular/cdk/a11y';
import {CdkToolbar} from '../toolbar';
import {CdkToolbarWidgetGroup} from '@angular/cdk-experimental/toolbar';

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

Expand Down Expand Up @@ -91,43 +96,49 @@ export function mapSignal<T, V>(
'(pointerdown)': 'pattern.onPointerdown($event)',
'(focusin)': 'onFocus()',
},
hostDirectives: [
{
directive: CdkToolbarWidgetGroup,
inputs: ['disabled'],
},
],
})
export class CdkRadioGroup<V> {
/** A reference to the radio group element. */
private readonly _elementRef = inject(ElementRef);

/** A reference to the CdkToolbarWidgetGroup, if the radio group is in a toolbar. */
private readonly _cdkToolbarWidgetGroup = inject(CdkToolbarWidgetGroup);

/** Whether the radio group is inside of a CdkToolbar. */
private readonly _hasToolbar = computed(() => !!this._cdkToolbarWidgetGroup.toolbar());

/** The CdkRadioButtons nested inside of the CdkRadioGroup. */
private readonly _cdkRadioButtons = contentChildren(CdkRadioButton, {descendants: true});

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

/** A signal wrapper for toolbar. */
toolbar = inject(CdkToolbar, {optional: true});

/** Toolbar pattern if applicable */
private readonly _toolbarPattern = computed(() => this.toolbar?.pattern);

/** The RadioButton UIPatterns of the child CdkRadioButtons. */
protected items = computed(() => this._cdkRadioButtons().map(radio => radio.pattern));

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

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

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

/** Whether the radio group is disabled. */
disabled = input(false, {transform: booleanAttribute});
readonly disabled = input(false, {transform: booleanAttribute});

/** Whether the radio group is readonly. */
readonly = input(false, {transform: booleanAttribute});
readonly readonly = input(false, {transform: booleanAttribute});

/** The value of the currently selected radio button. */
value = model<V | null>(null);
readonly value = model<V | null>(null);

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

/** The RadioGroup UIPattern. */
pattern: RadioGroupPattern<V> = new RadioGroupPattern<V>({
...this,
items: this.items,
value: this._value,
activeItem: signal(undefined),
textDirection: this.textDirection,
toolbar: this._toolbarPattern,
element: () => this._elementRef.nativeElement,
focusMode: this._toolbarPattern()?.inputs.focusMode ?? this.focusMode,
skipDisabled: this._toolbarPattern()?.inputs.skipDisabled ?? this.skipDisabled,
});
readonly pattern: RadioGroupPattern<V>;

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

constructor() {
const inputs: RadioGroupInputs<V> | ToolbarRadioGroupInputs<V> = {
...this,
items: this.items,
value: this._value,
activeItem: signal(undefined),
textDirection: this.textDirection,
element: () => this._elementRef.nativeElement,
getItem: e => {
if (!(e.target instanceof HTMLElement)) {
return undefined;
}
const element = e.target.closest('[role="radio"]');
return this.items().find(i => i.element() === element);
},
toolbar: this._cdkToolbarWidgetGroup.toolbar,
};

this.pattern = this._hasToolbar()
? new ToolbarRadioGroupPattern<V>(inputs as ToolbarRadioGroupInputs<V>)
: new RadioGroupPattern<V>(inputs as RadioGroupInputs<V>);

if (this._hasToolbar()) {
this._cdkToolbarWidgetGroup.controls.set(this.pattern as ToolbarRadioGroupPattern<V>);
}

afterRenderEffect(() => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
const violations = this.pattern.validate();
Expand All @@ -162,35 +188,15 @@ export class CdkRadioGroup<V> {
});

afterRenderEffect(() => {
if (!this._hasFocused() && !this.toolbar) {
if (!this._hasFocused() && !this._hasToolbar()) {
this.pattern.setDefaultState();
}
});

// TODO: Refactor to be handled within list behavior
afterRenderEffect(() => {
if (this.toolbar) {
const radioButtons = this._cdkRadioButtons();
// If the group is disabled and the toolbar is set to skip disabled items,
// the radio buttons should not be part of the toolbar's navigation.
if (this.disabled() && this.toolbar.skipDisabled()) {
radioButtons.forEach(radio => this.toolbar!.unregister(radio));
} else {
radioButtons.forEach(radio => this.toolbar!.register(radio));
}
}
});
}

onFocus() {
this._hasFocused.set(true);
}

toolbarButtonUnregister(radio: CdkRadioButton<V>) {
if (this.toolbar) {
this.toolbar.unregister(radio);
}
}
}

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

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

/** A unique identifier for the radio button. */
protected id = computed(() => this._generatedId);
readonly id = computed(() => this._generatedId);

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

/** The parent RadioGroup UIPattern. */
protected group = computed(() => this._cdkRadioGroup.pattern);
readonly group = computed(() => this._cdkRadioGroup.pattern);

/** A reference to the radio button element to be focused on navigation. */
element = computed(() => this._elementRef.nativeElement);
Expand All @@ -240,10 +246,4 @@ export class CdkRadioButton<V> implements OnDestroy {
group: this.group,
element: this.element,
});

ngOnDestroy() {
if (this._cdkRadioGroup.toolbar) {
this._cdkRadioGroup.toolbarButtonUnregister(this);
}
}
}
21 changes: 20 additions & 1 deletion src/cdk-experimental/toolbar/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("//tools:defaults.bzl", "ng_project")
load("//tools:defaults.bzl", "ng_project", "ng_web_test_suite", "ts_project")

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

Expand All @@ -15,3 +15,22 @@ ng_project(
"//src/cdk/bidi",
],
)

ts_project(
name = "unit_test_sources",
testonly = True,
srcs = [
"toolbar.spec.ts",
],
deps = [
":toolbar",
"//:node_modules/@angular/core",
"//:node_modules/@angular/platform-browser",
"//src/cdk/testing/private",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_test_sources"],
)
2 changes: 1 addition & 1 deletion src/cdk-experimental/toolbar/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
* found in the LICENSE file at https://angular.dev/license
*/

export {CdkToolbar, CdkToolbarWidget} from './toolbar';
export {CdkToolbar, CdkToolbarWidget, CdkToolbarWidgetGroup} from './toolbar';
Loading
Loading