diff --git a/.ng-dev/commit-message.mts b/.ng-dev/commit-message.mts index a99688ee822b..c868f0e0b1d4 100644 --- a/.ng-dev/commit-message.mts +++ b/.ng-dev/commit-message.mts @@ -14,7 +14,6 @@ export const commitMessage: CommitMessageConfig = { 'aria/grid', 'aria/listbox', 'aria/menu', - 'aria/radio-group', 'aria/tabs', 'aria/toolbar', 'aria/tree', diff --git a/src/aria/BUILD.bazel b/src/aria/BUILD.bazel index 77857eb7dddc..2c73d0293de5 100644 --- a/src/aria/BUILD.bazel +++ b/src/aria/BUILD.bazel @@ -47,7 +47,6 @@ copy_to_directory( "//src/aria/grid:json_api", "//src/aria/listbox:json_api", "//src/aria/menu:json_api", - "//src/aria/radio-group:json_api", "//src/aria/tabs:json_api", "//src/aria/toolbar:json_api", "//src/aria/tree:json_api", diff --git a/src/aria/config.bzl b/src/aria/config.bzl index 1ff1c24c2e0e..2994e7270056 100644 --- a/src/aria/config.bzl +++ b/src/aria/config.bzl @@ -6,7 +6,6 @@ ARIA_ENTRYPOINTS = [ "grid", "listbox", "menu", - "radio-group", "tabs", "toolbar", "tree", diff --git a/src/aria/private/BUILD.bazel b/src/aria/private/BUILD.bazel index a363b5d5b2ed..de861d17480c 100644 --- a/src/aria/private/BUILD.bazel +++ b/src/aria/private/BUILD.bazel @@ -16,7 +16,6 @@ ts_project( "//src/aria/private/grid", "//src/aria/private/listbox", "//src/aria/private/menu", - "//src/aria/private/radio-group", "//src/aria/private/tabs", "//src/aria/private/toolbar", "//src/aria/private/tree", diff --git a/src/aria/private/public-api.ts b/src/aria/private/public-api.ts index 90367fef69f0..9fb82d5c81ad 100644 --- a/src/aria/private/public-api.ts +++ b/src/aria/private/public-api.ts @@ -11,9 +11,6 @@ export * from './listbox/listbox'; export * from './listbox/option'; export * from './listbox/combobox-listbox'; export * from './menu/menu'; -export * from './radio-group/radio-group'; -export * from './radio-group/radio-button'; -export * from './radio-group/toolbar-radio-group'; export * from './behaviors/signal-like/signal-like'; export * from './tabs/tabs'; export * from './toolbar/toolbar'; diff --git a/src/aria/private/radio-group/BUILD.bazel b/src/aria/private/radio-group/BUILD.bazel deleted file mode 100644 index 00a75ece05b2..000000000000 --- a/src/aria/private/radio-group/BUILD.bazel +++ /dev/null @@ -1,37 +0,0 @@ -load("//tools:defaults.bzl", "ng_project", "ng_web_test_suite", "ts_project") - -package(default_visibility = ["//visibility:public"]) - -ts_project( - name = "radio-group", - srcs = [ - "radio-button.ts", - "radio-group.ts", - "toolbar-radio-group.ts", - ], - deps = [ - "//:node_modules/@angular/core", - "//src/aria/private/behaviors/event-manager", - "//src/aria/private/behaviors/list", - "//src/aria/private/behaviors/signal-like", - "//src/aria/private/toolbar", - ], -) - -ng_project( - name = "unit_test_sources", - testonly = True, - srcs = glob(["**/*.spec.ts"]), - deps = [ - ":radio-group", - "//:node_modules/@angular/core", - "//src/aria/private/toolbar", - "//src/cdk/keycodes", - "//src/cdk/testing/private", - ], -) - -ng_web_test_suite( - name = "unit_tests", - deps = [":unit_test_sources"], -) diff --git a/src/aria/private/radio-group/radio-button.ts b/src/aria/private/radio-group/radio-button.ts deleted file mode 100644 index 5c4258301057..000000000000 --- a/src/aria/private/radio-group/radio-button.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {computed} from '@angular/core'; -import {SignalLike} from '../behaviors/signal-like/signal-like'; -import {ListItem} from '../behaviors/list/list'; -import type {RadioGroupPattern} from './radio-group'; - -/** Represents the required inputs for a radio button in a radio group. */ -export interface RadioButtonInputs - extends Omit, 'searchTerm' | 'index' | 'selectable'> { - /** A reference to the parent radio group. */ - group: SignalLike | undefined>; -} - -/** Represents a radio button within a radio group. */ -export class RadioButtonPattern { - /** A unique identifier for the radio button. */ - readonly id: SignalLike; - - /** The value associated with the radio button. */ - readonly value: SignalLike; - - /** The position of the radio button within the group. */ - readonly index: SignalLike = computed( - () => this.group()?.listBehavior.inputs.items().indexOf(this) ?? -1, - ); - - /** Whether the radio button is currently the active one (focused). */ - readonly active = computed(() => this.group()?.listBehavior.inputs.activeItem() === this); - - /** Whether the radio button is selected. */ - readonly selected: SignalLike = computed( - () => !!this.group()?.listBehavior.inputs.value().includes(this.value()), - ); - - /** Whether the radio button is selectable. */ - readonly selectable = () => true; - - /** Whether the radio button is disabled. */ - readonly disabled: SignalLike; - - /** A reference to the parent radio group. */ - readonly group: SignalLike | undefined>; - - /** The tabindex of the radio button. */ - readonly tabindex = computed(() => this.group()?.listBehavior.getItemTabindex(this)); - - /** The HTML element associated with the radio button. */ - readonly element: SignalLike; - - /** The search term for typeahead. */ - readonly searchTerm = () => ''; // Radio groups do not support typeahead. - - constructor(readonly inputs: RadioButtonInputs) { - this.id = inputs.id; - this.value = inputs.value; - this.group = inputs.group; - this.element = inputs.element; - this.disabled = inputs.disabled; - } -} diff --git a/src/aria/private/radio-group/radio-group.ts b/src/aria/private/radio-group/radio-group.ts deleted file mode 100644 index 31fe4802cc72..000000000000 --- a/src/aria/private/radio-group/radio-group.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {computed, signal} from '@angular/core'; -import {KeyboardEventManager, PointerEventManager} from '../behaviors/event-manager'; -import {List, ListInputs} from '../behaviors/list/list'; -import {SignalLike} from '../behaviors/signal-like/signal-like'; -import {RadioButtonPattern} from './radio-button'; - -/** Represents the required inputs for a radio group. */ -export type RadioGroupInputs = Omit< - ListInputs, V>, - 'multi' | 'selectionMode' | 'wrap' | 'typeaheadDelay' -> & { - /** Whether the radio group is disabled. */ - disabled: SignalLike; - - /** Whether the radio group is readonly. */ - readonly: SignalLike; - - /** A function that returns the radio button associated with a given element. */ - getItem: (e: PointerEvent) => RadioButtonPattern | undefined; -}; - -/** Controls the state of a radio group. */ -export class RadioGroupPattern { - /** The list behavior for the radio group. */ - readonly listBehavior: List, V>; - - /** Whether the radio group is vertically or horizontally oriented. */ - readonly orientation: SignalLike<'vertical' | 'horizontal'>; - - /** Whether focus should wrap when navigating. */ - readonly wrap = signal(false); - - /** The selection strategy used by the radio group. */ - readonly selectionMode = signal<'follow' | 'explicit'>('follow'); - - /** Whether the radio group is disabled. */ - readonly disabled = computed(() => this.inputs.disabled() || this.listBehavior.disabled()); - - /** The currently selected radio button. */ - readonly selectedItem = computed(() => this.listBehavior.selectionBehavior.selectedItems()[0]); - - /** Whether the radio group is readonly. */ - readonly readonly = computed(() => this.selectedItem()?.disabled() || this.inputs.readonly()); - - /** The tabindex of the radio group. */ - readonly tabindex = computed(() => this.listBehavior.tabindex()); - - /** The id of the current active radio button (if using activedescendant). */ - readonly activedescendant = computed(() => this.listBehavior.activedescendant()); - - /** The key used to navigate to the previous radio button. */ - private readonly _prevKey = computed(() => { - if (this.inputs.orientation() === 'vertical') { - return 'ArrowUp'; - } - return this.inputs.textDirection() === 'rtl' ? 'ArrowRight' : 'ArrowLeft'; - }); - - /** The key used to navigate to the next radio button. */ - private readonly _nextKey = computed(() => { - if (this.inputs.orientation() === 'vertical') { - return 'ArrowDown'; - } - return this.inputs.textDirection() === 'rtl' ? 'ArrowLeft' : 'ArrowRight'; - }); - - /** The keydown event manager for the radio group. */ - readonly keydown = computed(() => { - const manager = new KeyboardEventManager(); - - // Readonly mode allows navigation but not selection changes. - if (this.readonly()) { - return manager - .on(this._prevKey, () => this.listBehavior.prev()) - .on(this._nextKey, () => this.listBehavior.next()) - .on('Home', () => this.listBehavior.first()) - .on('End', () => this.listBehavior.last()); - } - - // Default behavior: navigate and select on arrow keys, home, end. - // Space/Enter also select the focused item. - return manager - .on(this._prevKey, () => this.listBehavior.prev({selectOne: true})) - .on(this._nextKey, () => this.listBehavior.next({selectOne: true})) - .on('Home', () => this.listBehavior.first({selectOne: true})) - .on('End', () => this.listBehavior.last({selectOne: true})) - .on(' ', () => this.listBehavior.selectOne()) - .on('Enter', () => this.listBehavior.selectOne()); - }); - - /** The pointerdown event manager for the radio group. */ - readonly pointerdown = computed(() => { - const manager = new PointerEventManager(); - - if (this.readonly()) { - // Navigate focus only in readonly mode. - return manager.on(e => this.listBehavior.goto(this.inputs.getItem(e)!)); - } - - // Default behavior: navigate and select on click. - return manager.on(e => this.listBehavior.goto(this.inputs.getItem(e)!, {selectOne: true})); - }); - - constructor(readonly inputs: RadioGroupInputs) { - this.orientation = inputs.orientation; - this.listBehavior = new List({ - ...inputs, - wrap: this.wrap, - selectionMode: this.selectionMode, - multi: () => false, - typeaheadDelay: () => 0, // Radio groups do not support typeahead. - }); - } - - /** Handles keydown events for the radio group. */ - onKeydown(event: KeyboardEvent) { - if (!this.disabled()) { - this.keydown().handle(event); - } - } - - /** Handles pointerdown events for the radio group. */ - onPointerdown(event: PointerEvent) { - if (!this.disabled()) { - this.pointerdown().handle(event); - } - } - - /** - * Sets the radio group to its default initial state. - * - * Sets the active index to the selected radio button if one exists and is focusable. - * Otherwise, sets the active index to the first focusable radio button. - */ - setDefaultState() { - let firstItem: RadioButtonPattern | null = null; - - for (const item of this.inputs.items()) { - if (this.listBehavior.isFocusable(item)) { - if (!firstItem) { - firstItem = item; - } - if (item.selected()) { - this.inputs.activeItem.set(item); - return; - } - } - } - - if (firstItem) { - this.inputs.activeItem.set(firstItem); - } - } - - /** Validates the state of the radio group and returns a list of accessibility violations. */ - validate(): string[] { - const violations: string[] = []; - - if (this.selectedItem()?.disabled() && !this.inputs.softDisabled()) { - violations.push( - "Accessibility Violation: The selected radio button is disabled while 'softDisabled' is false, making the selection unreachable via keyboard.", - ); - } - - return violations; - } -} diff --git a/src/aria/private/radio-group/radio.spec.ts b/src/aria/private/radio-group/radio.spec.ts deleted file mode 100644 index 1292e71126e3..000000000000 --- a/src/aria/private/radio-group/radio.spec.ts +++ /dev/null @@ -1,308 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {signal, WritableSignal} from '@angular/core'; -import {RadioGroupInputs, RadioGroupPattern} from './radio-group'; -import {RadioButtonPattern} from './radio-button'; -import {createKeyboardEvent} from '@angular/cdk/testing/private'; -import {ModifierKeys} from '@angular/cdk/testing'; - -type TestInputs = RadioGroupInputs; -type TestRadio = RadioButtonPattern & { - disabled: WritableSignal; -}; -type TestRadioGroup = RadioGroupPattern; - -const up = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 38, 'ArrowUp', mods); -const down = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 40, 'ArrowDown', mods); -const left = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 37, 'ArrowLeft', mods); -const right = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 39, 'ArrowRight', mods); -const home = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 36, 'Home', mods); -const end = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 35, 'End', mods); -const space = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 32, ' ', mods); -const enter = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 13, 'Enter', mods); - -describe('RadioGroup Pattern', () => { - function getRadioGroup(inputs: Partial & Pick) { - return new RadioGroupPattern({ - items: inputs.items, - value: inputs.value ?? signal([]), - activeItem: signal(undefined), - element: signal(document.createElement('div')), - readonly: inputs.readonly ?? signal(false), - disabled: inputs.disabled ?? signal(false), - softDisabled: inputs.softDisabled ?? signal(false), - focusMode: inputs.focusMode ?? signal('roving'), - textDirection: inputs.textDirection ?? signal('ltr'), - orientation: inputs.orientation ?? signal('vertical'), - getItem: e => inputs.items().find(i => i.element() === e.target), - }); - } - - function getRadios(radioGroup: TestRadioGroup, values: string[]): TestRadio[] { - return values.map((value, index) => { - const element = document.createElement('div'); - element.role = 'radio'; - return new RadioButtonPattern({ - value: signal(value), - id: signal(`radio-${index}`), - disabled: signal(false), - group: signal(radioGroup), - element: signal(element), - }); - }) as TestRadio[]; - } - - function getPatterns(values: string[], inputs: Partial = {}) { - const radioButtons = signal([]); - const radioGroup = getRadioGroup({...inputs, items: radioButtons}); - radioButtons.set(getRadios(radioGroup, values)); - radioGroup.inputs.activeItem.set(radioButtons()[0]); - return {radioGroup, radioButtons: radioButtons()}; - } - - function getDefaultPatterns(inputs: Partial = {}) { - return getPatterns(['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'], inputs); - } - - describe('Keyboard Navigation', () => { - it('should navigate next on ArrowDown', () => { - const {radioGroup, radioButtons} = getDefaultPatterns(); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - radioGroup.onKeydown(down()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - }); - - it('should navigate prev on ArrowUp', () => { - const {radioGroup, radioButtons} = getDefaultPatterns(); - radioGroup.inputs.activeItem.set(radioButtons[1]); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - radioGroup.onKeydown(up()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should navigate next on ArrowRight (horizontal)', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({orientation: signal('horizontal')}); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - radioGroup.onKeydown(right()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - }); - - it('should navigate prev on ArrowLeft (horizontal)', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({orientation: signal('horizontal')}); - radioGroup.inputs.activeItem.set(radioButtons[1]); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - radioGroup.onKeydown(left()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should navigate next on ArrowLeft (horizontal & rtl)', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({ - textDirection: signal('rtl'), - orientation: signal('horizontal'), - }); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - radioGroup.onKeydown(left()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - }); - - it('should navigate prev on ArrowRight (horizontal & rtl)', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({ - textDirection: signal('rtl'), - orientation: signal('horizontal'), - }); - radioGroup.inputs.activeItem.set(radioButtons[1]); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - radioGroup.onKeydown(right()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should navigate to the first radio on Home', () => { - const {radioGroup, radioButtons} = getDefaultPatterns(); - radioGroup.inputs.activeItem.set(radioButtons[4]); - - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[4]); - radioGroup.onKeydown(home()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should navigate to the last radio on End', () => { - const {radioGroup, radioButtons} = getDefaultPatterns(); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - radioGroup.onKeydown(end()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[4]); - }); - - it('should skip disabled radios when softDisabled is false', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({softDisabled: signal(false)}); - radioButtons[1].disabled.set(true); - radioGroup.onKeydown(down()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[2]); - radioGroup.onKeydown(up()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should not skip disabled radios when softDisabled is true', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({softDisabled: signal(true)}); - radioButtons[1].disabled.set(true); - radioGroup.onKeydown(down()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - radioGroup.onKeydown(up()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should be able to navigate in readonly mode', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({readonly: signal(true)}); - radioGroup.onKeydown(down()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - radioGroup.onKeydown(up()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - radioGroup.onKeydown(end()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[4]); - radioGroup.onKeydown(home()); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - }); - - describe('Keyboard Selection', () => { - let radioGroup: TestRadioGroup; - - beforeEach(() => { - radioGroup = getDefaultPatterns({value: signal([])}).radioGroup; - }); - - it('should select a radio on Space', () => { - radioGroup.onKeydown(space()); - expect(radioGroup.inputs.value()).toEqual(['Apple']); - }); - - it('should select a radio on Enter', () => { - radioGroup.onKeydown(enter()); - expect(radioGroup.inputs.value()).toEqual(['Apple']); - }); - - it('should select the focused radio on navigation (implicit selection)', () => { - radioGroup.onKeydown(down()); - expect(radioGroup.inputs.value()).toEqual(['Banana']); - radioGroup.onKeydown(up()); - expect(radioGroup.inputs.value()).toEqual(['Apple']); - radioGroup.onKeydown(end()); - expect(radioGroup.inputs.value()).toEqual(['Elderberry']); - radioGroup.onKeydown(home()); - expect(radioGroup.inputs.value()).toEqual(['Apple']); - }); - - it('should not be able to change selection when in readonly mode', () => { - const readonly = radioGroup.inputs.readonly as WritableSignal; - readonly.set(true); - radioGroup.onKeydown(space()); - expect(radioGroup.inputs.value()).toEqual([]); - - radioGroup.onKeydown(down()); // Navigation still works - expect(radioGroup.inputs.activeItem()).toBe(radioGroup.inputs.items()[1]); - expect(radioGroup.inputs.value()).toEqual([]); // Selection doesn't change - - radioGroup.onKeydown(enter()); - expect(radioGroup.inputs.value()).toEqual([]); - }); - - it('should not select a disabled radio via keyboard', () => { - const {radioGroup, radioButtons} = getPatterns(['A', 'B', 'C'], { - softDisabled: signal(true), - }); - radioButtons[1].disabled.set(true); - - radioGroup.onKeydown(down()); // Focus B (disabled) - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - expect(radioGroup.inputs.value()).toEqual([]); // Should not select B - - radioGroup.onKeydown(space()); // Try selecting B with space - expect(radioGroup.inputs.value()).toEqual([]); - - radioGroup.onKeydown(enter()); // Try selecting B with enter - expect(radioGroup.inputs.value()).toEqual([]); - - radioGroup.onKeydown(down()); // Focus C - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[2]); - expect(radioGroup.inputs.value()).toEqual(['C']); // Selects C on navigation - }); - }); - - describe('Pointer Events', () => { - function click(radios: TestRadio[], index: number) { - return { - target: radios[index].element(), - } as unknown as PointerEvent; - } - - it('should select a radio on click', () => { - const {radioGroup, radioButtons} = getDefaultPatterns(); - radioGroup.onPointerdown(click(radioButtons, 1)); - expect(radioGroup.inputs.value()).toEqual(['Banana']); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - }); - - it('should not select a disabled radio on click', () => { - const {radioGroup, radioButtons} = getDefaultPatterns(); - radioButtons[1].disabled.set(true); - radioGroup.onPointerdown(click(radioButtons, 1)); - expect(radioGroup.inputs.value()).toEqual([]); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); // Active index shouldn't change - }); - - it('should only update active index when readonly', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({readonly: signal(true)}); - radioGroup.onPointerdown(click(radioButtons, 1)); - expect(radioGroup.inputs.value()).toEqual([]); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); // Active index should update - }); - }); - - describe('#setDefaultState', () => { - it('should set the active index to the first radio', () => { - const {radioGroup, radioButtons} = getDefaultPatterns(); - radioGroup.setDefaultState(); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should set the active index to the first focusable radio', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({softDisabled: signal(false)}); - radioButtons[0].disabled.set(true); - radioGroup.setDefaultState(); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - }); - - it('should set the active index to the selected radio', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({value: signal(['Cherry'])}); - radioGroup.setDefaultState(); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[2]); - }); - - it('should set the active index to the first focusable radio if selected is disabled', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({ - value: signal(['Cherry']), - softDisabled: signal(false), - }); - radioButtons[2].disabled.set(true); // Disable Cherry - radioGroup.setDefaultState(); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); // Defaults to first focusable - }); - }); - - describe('validate', () => { - it('should report a violation if the selected item is disabled and softDisabled is false', () => { - const {radioGroup, radioButtons} = getDefaultPatterns({ - value: signal(['Banana']), - softDisabled: signal(false), - }); - radioButtons[1].disabled.set(true); // Disable the selected item. - const violations = radioGroup.validate(); - expect(violations.length).toBe(1); - }); - }); -}); diff --git a/src/aria/private/radio-group/toolbar-radio-group.spec.ts b/src/aria/private/radio-group/toolbar-radio-group.spec.ts deleted file mode 100644 index 52566c97215e..000000000000 --- a/src/aria/private/radio-group/toolbar-radio-group.spec.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {signal, WritableSignal} from '@angular/core'; -import {ToolbarRadioGroupInputs, ToolbarRadioGroupPattern} from './toolbar-radio-group'; -import {RadioButtonPattern} from './radio-button'; -import {ToolbarPattern} from '../toolbar/toolbar'; -import {createKeyboardEvent} from '@angular/cdk/testing/private'; -import {ModifierKeys} from '@angular/cdk/testing'; - -type TestInputs = ToolbarRadioGroupInputs; -type TestRadio = RadioButtonPattern & { - disabled: WritableSignal; -}; -type TestRadioGroup = ToolbarRadioGroupPattern; - -const down = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 40, 'ArrowDown', mods); -const space = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 32, ' ', mods); -const enter = (mods?: ModifierKeys) => createKeyboardEvent('keydown', 13, 'Enter', mods); - -describe('ToolbarRadioGroup Pattern', () => { - function getToolbarRadioGroup(inputs: Partial & Pick) { - return new ToolbarRadioGroupPattern({ - items: inputs.items, - value: inputs.value ?? signal([]), - activeItem: signal(undefined), - element: signal(document.createElement('div')), - readonly: inputs.readonly ?? signal(false), - disabled: inputs.disabled ?? signal(false), - softDisabled: inputs.softDisabled ?? signal(false), - focusMode: inputs.focusMode ?? signal('roving'), - textDirection: inputs.textDirection ?? signal('ltr'), - orientation: inputs.orientation ?? signal('vertical'), - toolbar: inputs.toolbar ?? signal(undefined), - getItem: e => inputs.items().find(i => i.element() === e.target), - }); - } - - function getRadios(radioGroup: TestRadioGroup, values: string[]): TestRadio[] { - return values.map((value, index) => { - const element = document.createElement('div'); - element.role = 'radio'; - return new RadioButtonPattern({ - value: signal(value), - id: signal(`radio-${index}`), - disabled: signal(false), - group: signal(radioGroup), - element: signal(element), - }); - }) as TestRadio[]; - } - - function getPatterns(values: string[], inputs: Partial = {}) { - const radioButtons = signal([]); - const radioGroup = getToolbarRadioGroup({...inputs, items: radioButtons}); - radioButtons.set(getRadios(radioGroup, values)); - radioGroup.inputs.activeItem.set(radioButtons()[0]); - return {radioGroup, radioButtons: radioButtons()}; - } - - function getDefaultPatterns(inputs: Partial = {}) { - return getPatterns(['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'], inputs); - } - - let radioGroup: TestRadioGroup; - let radioButtons: TestRadio[]; - let toolbar: ToolbarPattern; - - beforeEach(() => { - toolbar = new ToolbarPattern({ - items: signal([]), - activeItem: signal(undefined), - element: signal(document.createElement('div')), - orientation: signal('horizontal'), - textDirection: signal('ltr'), - disabled: signal(false), - softDisabled: signal(false), - wrap: signal(false), - getItem: (e: Element) => undefined, - }); - const patterns = getDefaultPatterns({ - toolbar: signal(toolbar), - }); - radioButtons = patterns.radioButtons; - radioGroup = patterns.radioGroup; - }); - - it('should ignore keyboard navigation when within a toolbar', () => { - radioGroup.inputs.activeItem.set(radioButtons[0]); - const initialActive = radioGroup.inputs.activeItem(); - radioGroup.onKeydown(down()); - expect(radioGroup.inputs.activeItem()).toBe(initialActive); - }); - - it('should ignore keyboard selection when within a toolbar', () => { - expect(radioGroup.inputs.value()).toEqual([]); - radioGroup.onKeydown(space()); - expect(radioGroup.inputs.value()).toEqual([]); - radioGroup.onKeydown(enter()); - expect(radioGroup.inputs.value()).toEqual([]); - }); - - it('should ignore pointer events when within a toolbar', () => { - radioGroup.inputs.activeItem.set(radioButtons[0]); - const initialActive = radioGroup.inputs.activeItem(); - expect(radioGroup.inputs.value()).toEqual([]); - - const clickEvent = { - target: radioButtons[1].element(), - } as unknown as PointerEvent; - radioGroup.onPointerdown(clickEvent); - - expect(radioGroup.inputs.activeItem()).toBe(initialActive); - expect(radioGroup.inputs.value()).toEqual([]); - }); - - describe('Toolbar Widget Group controls', () => { - beforeEach(() => { - radioGroup.inputs.activeItem.set(radioButtons[0]); - }); - - it('should correctly report when on the first item', () => { - radioGroup.inputs.activeItem.set(radioButtons[0]); - expect(radioGroup.isOnFirstItem()).toBe(true); - radioGroup.inputs.activeItem.set(radioButtons[1]); - expect(radioGroup.isOnFirstItem()).toBe(false); - }); - - it('should correctly report when on the last item', () => { - radioGroup.inputs.activeItem.set(radioButtons[4]); - expect(radioGroup.isOnLastItem()).toBe(true); - radioGroup.inputs.activeItem.set(radioButtons[3]); - expect(radioGroup.isOnLastItem()).toBe(false); - }); - - it('should handle "next" control', () => { - radioGroup.next(false); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[1]); - }); - - it('should handle "prev" control', () => { - radioGroup.inputs.activeItem.set(radioButtons[1]); - radioGroup.prev(false); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should handle "first" control', () => { - radioGroup.first(); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should handle "last" control', () => { - radioGroup.last(); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[4]); - }); - - it('should handle "unfocus" control by clearing active item', () => { - radioGroup.unfocus(); - expect(radioGroup.inputs.activeItem()).toBe(undefined); - }); - - it('should handle "trigger" control to select an item', () => { - expect(radioGroup.inputs.value()).toEqual([]); - radioGroup.trigger(); - expect(radioGroup.inputs.value()).toEqual(['Apple']); - }); - - it('should not "trigger" selection when readonly', () => { - (radioGroup.inputs.readonly as WritableSignal).set(true); - expect(radioGroup.inputs.value()).toEqual([]); - radioGroup.trigger(); - expect(radioGroup.inputs.value()).toEqual([]); - }); - - it('should handle "goto" control', () => { - const event = {target: radioButtons[2].element()} as unknown as PointerEvent; - radioGroup.goto(event); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[2]); - expect(radioGroup.inputs.value()).toEqual(['Cherry']); - }); - - it('should handle "goto" control in readonly mode (no selection)', () => { - (radioGroup.inputs.readonly as WritableSignal).set(true); - const event = {target: radioButtons[2].element()} as unknown as PointerEvent; - radioGroup.goto(event); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[2]); - expect(radioGroup.inputs.value()).toEqual([]); - }); - - it('should handle "setDefaultState" control', () => { - radioGroup.inputs.activeItem.set(undefined); - radioGroup.setDefaultState(); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should wrap on "next" with wrap', () => { - radioGroup.inputs.activeItem.set(radioButtons[4]); - radioGroup.next(true); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[0]); - }); - - it('should wrap on "prev" with wrap', () => { - radioGroup.prev(true); - expect(radioGroup.inputs.activeItem()).toBe(radioButtons[4]); - }); - }); -}); diff --git a/src/aria/private/radio-group/toolbar-radio-group.ts b/src/aria/private/radio-group/toolbar-radio-group.ts deleted file mode 100644 index c49adf79ec65..000000000000 --- a/src/aria/private/radio-group/toolbar-radio-group.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {SignalLike} from '../behaviors/signal-like/signal-like'; -import {RadioGroupInputs, RadioGroupPattern} from './radio-group'; -import type {ToolbarPattern} from '../toolbar/toolbar'; -import type {ToolbarWidgetGroupControls} from '../toolbar/toolbar-widget-group'; - -/** Represents the required inputs for a toolbar controlled radio group. */ -export type ToolbarRadioGroupInputs = RadioGroupInputs & { - /** The toolbar controlling the radio group. */ - toolbar: SignalLike | undefined>; -}; - -/** Controls the state of a radio group in a toolbar. */ -export class ToolbarRadioGroupPattern - extends RadioGroupPattern - implements ToolbarWidgetGroupControls -{ - constructor(override readonly inputs: ToolbarRadioGroupInputs) { - if (!!inputs.toolbar()) { - inputs.orientation = inputs.toolbar()!.orientation; - inputs.softDisabled = inputs.toolbar()!.softDisabled; - } - - super(inputs); - } - - /** Noop. The toolbar handles keydown events. */ - override onKeydown(_: KeyboardEvent): void {} - - /** Noop. The toolbar handles pointerdown events. */ - override onPointerdown(_: PointerEvent): void {} - - /** Whether the radio group is currently on the first item. */ - isOnFirstItem() { - return this.listBehavior.navigationBehavior.peekPrev() === undefined; - } - - /** Whether the radio group is currently on the last item. */ - isOnLastItem() { - return this.listBehavior.navigationBehavior.peekNext() === undefined; - } - - /** Navigates to the next radio button in the group. */ - next(wrap: boolean) { - this.wrap.set(wrap); - this.listBehavior.next(); - this.wrap.set(false); - } - - /** Navigates to the previous radio button in the group. */ - prev(wrap: boolean) { - this.wrap.set(wrap); - this.listBehavior.prev(); - this.wrap.set(false); - } - - /** Navigates to the first radio button in the group. */ - first() { - this.listBehavior.first(); - } - - /** Navigates to the last radio button in the group. */ - last() { - this.listBehavior.last(); - } - - /** Removes focus from the radio group. */ - unfocus() { - this.inputs.activeItem.set(undefined); - } - - /** Triggers the action of the currently active radio button in the group. */ - trigger() { - if (this.readonly()) return; - this.listBehavior.selectOne(); - } - - /** Navigates to the radio button targeted by a pointer event. */ - goto(e: PointerEvent) { - this.listBehavior.goto(this.inputs.getItem(e)!, { - selectOne: !this.readonly(), - }); - } -} diff --git a/src/aria/private/toolbar/BUILD.bazel b/src/aria/private/toolbar/BUILD.bazel index 9c09cf751c64..6c2dd3d6221b 100644 --- a/src/aria/private/toolbar/BUILD.bazel +++ b/src/aria/private/toolbar/BUILD.bazel @@ -25,7 +25,6 @@ ng_project( ":toolbar", "//:node_modules/@angular/core", "//src/aria/private/behaviors/signal-like", - "//src/aria/private/radio-group", "//src/cdk/keycodes", "//src/cdk/testing/private", ], diff --git a/src/aria/radio-group/BUILD.bazel b/src/aria/radio-group/BUILD.bazel deleted file mode 100644 index c12f3a486eb5..000000000000 --- a/src/aria/radio-group/BUILD.bazel +++ /dev/null @@ -1,59 +0,0 @@ -load("//tools:defaults.bzl", "ng_project", "ng_web_test_suite") -load("//tools/adev-api-extraction:extract_api_to_json.bzl", "extract_api_to_json") - -package(default_visibility = ["//visibility:public"]) - -ng_project( - name = "radio-group", - srcs = glob( - ["**/*.ts"], - exclude = ["**/*.spec.ts"], - ), - deps = [ - "//:node_modules/@angular/core", - "//src/aria/private", - "//src/aria/toolbar", - "//src/cdk/a11y", - "//src/cdk/bidi", - ], -) - -ng_project( - name = "unit_test_sources", - testonly = True, - srcs = glob( - ["**/*.spec.ts"], - exclude = ["**/*.e2e.spec.ts"], - ), - deps = [ - ":radio-group", - "//:node_modules/@angular/core", - "//:node_modules/@angular/platform-browser", - "//src/cdk/testing/private", - ], -) - -ng_web_test_suite( - name = "unit_tests", - deps = [":unit_test_sources"], -) - -filegroup( - name = "source-files", - srcs = glob( - ["**/*.ts"], - exclude = ["**/*.spec.ts"], - ), -) - -extract_api_to_json( - name = "json_api", - srcs = [ - ":source-files", - ], - entry_point = ":index.ts", - module_name = "@angular/aria/radio-group", - output_name = "aria-radio-group.json", - private_modules = [""], - repo = "angular/components", -) diff --git a/src/aria/radio-group/index.ts b/src/aria/radio-group/index.ts deleted file mode 100644 index 34aab88aafe3..000000000000 --- a/src/aria/radio-group/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -export {RadioGroup, RadioButton} from './radio-group'; diff --git a/src/aria/radio-group/radio-group.spec.ts b/src/aria/radio-group/radio-group.spec.ts deleted file mode 100644 index 67f37cb36cf0..000000000000 --- a/src/aria/radio-group/radio-group.spec.ts +++ /dev/null @@ -1,570 +0,0 @@ -import {Component, DebugElement, signal} from '@angular/core'; -import {RadioButton, RadioGroup} from './radio-group'; -import {ComponentFixture, TestBed} from '@angular/core/testing'; -import {By} from '@angular/platform-browser'; -import {Direction} from '@angular/cdk/bidi'; -import {provideFakeDirectionality, runAccessibilityChecks} from '@angular/cdk/testing/private'; - -describe('RadioGroup', () => { - let fixture: ComponentFixture; - let radioGroup: DebugElement; - let radioButtons: DebugElement[]; - let radioGroupInstance: RadioGroup; - let radioGroupElement: HTMLElement; - let radioButtonElements: HTMLElement[]; - - const keydown = (key: string) => { - radioGroupElement.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, key})); - fixture.detectChanges(); - }; - - const click = (index: number) => { - radioButtonElements[index].dispatchEvent(new PointerEvent('pointerdown', {bubbles: true})); - fixture.detectChanges(); - }; - - const space = () => keydown(' '); - const enter = () => keydown('Enter'); - const up = () => keydown('ArrowUp'); - const down = () => keydown('ArrowDown'); - const left = () => keydown('ArrowLeft'); - const right = () => keydown('ArrowRight'); - const home = () => keydown('Home'); - const end = () => keydown('End'); - - function setupRadioGroup(opts?: { - orientation?: 'horizontal' | 'vertical'; - disabled?: boolean; - readonly?: boolean; - value?: number | null; - softDisabled?: boolean; - focusMode?: 'roving' | 'activedescendant'; - disabledOptions?: number[]; - options?: TestOption[]; - textDirection?: Direction; - }) { - TestBed.configureTestingModule({ - providers: [provideFakeDirectionality(opts?.textDirection ?? 'ltr')], - }); - - fixture = TestBed.createComponent(RadioGroupExample); - const testComponent = fixture.componentInstance; - - if (opts?.orientation !== undefined) { - testComponent.orientation = opts.orientation; - } - if (opts?.disabled !== undefined) { - testComponent.disabled = opts.disabled; - } - if (opts?.readonly !== undefined) { - testComponent.readonly = opts.readonly; - } - if (opts?.value !== undefined) { - testComponent.value = opts.value; - } - if (opts?.softDisabled !== undefined) { - testComponent.softDisabled = opts.softDisabled; - } - if (opts?.focusMode !== undefined) { - testComponent.focusMode = opts.focusMode; - } - if (opts?.options !== undefined) { - testComponent.options.set(opts.options); - } - if (opts?.disabledOptions !== undefined) { - opts.disabledOptions.forEach(index => { - testComponent.options()[index].disabled = true; - }); - } - - fixture.detectChanges(); - defineTestVariables(fixture); - } - - function setupDefaultRadioGroup() { - TestBed.configureTestingModule({ - providers: [provideFakeDirectionality('ltr')], - }); - - const fixture = TestBed.createComponent(DefaultRadioGroupExample); - fixture.detectChanges(); - defineTestVariables(fixture); - } - - function defineTestVariables(fixture: ComponentFixture) { - radioGroup = fixture.debugElement.query(By.directive(RadioGroup)); - radioButtons = fixture.debugElement.queryAll(By.directive(RadioButton)); - radioGroupInstance = radioGroup.injector.get>(RadioGroup); - radioGroupElement = radioGroup.nativeElement; - radioButtonElements = radioButtons.map(radioButton => radioButton.nativeElement); - } - - afterEach(async () => { - await runAccessibilityChecks(radioGroupElement); - }); - - describe('ARIA attributes and roles', () => { - describe('default configuration', () => { - it('should correctly set the role attribute to "radiogroup"', () => { - setupDefaultRadioGroup(); - expect(radioGroupElement.getAttribute('role')).toBe('radiogroup'); - }); - - it('should correctly set the role attribute to "radio" for the radio buttons', () => { - setupDefaultRadioGroup(); - radioButtonElements.forEach(radioButtonElement => { - expect(radioButtonElement.getAttribute('role')).toBe('radio'); - }); - }); - - it('should set aria-orientation to "vertical"', () => { - setupDefaultRadioGroup(); - expect(radioGroupElement.getAttribute('aria-orientation')).toBe('vertical'); - }); - - it('should set aria-disabled to false', () => { - setupDefaultRadioGroup(); - expect(radioGroupElement.getAttribute('aria-disabled')).toBe('false'); - }); - - it('should set aria-readonly to false', () => { - setupDefaultRadioGroup(); - expect(radioGroupElement.getAttribute('aria-readonly')).toBe('false'); - }); - }); - - describe('custom configuration', () => { - it('should be able to set aria-orientation to "vertical"', () => { - setupRadioGroup({orientation: 'vertical'}); - expect(radioGroupElement.getAttribute('aria-orientation')).toBe('vertical'); - }); - - it('should be able to set aria-disabled to true', () => { - setupRadioGroup({disabled: true}); - expect(radioGroupElement.getAttribute('aria-disabled')).toBe('true'); - }); - - it('should be able to set aria-readonly to true', () => { - setupRadioGroup({readonly: true}); - expect(radioGroupElement.getAttribute('aria-readonly')).toBe('true'); - }); - }); - - describe('roving focus mode', () => { - it('should have tabindex="-1" when focusMode is "roving"', () => { - setupRadioGroup({focusMode: 'roving'}); - expect(radioGroupElement.getAttribute('tabindex')).toBe('-1'); - }); - - it('should set tabindex="0" when disabled', () => { - setupRadioGroup({disabled: true, focusMode: 'roving'}); - expect(radioGroupElement.getAttribute('tabindex')).toBe('0'); - }); - - it('should set initial focus on the selected option', () => { - setupRadioGroup({focusMode: 'roving', value: 3}); - expect(radioButtonElements[3].getAttribute('tabindex')).toBe('0'); - }); - - it('should set initial focus on the first option if none are selected', () => { - setupRadioGroup({focusMode: 'roving'}); - expect(radioButtonElements[0].getAttribute('tabindex')).toBe('0'); - }); - - it('should not have aria-activedescendant when focusMode is "roving"', () => { - setupRadioGroup({focusMode: 'roving'}); - expect(radioGroupElement.getAttribute('aria-activedescendant')).toBeNull(); - }); - }); - - describe('activedescendant focus mode', () => { - it('should have tabindex="0"', () => { - setupRadioGroup({focusMode: 'activedescendant'}); - expect(radioGroupElement.getAttribute('tabindex')).toBe('0'); - }); - - it('should set initial focus on the selected option', () => { - setupRadioGroup({focusMode: 'activedescendant', value: 3}); - expect(radioGroupElement.getAttribute('aria-activedescendant')).toBe( - radioButtonElements[3].id, - ); - }); - - it('should set initial focus on the first option if none are selected', () => { - setupRadioGroup({focusMode: 'activedescendant'}); - expect(radioGroupElement.getAttribute('aria-activedescendant')).toBe( - radioButtonElements[0].id, - ); - }); - }); - }); - - describe('value and selection', () => { - it('should select the radio button corresponding to the value input', () => { - setupRadioGroup(); - radioGroupInstance.value.set(1); - fixture.detectChanges(); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('true'); - }); - - it('should update the value model when the value of a radio group is changed through the ui', () => { - setupRadioGroup(); - click(1); - expect(radioGroupInstance.value()).toBe(1); - }); - - describe('pointer interaction', () => { - it('should update the group value when a radio button is selected via pointer click', () => { - setupRadioGroup(); - click(1); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('true'); - }); - - it('should only allow one radio button to be selected at a time', () => { - setupRadioGroup(); - click(1); - click(2); - expect(radioButtonElements[0].getAttribute('aria-checked')).toBe('false'); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('false'); - expect(radioButtonElements[2].getAttribute('aria-checked')).toBe('true'); - expect(radioButtonElements[3].getAttribute('aria-checked')).toBe('false'); - expect(radioButtonElements[4].getAttribute('aria-checked')).toBe('false'); - }); - - it('should not change the value if the radio group is readonly', () => { - setupRadioGroup({readonly: true}); - click(3); - expect(radioButtonElements[3].getAttribute('aria-checked')).toBe('false'); - }); - - it('should not change the value if the radio group is disabled', () => { - setupRadioGroup({disabled: true}); - click(3); - expect(radioButtonElements[3].getAttribute('aria-checked')).toBe('false'); - }); - - it('should not change the value if a disabled radio button is clicked', () => { - setupRadioGroup({disabledOptions: [2]}); - click(2); - expect(radioButtonElements[2].getAttribute('aria-checked')).toBe('false'); - }); - - it('should not change the value if a radio button is clicked in a readonly group', () => { - setupRadioGroup({readonly: true}); - click(1); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('false'); - }); - }); - - describe('keyboard interaction', () => { - it('should update the group value on Space', () => { - setupRadioGroup(); - space(); - expect(radioButtonElements[0].getAttribute('aria-checked')).toBe('true'); - }); - - it('should update the group value on Enter', () => { - setupRadioGroup(); - enter(); - expect(radioButtonElements[0].getAttribute('aria-checked')).toBe('true'); - }); - - it('should not change the value if the radio group is readonly', () => { - setupRadioGroup({orientation: 'horizontal', readonly: true}); - right(); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('false'); - }); - - it('should not change the value if the radio group is disabled', () => { - setupRadioGroup({orientation: 'horizontal', disabled: true}); - right(); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('false'); - }); - - describe('horizontal orientation', () => { - it('should update the group value on ArrowRight', () => { - setupRadioGroup({orientation: 'horizontal'}); - right(); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('true'); - }); - - it('should update the group value on ArrowLeft', () => { - setupRadioGroup({orientation: 'horizontal'}); - right(); - right(); - left(); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('true'); - }); - - describe('text direction rtl', () => { - it('should update the group value on ArrowLeft', () => { - setupRadioGroup({orientation: 'horizontal', textDirection: 'rtl'}); - left(); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('true'); - }); - - it('should update the group value on ArrowRight', () => { - setupRadioGroup({orientation: 'horizontal', textDirection: 'rtl'}); - left(); - left(); - right(); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('true'); - }); - }); - }); - - describe('vertical orientation', () => { - it('should update the group value on ArrowDown', () => { - setupRadioGroup({orientation: 'vertical'}); - down(); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('true'); - }); - - it('should update the group value on ArrowUp', () => { - setupRadioGroup({orientation: 'vertical'}); - down(); - down(); - up(); - expect(radioButtonElements[1].getAttribute('aria-checked')).toBe('true'); - }); - }); - }); - }); - - function runNavigationTests( - focusMode: 'activedescendant' | 'roving', - isFocused: (index: number) => boolean, - ) { - describe(`keyboard navigation (focusMode="${focusMode}")`, () => { - it('should move focus to and select the last enabled radio button on End', () => { - setupRadioGroup({focusMode}); - end(); - expect(isFocused(4)).toBe(true); - }); - - it('should move focus to and select the first enabled radio button on Home', () => { - setupRadioGroup({focusMode}); - end(); - home(); - expect(isFocused(0)).toBe(true); - }); - - it('should not allow keyboard navigation or selection if the group is disabled', () => { - setupRadioGroup({focusMode, orientation: 'horizontal', disabled: true}); - right(); - expect(isFocused(0)).toBe(false); - }); - - it('should allow keyboard navigation if the group is readonly', () => { - setupRadioGroup({focusMode, orientation: 'horizontal', readonly: true}); - right(); - expect(isFocused(1)).toBe(true); - }); - - describe('vertical orientation', () => { - it('should move focus to the next radio button on ArrowDown', () => { - setupRadioGroup({focusMode, orientation: 'vertical'}); - down(); - expect(isFocused(1)).toBe(true); - }); - - it('should move focus to the previous radio button on ArrowUp', () => { - setupRadioGroup({focusMode, orientation: 'vertical'}); - down(); - down(); - up(); - expect(isFocused(1)).toBe(true); - }); - - it('should skip disabled radio buttons when softDisabled is false', () => { - setupRadioGroup({ - focusMode, - orientation: 'vertical', - softDisabled: false, - disabledOptions: [1, 2], - }); - down(); - expect(isFocused(3)).toBe(true); - }); - - it('should not skip disabled radio buttons (softDisabled="true")', () => { - setupRadioGroup({ - focusMode, - orientation: 'vertical', - softDisabled: true, - disabledOptions: [1, 2], - }); - down(); - expect(isFocused(1)).toBe(true); - }); - }); - - describe('horizontal orientation', () => { - it('should move focus to the next radio button on ArrowRight', () => { - setupRadioGroup({focusMode, orientation: 'horizontal'}); - right(); - expect(isFocused(1)).toBe(true); - }); - - it('should move focus to the previous radio button on ArrowLeft', () => { - setupRadioGroup({focusMode, orientation: 'horizontal'}); - right(); - right(); - left(); - expect(isFocused(1)).toBe(true); - }); - - it('should skip disabled radio buttons (softDisabled="false")', () => { - setupRadioGroup({ - focusMode, - orientation: 'horizontal', - softDisabled: false, - disabledOptions: [1, 2], - }); - right(); - expect(isFocused(3)).toBe(true); - }); - - it('should not skip disabled radio buttons (softDisabled="true")', () => { - setupRadioGroup({ - focusMode, - orientation: 'horizontal', - softDisabled: true, - disabledOptions: [1, 2], - }); - right(); - expect(isFocused(1)).toBe(true); - }); - - describe('text direction rtl', () => { - it('should move focus to the next radio button on ArrowLeft', () => { - setupRadioGroup({focusMode, textDirection: 'rtl', orientation: 'horizontal'}); - left(); - expect(isFocused(1)).toBe(true); - }); - - it('should move focus to the previous radio button on ArrowRight', () => { - setupRadioGroup({focusMode, textDirection: 'rtl', orientation: 'horizontal'}); - left(); - left(); - right(); - expect(isFocused(1)).toBe(true); - }); - - it('should skip disabled radio buttons when navigating', () => { - setupRadioGroup({ - focusMode, - softDisabled: false, - textDirection: 'rtl', - disabledOptions: [1, 2], - orientation: 'horizontal', - }); - left(); - expect(isFocused(3)).toBe(true); - }); - }); - }); - }); - - describe(`pointer navigation (focusMode="${focusMode}")`, () => { - it('should move focus to the clicked radio button', () => { - setupRadioGroup({focusMode}); - click(3); - expect(isFocused(3)).toBe(true); - }); - - it('should not move focus to the clicked radio button if the group is disabled (softDisabled="false")', () => { - setupRadioGroup({focusMode, softDisabled: false, disabled: true}); - click(3); - expect(isFocused(3)).toBe(false); - }); - - it('should not move focus to the clicked radio button if the group is disabled (softDisabled="true")', () => { - setupRadioGroup({focusMode, softDisabled: true, disabled: true}); - click(3); - expect(isFocused(0)).toBe(false); - }); - - it('should move focus to the clicked radio button if the group is readonly', () => { - setupRadioGroup({focusMode, readonly: true}); - click(3); - expect(isFocused(3)).toBe(true); - }); - }); - } - - runNavigationTests('roving', i => { - return radioButtonElements[i].getAttribute('tabindex') === '0'; - }); - - runNavigationTests('activedescendant', i => { - return radioGroupElement.getAttribute('aria-activedescendant') === radioButtonElements[i].id; - }); - - describe('failure cases', () => { - it('should handle an empty set of radio buttons gracefully', () => { - setupRadioGroup({options: []}); - expect(radioButtons.length).toBe(0); - }); - - describe('bad accessibility violations', () => { - it('should report when the selected radio button is disabled and softDisabled is false', () => { - spyOn(console, 'error'); - setupRadioGroup({value: 1, softDisabled: false, disabledOptions: [1]}); - expect(console.error).toHaveBeenCalled(); - }); - }); - }); -}); - -interface TestOption { - value: number; - label: string; - disabled: boolean; -} - -@Component({ - template: ` -
- @for (option of options(); track option.value) { -
{{ option.label }}
- } -
- `, - imports: [RadioGroup, RadioButton], -}) -class RadioGroupExample { - options = signal([ - {value: 0, label: '0', disabled: false}, - {value: 1, label: '1', disabled: false}, - {value: 2, label: '2', disabled: false}, - {value: 3, label: '3', disabled: false}, - {value: 4, label: '4', disabled: false}, - ]); - - value: number | null = null; - disabled = false; - readonly = false; - softDisabled = false; - focusMode: 'roving' | 'activedescendant' = 'roving'; - orientation: 'horizontal' | 'vertical' = 'horizontal'; -} - -@Component({ - template: ` -
-
0
-
1
-
2
-
- `, - imports: [RadioGroup, RadioButton], -}) -class DefaultRadioGroupExample {} diff --git a/src/aria/radio-group/radio-group.ts b/src/aria/radio-group/radio-group.ts deleted file mode 100644 index b9d023808af6..000000000000 --- a/src/aria/radio-group/radio-group.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import { - afterRenderEffect, - booleanAttribute, - computed, - contentChildren, - Directive, - ElementRef, - inject, - input, - linkedSignal, - model, - signal, - WritableSignal, -} from '@angular/core'; -import { - RadioButtonPattern, - RadioGroupInputs, - RadioGroupPattern, - ToolbarRadioGroupInputs, - ToolbarRadioGroupPattern, -} from '@angular/aria/private'; -import {Directionality} from '@angular/cdk/bidi'; -import {_IdGenerator} from '@angular/cdk/a11y'; -import {ToolbarWidgetGroup} from '@angular/aria/toolbar'; - -// TODO: Move mapSignal to it's own file so it can be reused across components. - -/** - * Creates a new writable signal (signal V) whose value is connected to the given original - * writable signal (signal T) such that updating signal V updates signal T and vice-versa. - * - * This function establishes a two-way synchronization between the source signal and the new mapped - * signal. When the source signal changes, the mapped signal updates by applying the `transform` - * function. When the mapped signal is explicitly set or updated, the change is propagated back to - * the source signal by applying the `reverse` function. - */ -export function mapSignal( - originalSignal: WritableSignal, - operations: { - transform: (value: T) => V; - reverse: (value: V) => T; - }, -) { - const mappedSignal = linkedSignal(() => operations.transform(originalSignal())); - const updateMappedSignal = mappedSignal.update; - const setMappedSignal = mappedSignal.set; - - mappedSignal.set = (newValue: V) => { - setMappedSignal(newValue); - originalSignal.set(operations.reverse(newValue)); - }; - - mappedSignal.update = (updateFn: (value: V) => V) => { - updateMappedSignal(oldValue => updateFn(oldValue)); - originalSignal.update(oldValue => operations.reverse(updateFn(operations.transform(oldValue)))); - }; - - return mappedSignal; -} - -/** - * A radio button group container. - * - * Radio groups are used to group multiple radio buttons or radio group labels so they function as - * a single form control. The RadioGroup is meant to be used in conjunction with RadioButton - * as follows: - * - * ```html - *
- *
Option 1
- *
Option 2
- *
Option 3
- *
- * ``` - */ -@Directive({ - selector: '[ngRadioGroup]', - exportAs: 'ngRadioGroup', - host: { - 'role': 'radiogroup', - 'class': 'ng-radio-group', - '[attr.tabindex]': '_pattern.tabindex()', - '[attr.aria-readonly]': '_pattern.readonly()', - '[attr.aria-disabled]': '_pattern.disabled()', - '[attr.aria-orientation]': '_pattern.orientation()', - '[attr.aria-activedescendant]': '_pattern.activedescendant()', - '(keydown)': '_pattern.onKeydown($event)', - '(pointerdown)': '_pattern.onPointerdown($event)', - '(focusin)': 'onFocus()', - }, - hostDirectives: [ - { - directive: ToolbarWidgetGroup, - inputs: ['disabled'], - }, - ], -}) -export class RadioGroup { - /** A reference to the radio group element. */ - private readonly _elementRef = inject(ElementRef); - - /** A reference to the ToolbarWidgetGroup, if the radio group is in a toolbar. */ - private readonly _toolbarWidgetGroup = inject(ToolbarWidgetGroup); - - /** Whether the radio group is inside of a Toolbar. */ - private readonly _hasToolbar = computed(() => !!this._toolbarWidgetGroup.toolbar()); - - /** The RadioButtons nested inside of the RadioGroup. */ - private readonly _radioButtons = contentChildren(RadioButton, {descendants: true}); - - /** A signal wrapper for directionality. */ - protected textDirection = inject(Directionality).valueSignal; - - /** The RadioButton UIPatterns of the child RadioButtons. */ - protected items = computed(() => this._radioButtons().map(radio => radio._pattern)); - - /** Whether the radio group is vertically or horizontally oriented. */ - readonly orientation = input<'vertical' | 'horizontal'>('vertical'); - - /** Whether disabled items in the group should be focusable. */ - readonly softDisabled = input(false, {transform: booleanAttribute}); - - /** The focus strategy used by the radio group. */ - readonly focusMode = input<'roving' | 'activedescendant'>('roving'); - - /** Whether the radio group is disabled. */ - readonly disabled = input(false, {transform: booleanAttribute}); - - /** Whether the radio group is readonly. */ - readonly readonly = input(false, {transform: booleanAttribute}); - - /** The value of the currently selected radio button. */ - readonly value = model(null); - - /** The internal selection state for the radio group. */ - private readonly _value = mapSignal(this.value, { - transform: value => (value !== null ? [value] : []), - reverse: values => (values.length === 0 ? null : values[0]), - }); - - /** The RadioGroup UIPattern. */ - readonly _pattern: RadioGroupPattern; - - /** Whether the radio group has received focus yet. */ - private _hasFocused = signal(false); - - constructor() { - const inputs: RadioGroupInputs | ToolbarRadioGroupInputs = { - ...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._toolbarWidgetGroup.toolbar, - }; - - this._pattern = this._hasToolbar() - ? new ToolbarRadioGroupPattern(inputs as ToolbarRadioGroupInputs) - : new RadioGroupPattern(inputs as RadioGroupInputs); - - if (this._hasToolbar()) { - this._toolbarWidgetGroup.controls.set(this._pattern as ToolbarRadioGroupPattern); - } - - afterRenderEffect(() => { - if (typeof ngDevMode === 'undefined' || ngDevMode) { - const violations = this._pattern.validate(); - for (const violation of violations) { - console.error(violation); - } - } - }); - - afterRenderEffect(() => { - if (!this._hasFocused() && !this._hasToolbar()) { - this._pattern.setDefaultState(); - } - }); - } - - onFocus() { - this._hasFocused.set(true); - } -} - -/** A selectable radio button in a RadioGroup. */ -@Directive({ - selector: '[ngRadioButton]', - exportAs: 'ngRadioButton', - host: { - 'role': 'radio', - 'class': 'ng-radio-button', - '[attr.data-active]': '_pattern.active()', - '[attr.tabindex]': '_pattern.tabindex()', - '[attr.aria-checked]': '_pattern.selected()', - '[attr.aria-disabled]': '_pattern.disabled()', - '[id]': '_pattern.id()', - }, -}) -export class RadioButton { - /** A reference to the radio button element. */ - private readonly _elementRef = inject(ElementRef); - - /** The parent RadioGroup. */ - private readonly _radioGroup = inject(RadioGroup); - - /** A unique identifier for the radio button. */ - private readonly _generatedId = inject(_IdGenerator).getId('ng-radio-button-', true); - - /** A unique identifier for the radio button. */ - readonly id = computed(() => this._generatedId); - - /** The value associated with the radio button. */ - readonly value = input.required(); - - /** The parent RadioGroup UIPattern. */ - readonly group = computed(() => this._radioGroup._pattern); - - /** A reference to the radio button element to be focused on navigation. */ - element = computed(() => this._elementRef.nativeElement); - - /** Whether the radio button is disabled. */ - disabled = input(false, {transform: booleanAttribute}); - - /** Whether the radio button is selected. */ - readonly selected = computed(() => this._pattern.selected()); - - /** The RadioButton UIPattern. */ - readonly _pattern = new RadioButtonPattern({ - ...this, - id: this.id, - value: this.value, - group: this.group, - element: this.element, - }); -} diff --git a/src/components-examples/aria/radio-group/BUILD.bazel b/src/components-examples/aria/radio-group/BUILD.bazel deleted file mode 100644 index fa62632483d9..000000000000 --- a/src/components-examples/aria/radio-group/BUILD.bazel +++ /dev/null @@ -1,29 +0,0 @@ -load("//tools:defaults.bzl", "ng_project") - -package(default_visibility = ["//visibility:public"]) - -ng_project( - name = "radio-group", - srcs = glob(["**/*.ts"]), - assets = glob([ - "**/*.html", - "**/*.css", - ]), - deps = [ - "//:node_modules/@angular/core", - "//:node_modules/@angular/forms", - "//src/aria/radio-group", - "//src/material/checkbox", - "//src/material/form-field", - "//src/material/select", - ], -) - -filegroup( - name = "source-files", - srcs = glob([ - "**/*.html", - "**/*.css", - "**/*.ts", - ]), -) diff --git a/src/components-examples/aria/radio-group/index.ts b/src/components-examples/aria/radio-group/index.ts deleted file mode 100644 index 1eac914e3216..000000000000 --- a/src/components-examples/aria/radio-group/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export {RadioGroupStandardExample} from './radio-group-standard/radio-group-standard-example'; -export {RadioGroupHorizontalExample} from './radio-group-horizontal/radio-group-horizontal-example'; -export {RadioGroupRtlHorizontalExample} from './radio-group-rtl-horizontal/radio-group-rtl-horizontal-example'; -export {RadioGroupActiveDescendantExample} from './radio-group-active-descendant/radio-group-active-descendant-example'; -export {RadioGroupDisabledFocusableExample} from './radio-group-disabled-focusable/radio-group-disabled-focusable-example'; -export {RadioGroupDisabledSkippedExample} from './radio-group-disabled-skipped/radio-group-disabled-skipped-example'; -export {RadioGroupReadonlyExample} from './radio-group-readonly/radio-group-readonly-example'; -export {RadioGroupDisabledExample} from './radio-group-disabled/radio-group-disabled-example'; -export {RadioGroupConfigurableExample} from './radio-group-configurable/radio-group-configurable-example'; diff --git a/src/components-examples/aria/radio-group/radio-common.css b/src/components-examples/aria/radio-group/radio-common.css deleted file mode 100644 index 35d90bdb095e..000000000000 --- a/src/components-examples/aria/radio-group/radio-common.css +++ /dev/null @@ -1,90 +0,0 @@ -.example-radio-controls { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 16px; - padding-bottom: 16px; -} - -.example-radio-group { - gap: 4px; - margin: 0; - padding: 8px; - max-height: 300px; - border: 1px solid var(--mat-sys-outline); - border-radius: var(--mat-sys-corner-extra-small); - display: flex; - list-style: none; - flex-direction: column; - overflow: scroll; -} - -.example-radio-group[aria-orientation='horizontal'] { - flex-direction: row; -} - -.example-radio-group[aria-disabled='true'] { - pointer-events: none; -} - -.example-radio-group label { - padding: 16px; - flex-shrink: 0; -} - -.example-radio-button { - gap: 16px; - padding: 16px; - display: flex; - cursor: pointer; - position: relative; - align-items: center; - border-radius: var(--mat-sys-corner-extra-small); -} - -/* Basic visual indicator for the radio button */ -.example-radio-indicator { - width: 16px; - height: 16px; - border-radius: 50%; - border: 2px solid var(--mat-sys-outline); - display: inline-block; - position: relative; -} - -.example-radio-button[aria-checked='true'] .example-radio-indicator { - border-color: var(--mat-sys-primary); -} - -.example-radio-button[aria-checked='true'] .example-radio-indicator::after { - content: ''; - display: block; - width: 8px; - height: 8px; - border-radius: 50%; - background-color: var(--mat-sys-primary); - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -.example-radio-button[aria-disabled='true'][aria-checked='true'] .example-radio-indicator::after { - background-color: var(--mat-sys-outline); -} - -.example-radio-button[aria-disabled='true'] { - cursor: default; -} - -.example-radio-button[aria-disabled='true']::before { - content: ''; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - border-radius: var(--mat-sys-corner-extra-small); - background-color: var(--mat-sys-on-surface); - opacity: var(--mat-sys-focus-state-layer-opacity); -} diff --git a/src/components-examples/aria/radio-group/radio-group-active-descendant/radio-group-active-descendant-example.html b/src/components-examples/aria/radio-group/radio-group-active-descendant/radio-group-active-descendant-example.html deleted file mode 100644 index 47695d797949..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-active-descendant/radio-group-active-descendant-example.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
    - - @for (fruit of fruits; track fruit) { -
  • - - {{ fruit }} -
  • - } -
-
diff --git a/src/components-examples/aria/radio-group/radio-group-active-descendant/radio-group-active-descendant-example.ts b/src/components-examples/aria/radio-group/radio-group-active-descendant/radio-group-active-descendant-example.ts deleted file mode 100644 index d111affcfe74..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-active-descendant/radio-group-active-descendant-example.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Component} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {RadioGroup, RadioButton} from '@angular/aria/radio-group'; - -/** @title Active descendant radio group. */ -@Component({ - selector: 'radio-group-active-descendant-example', - templateUrl: 'radio-group-active-descendant-example.html', - styleUrl: '../radio-common.css', - imports: [RadioGroup, RadioButton, FormsModule], -}) -export class RadioGroupActiveDescendantExample { - fruits = [ - 'Apple', - 'Apricot', - 'Banana', - 'Blackberry', - 'Blueberry', - 'Cantaloupe', - 'Cherry', - 'Clementine', - 'Cranberry', - 'Dates', - 'Figs', - 'Grapes', - 'Grapefruit', - 'Guava', - 'Kiwi', - 'Kumquat', - 'Lemon', - 'Lime', - 'Mandarin', - 'Mango', - 'Nectarine', - 'Orange', - 'Papaya', - 'Passion', - 'Peach', - 'Pear', - 'Pineapple', - 'Plum', - 'Pomegranate', - 'Raspberries', - 'Strawberry', - 'Tangerine', - 'Watermelon', - ]; -} diff --git a/src/components-examples/aria/radio-group/radio-group-configurable/radio-group-configurable-example.html b/src/components-examples/aria/radio-group/radio-group-configurable/radio-group-configurable-example.html deleted file mode 100644 index 48ae573fac4a..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-configurable/radio-group-configurable-example.html +++ /dev/null @@ -1,56 +0,0 @@ -
- Disabled - Readonly - Soft Disabled - - - Disabled Radio Options - - @for (fruit of fruits; track fruit) { - {{fruit}} - } - - - - - Orientation - - Vertical - Horizontal - - - - - Focus strategy - - Roving Tabindex - Active Descendant - - -
- - -
    - @for (fruit of fruits; track fruit) { - @let optionDisabled = disabledOptions.includes(fruit); -
  • - - {{ fruit }} -
  • - } -
- diff --git a/src/components-examples/aria/radio-group/radio-group-configurable/radio-group-configurable-example.ts b/src/components-examples/aria/radio-group/radio-group-configurable/radio-group-configurable-example.ts deleted file mode 100644 index e2a450967988..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-configurable/radio-group-configurable-example.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {Component} from '@angular/core'; -import {RadioGroup, RadioButton} from '@angular/aria/radio-group'; -import {MatCheckboxModule} from '@angular/material/checkbox'; -import {MatFormFieldModule} from '@angular/material/form-field'; -import {MatSelectModule} from '@angular/material/select'; -import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; - -/** @title Configurable CDK Radio Group */ -@Component({ - selector: 'radio-group-configurable-example', - templateUrl: 'radio-group-configurable-example.html', - styleUrl: '../radio-common.css', - imports: [ - RadioGroup, - RadioButton, - MatCheckboxModule, - MatFormFieldModule, - MatSelectModule, - FormsModule, - ReactiveFormsModule, - ], -}) -export class RadioGroupConfigurableExample { - orientation: 'vertical' | 'horizontal' = 'vertical'; - disabled = new FormControl(false, {nonNullable: true}); - - fruits = [ - 'Apple', - 'Apricot', - 'Banana', - 'Blackberry', - 'Blueberry', - 'Cantaloupe', - 'Cherry', - 'Clementine', - 'Cranberry', - 'Dates', - 'Figs', - 'Grapes', - 'Grapefruit', - 'Guava', - 'Kiwi', - 'Kumquat', - 'Lemon', - 'Lime', - 'Mandarin', - 'Mango', - 'Nectarine', - 'Orange', - 'Papaya', - 'Passion', - 'Peach', - 'Pear', - 'Pineapple', - 'Plum', - 'Pomegranate', - 'Raspberries', - 'Strawberry', - 'Tangerine', - 'Watermelon', - ]; - - // New controls - readonly = new FormControl(false, {nonNullable: true}); - softDisabled = new FormControl(false, {nonNullable: true}); - focusMode: 'roving' | 'activedescendant' = 'roving'; - - // Control for which radio options are individually disabled - disabledOptions: string[] = ['Banana']; -} diff --git a/src/components-examples/aria/radio-group/radio-group-disabled-focusable/radio-group-disabled-focusable-example.html b/src/components-examples/aria/radio-group/radio-group-disabled-focusable/radio-group-disabled-focusable-example.html deleted file mode 100644 index a99742b60561..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-disabled-focusable/radio-group-disabled-focusable-example.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
    - - @for (fruit of fruits; track fruit) { -
  • - - {{ fruit }} {{ disabledFruits.includes(fruit) ? '(Disabled)' : '' }} -
  • - } -
-
diff --git a/src/components-examples/aria/radio-group/radio-group-disabled-focusable/radio-group-disabled-focusable-example.ts b/src/components-examples/aria/radio-group/radio-group-disabled-focusable/radio-group-disabled-focusable-example.ts deleted file mode 100644 index d85354a2199c..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-disabled-focusable/radio-group-disabled-focusable-example.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {Component} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {RadioGroup, RadioButton} from '@angular/aria/radio-group'; - -/** @title Radio group with disabled options that are focusable. */ -@Component({ - selector: 'radio-group-disabled-focusable-example', - templateUrl: 'radio-group-disabled-focusable-example.html', - styleUrl: '../radio-common.css', - imports: [RadioGroup, RadioButton, FormsModule], -}) -export class RadioGroupDisabledFocusableExample { - fruits = [ - 'Apple', - 'Apricot', - 'Banana', - 'Blackberry', - 'Blueberry', - 'Cantaloupe', - 'Cherry', - 'Clementine', - 'Cranberry', - 'Dates', - 'Figs', - 'Grapes', - 'Grapefruit', - 'Guava', - 'Kiwi', - 'Kumquat', - 'Lemon', - 'Lime', - 'Mandarin', - 'Mango', - 'Nectarine', - 'Orange', - 'Papaya', - 'Passion', - 'Peach', - 'Pear', - 'Pineapple', - 'Plum', - 'Pomegranate', - 'Raspberries', - 'Strawberry', - 'Tangerine', - 'Watermelon', - ]; - disabledFruits = ['Banana', 'Kiwi']; -} diff --git a/src/components-examples/aria/radio-group/radio-group-disabled-skipped/radio-group-disabled-skipped-example.html b/src/components-examples/aria/radio-group/radio-group-disabled-skipped/radio-group-disabled-skipped-example.html deleted file mode 100644 index 45a7b882f3ea..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-disabled-skipped/radio-group-disabled-skipped-example.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
    - - @for (fruit of fruits; track fruit) { -
  • - - {{ fruit }} {{ disabledFruits.includes(fruit) ? '(Disabled)' : '' }} -
  • - } -
-
diff --git a/src/components-examples/aria/radio-group/radio-group-disabled-skipped/radio-group-disabled-skipped-example.ts b/src/components-examples/aria/radio-group/radio-group-disabled-skipped/radio-group-disabled-skipped-example.ts deleted file mode 100644 index 9e74081d5788..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-disabled-skipped/radio-group-disabled-skipped-example.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {Component} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {RadioGroup, RadioButton} from '@angular/aria/radio-group'; - -/** @title Radio group with disabled options that are skipped. */ -@Component({ - selector: 'radio-group-disabled-skipped-example', - templateUrl: 'radio-group-disabled-skipped-example.html', - styleUrl: '../radio-common.css', - imports: [RadioGroup, RadioButton, FormsModule], -}) -export class RadioGroupDisabledSkippedExample { - fruits = [ - 'Apple', - 'Apricot', - 'Banana', - 'Blackberry', - 'Blueberry', - 'Cantaloupe', - 'Cherry', - 'Clementine', - 'Cranberry', - 'Dates', - 'Figs', - 'Grapes', - 'Grapefruit', - 'Guava', - 'Kiwi', - 'Kumquat', - 'Lemon', - 'Lime', - 'Mandarin', - 'Mango', - 'Nectarine', - 'Orange', - 'Papaya', - 'Passion', - 'Peach', - 'Pear', - 'Pineapple', - 'Plum', - 'Pomegranate', - 'Raspberries', - 'Strawberry', - 'Tangerine', - 'Watermelon', - ]; - disabledFruits = ['Banana', 'Kiwi']; -} diff --git a/src/components-examples/aria/radio-group/radio-group-disabled/radio-group-disabled-example.html b/src/components-examples/aria/radio-group/radio-group-disabled/radio-group-disabled-example.html deleted file mode 100644 index ec853708668b..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-disabled/radio-group-disabled-example.html +++ /dev/null @@ -1,25 +0,0 @@ -
-
    - - @for (fruit of fruits; track fruit) { -
  • - - {{ fruit }} -
  • - } -
-

The entire radio group is disabled. Focus should not enter the group.

-
diff --git a/src/components-examples/aria/radio-group/radio-group-disabled/radio-group-disabled-example.ts b/src/components-examples/aria/radio-group/radio-group-disabled/radio-group-disabled-example.ts deleted file mode 100644 index 932769eb06a4..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-disabled/radio-group-disabled-example.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Component} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {RadioGroup, RadioButton} from '@angular/aria/radio-group'; - -/** @title Disabled radio group. */ -@Component({ - selector: 'radio-group-disabled-example', - templateUrl: 'radio-group-disabled-example.html', - styleUrl: '../radio-common.css', - imports: [RadioGroup, RadioButton, FormsModule], -}) -export class RadioGroupDisabledExample { - fruits = [ - 'Apple', - 'Apricot', - 'Banana', - 'Blackberry', - 'Blueberry', - 'Cantaloupe', - 'Cherry', - 'Clementine', - 'Cranberry', - 'Dates', - 'Figs', - 'Grapes', - 'Grapefruit', - 'Guava', - 'Kiwi', - 'Kumquat', - 'Lemon', - 'Lime', - 'Mandarin', - 'Mango', - 'Nectarine', - 'Orange', - 'Papaya', - 'Passion', - 'Peach', - 'Pear', - 'Pineapple', - 'Plum', - 'Pomegranate', - 'Raspberries', - 'Strawberry', - 'Tangerine', - 'Watermelon', - ]; -} diff --git a/src/components-examples/aria/radio-group/radio-group-horizontal/radio-group-horizontal-example.html b/src/components-examples/aria/radio-group/radio-group-horizontal/radio-group-horizontal-example.html deleted file mode 100644 index 1454692ce4ba..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-horizontal/radio-group-horizontal-example.html +++ /dev/null @@ -1,24 +0,0 @@ -
-
    - - @for (fruit of fruits; track fruit) { -
  • - - {{ fruit }} -
  • - } -
-
diff --git a/src/components-examples/aria/radio-group/radio-group-horizontal/radio-group-horizontal-example.ts b/src/components-examples/aria/radio-group/radio-group-horizontal/radio-group-horizontal-example.ts deleted file mode 100644 index 081b62def973..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-horizontal/radio-group-horizontal-example.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Component} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {RadioGroup, RadioButton} from '@angular/aria/radio-group'; - -/** @title Horizontal radio group. */ -@Component({ - selector: 'radio-group-horizontal-example', - templateUrl: 'radio-group-horizontal-example.html', - styleUrl: '../radio-common.css', - imports: [RadioGroup, RadioButton, FormsModule], -}) -export class RadioGroupHorizontalExample { - fruits = [ - 'Apple', - 'Apricot', - 'Banana', - 'Blackberry', - 'Blueberry', - 'Cantaloupe', - 'Cherry', - 'Clementine', - 'Cranberry', - 'Dates', - 'Figs', - 'Grapes', - 'Grapefruit', - 'Guava', - 'Kiwi', - 'Kumquat', - 'Lemon', - 'Lime', - 'Mandarin', - 'Mango', - 'Nectarine', - 'Orange', - 'Papaya', - 'Passion', - 'Peach', - 'Pear', - 'Pineapple', - 'Plum', - 'Pomegranate', - 'Raspberries', - 'Strawberry', - 'Tangerine', - 'Watermelon', - ]; -} diff --git a/src/components-examples/aria/radio-group/radio-group-readonly/radio-group-readonly-example.html b/src/components-examples/aria/radio-group/radio-group-readonly/radio-group-readonly-example.html deleted file mode 100644 index b0cf762d9e8b..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-readonly/radio-group-readonly-example.html +++ /dev/null @@ -1,25 +0,0 @@ -
-
    - - @for (fruit of fruits; track fruit) { -
  • - - {{ fruit }} -
  • - } -
-

The radio group is navigable, but selection cannot be changed.

-
diff --git a/src/components-examples/aria/radio-group/radio-group-readonly/radio-group-readonly-example.ts b/src/components-examples/aria/radio-group/radio-group-readonly/radio-group-readonly-example.ts deleted file mode 100644 index 3bb4d5cbad03..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-readonly/radio-group-readonly-example.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Component} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {RadioGroup, RadioButton} from '@angular/aria/radio-group'; - -/** @title Readonly radio group. */ -@Component({ - selector: 'radio-group-readonly-example', - templateUrl: 'radio-group-readonly-example.html', - styleUrl: '../radio-common.css', - imports: [RadioGroup, RadioButton, FormsModule], -}) -export class RadioGroupReadonlyExample { - fruits = [ - 'Apple', - 'Apricot', - 'Banana', - 'Blackberry', - 'Blueberry', - 'Cantaloupe', - 'Cherry', - 'Clementine', - 'Cranberry', - 'Dates', - 'Figs', - 'Grapes', - 'Grapefruit', - 'Guava', - 'Kiwi', - 'Kumquat', - 'Lemon', - 'Lime', - 'Mandarin', - 'Mango', - 'Nectarine', - 'Orange', - 'Papaya', - 'Passion', - 'Peach', - 'Pear', - 'Pineapple', - 'Plum', - 'Pomegranate', - 'Raspberries', - 'Strawberry', - 'Tangerine', - 'Watermelon', - ]; -} diff --git a/src/components-examples/aria/radio-group/radio-group-rtl-horizontal/radio-group-rtl-horizontal-example.html b/src/components-examples/aria/radio-group/radio-group-rtl-horizontal/radio-group-rtl-horizontal-example.html deleted file mode 100644 index fd7aee3fac99..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-rtl-horizontal/radio-group-rtl-horizontal-example.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
    - - @for (fruit of fruits; track fruit) { -
  • - - {{ fruit }} -
  • - } -
-
diff --git a/src/components-examples/aria/radio-group/radio-group-rtl-horizontal/radio-group-rtl-horizontal-example.ts b/src/components-examples/aria/radio-group/radio-group-rtl-horizontal/radio-group-rtl-horizontal-example.ts deleted file mode 100644 index 46a3bcc6fab1..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-rtl-horizontal/radio-group-rtl-horizontal-example.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {Component} from '@angular/core'; -import {Dir} from '@angular/cdk/bidi'; -import {FormsModule} from '@angular/forms'; -import {RadioGroup, RadioButton} from '@angular/aria/radio-group'; - -/** @title RTL horizontal radio group. */ -@Component({ - selector: 'radio-group-rtl-horizontal-example', - templateUrl: 'radio-group-rtl-horizontal-example.html', - styleUrl: '../radio-common.css', - imports: [RadioGroup, RadioButton, Dir, FormsModule], -}) -export class RadioGroupRtlHorizontalExample { - fruits = [ - 'Apple', - 'Apricot', - 'Banana', - 'Blackberry', - 'Blueberry', - 'Cantaloupe', - 'Cherry', - 'Clementine', - 'Cranberry', - 'Dates', - 'Figs', - 'Grapes', - 'Grapefruit', - 'Guava', - 'Kiwi', - 'Kumquat', - 'Lemon', - 'Lime', - 'Mandarin', - 'Mango', - 'Nectarine', - 'Orange', - 'Papaya', - 'Passion', - 'Peach', - 'Pear', - 'Pineapple', - 'Plum', - 'Pomegranate', - 'Raspberries', - 'Strawberry', - 'Tangerine', - 'Watermelon', - ]; -} diff --git a/src/components-examples/aria/radio-group/radio-group-standard/radio-group-standard-example.html b/src/components-examples/aria/radio-group/radio-group-standard/radio-group-standard-example.html deleted file mode 100644 index df143ec1ed1b..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-standard/radio-group-standard-example.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
    - - @for (fruit of fruits; track fruit) { -
  • - - {{ fruit }} -
  • - } -
-
diff --git a/src/components-examples/aria/radio-group/radio-group-standard/radio-group-standard-example.ts b/src/components-examples/aria/radio-group/radio-group-standard/radio-group-standard-example.ts deleted file mode 100644 index aa3f9564f31e..000000000000 --- a/src/components-examples/aria/radio-group/radio-group-standard/radio-group-standard-example.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Component} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {RadioGroup, RadioButton} from '@angular/aria/radio-group'; - -/** @title Basic radio group. */ -@Component({ - selector: 'radio-group-standard-example', - templateUrl: 'radio-group-standard-example.html', - styleUrl: '../radio-common.css', - imports: [RadioGroup, RadioButton, FormsModule], -}) -export class RadioGroupStandardExample { - fruits = [ - 'Apple', - 'Apricot', - 'Banana', - 'Blackberry', - 'Blueberry', - 'Cantaloupe', - 'Cherry', - 'Clementine', - 'Cranberry', - 'Dates', - 'Figs', - 'Grapes', - 'Grapefruit', - 'Guava', - 'Kiwi', - 'Kumquat', - 'Lemon', - 'Lime', - 'Mandarin', - 'Mango', - 'Nectarine', - 'Orange', - 'Papaya', - 'Passion', - 'Peach', - 'Pear', - 'Pineapple', - 'Plum', - 'Pomegranate', - 'Raspberries', - 'Strawberry', - 'Tangerine', - 'Watermelon', - ]; -} diff --git a/src/components-examples/aria/toolbar/BUILD.bazel b/src/components-examples/aria/toolbar/BUILD.bazel index aac5f1eb7eed..13d158794d80 100644 --- a/src/components-examples/aria/toolbar/BUILD.bazel +++ b/src/components-examples/aria/toolbar/BUILD.bazel @@ -12,7 +12,6 @@ ng_project( deps = [ "//:node_modules/@angular/core", "//:node_modules/@angular/forms", - "//src/aria/radio-group", "//src/aria/toolbar", "//src/cdk/a11y", "//src/material/checkbox", diff --git a/src/components-examples/aria/toolbar/toolbar-basic-horizontal/toolbar-basic-horizontal-example.html b/src/components-examples/aria/toolbar/toolbar-basic-horizontal/toolbar-basic-horizontal-example.html index caa014e61494..7c8de4b52241 100644 --- a/src/components-examples/aria/toolbar/toolbar-basic-horizontal/toolbar-basic-horizontal-example.html +++ b/src/components-examples/aria/toolbar/toolbar-basic-horizontal/toolbar-basic-horizontal-example.html @@ -21,18 +21,6 @@ (keydown.enter)="format('underline')"> Underline -
    - @for (alignment of alignments; track alignment) { -
  • - - {{ alignment.label }} -
  • - } -
-
    - @for (alignment of alignments; track alignment) { -
  • - - {{ alignment.label }} -
  • - } -
-
    - @for (fruit of fruits; track fruit) { - @let optionDisabled = disabledOptions.includes(fruit); -
  • - - {{ fruit }} -
  • - } -
-
    - @for (fruit of fruits; track fruit) { - @let optionDisabled = disabledOptions.includes(fruit); -
  • - - {{ fruit }} -
  • - } -
-
    - @for (alignment of alignments; track alignment) { -
  • - - {{ alignment.label }} -
  • - } -
-
    - @for (alignment of alignments; track alignment) { -
  • - - {{ alignment.label }} -
  • - } -