diff --git a/goldens/aria/private/index.api.md b/goldens/aria/private/index.api.md index a605f4808bd1..d50314736c02 100644 --- a/goldens/aria/private/index.api.md +++ b/goldens/aria/private/index.api.md @@ -642,20 +642,29 @@ export class TabPattern { } // @public -export type ToolbarInputs = Omit, V>, 'multi' | 'typeaheadDelay' | 'selectionMode' | 'focusMode'> & { - getItem: (e: Element) => ToolbarWidgetPattern | undefined; +export type ToolbarInputs = { + element: SignalLike; + activeItem: WritableSignalLike; + items: SignalLike; + softDisabled: SignalLike; + disabled: SignalLike; + orientation: SignalLike<'vertical' | 'horizontal'>; + textDirection: SignalLike<'rtl' | 'ltr'>; + wrap: SignalLike; + getItem: (e: Element) => ToolbarWidgetPattern | undefined; }; // @public -export class ToolbarPattern { - constructor(inputs: ToolbarInputs); +export class ToolbarPattern { + constructor(inputs: ToolbarInputs); readonly activeDescendant: SignalLike; - readonly activeItem: () => ToolbarWidgetPattern | undefined; + readonly activeItem: () => ToolbarWidgetPattern | undefined; readonly disabled: SignalLike; + readonly focusManager: ListFocus; readonly hasBeenInteracted: WritableSignalLike; // (undocumented) - readonly inputs: ToolbarInputs; - readonly listBehavior: List, V>; + readonly inputs: ToolbarInputs; + readonly navigationBehavior: ListNavigation; onClick(event: MouseEvent): void; // (undocumented) onFocusIn(): void; @@ -663,64 +672,50 @@ export class ToolbarPattern { // (undocumented) onPointerdown(event: PointerEvent): void; readonly orientation: SignalLike<'vertical' | 'horizontal'>; - // (undocumented) - select(): void; setDefaultState(): void; setDefaultStateEffect(): void; readonly softDisabled: SignalLike; readonly tabIndex: SignalLike<-1 | 0>; - validate(): string[]; } // @public -export interface ToolbarWidgetGroupInputs, V> { +export interface ToolbarWidgetGroupInputs { disabled: SignalLike; - items: SignalLike; - multi: SignalLike; - toolbar: SignalLike | undefined>; + items: SignalLike; + toolbar: SignalLike; } // @public -export class ToolbarWidgetGroupPattern, V> { - constructor(inputs: ToolbarWidgetGroupInputs); +export class ToolbarWidgetGroupPattern { + constructor(inputs: ToolbarWidgetGroupInputs); readonly disabled: () => boolean; // (undocumented) - readonly element: () => undefined; - // (undocumented) - readonly inputs: ToolbarWidgetGroupInputs; - readonly multi: () => boolean; - // (undocumented) - readonly searchTerm: () => string; - // (undocumented) - readonly selectable: () => boolean; - readonly toolbar: () => ToolbarPattern | undefined; - // (undocumented) - readonly value: () => V; + readonly inputs: ToolbarWidgetGroupInputs; + readonly toolbar: () => ToolbarPattern | undefined; } // @public -export interface ToolbarWidgetInputs extends Omit, 'searchTerm' | 'index' | 'selectable'> { - group: SignalLike, V> | undefined>; - toolbar: SignalLike>; +export interface ToolbarWidgetInputs { + disabled: SignalLike; + element: SignalLike; + group: SignalLike; + id: SignalLike; + toolbar: SignalLike; } // @public (undocumented) -export class ToolbarWidgetPattern implements ListItem { - constructor(inputs: ToolbarWidgetInputs); +export class ToolbarWidgetPattern implements ListFocusItem, ListNavigationItem { + constructor(inputs: ToolbarWidgetInputs); readonly active: SignalLike; readonly disabled: () => boolean; readonly element: () => HTMLElement | undefined; - readonly group: () => ToolbarWidgetGroupPattern, V> | undefined; + readonly group: () => ToolbarWidgetGroupPattern | undefined; readonly id: () => string; readonly index: SignalLike; // (undocumented) - readonly inputs: ToolbarWidgetInputs; - readonly searchTerm: () => string; - readonly selectable: () => boolean; - readonly selected: SignalLike; + readonly inputs: ToolbarWidgetInputs; readonly tabIndex: SignalLike<-1 | 0>; - readonly toolbar: () => ToolbarPattern; - readonly value: () => V; + readonly toolbar: () => ToolbarPattern; } // @public diff --git a/goldens/aria/toolbar/index.api.md b/goldens/aria/toolbar/index.api.md index 214740b50357..b5271638b38c 100644 --- a/goldens/aria/toolbar/index.api.md +++ b/goldens/aria/toolbar/index.api.md @@ -12,62 +12,58 @@ import { OnInit } from '@angular/core'; import { Signal } from '@angular/core'; // @public -export class Toolbar implements OnDestroy { +export class Toolbar implements OnDestroy { constructor(); - readonly _collection: SortedCollection>; + readonly _collection: SortedCollection; readonly disabled: _angular_core.InputSignalWithTransform; readonly element: HTMLElement; - readonly _itemPatterns: _angular_core.Signal[]>; + readonly _itemPatterns: _angular_core.Signal; // (undocumented) ngOnDestroy(): void; readonly orientation: _angular_core.InputSignal<"horizontal" | "vertical">; - readonly _pattern: ToolbarPattern; + readonly _pattern: ToolbarPattern; readonly softDisabled: _angular_core.InputSignalWithTransform; readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>; - readonly value: _angular_core.ModelSignal; readonly wrap: _angular_core.InputSignalWithTransform; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngToolbar]", ["ngToolbar"], { "orientation": { "alias": "orientation"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "wrap": { "alias": "wrap"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, never>; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) - static ɵfac: _angular_core.ɵɵFactoryDeclaration, never>; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; } // @public -export const TOOLBAR_WIDGET_GROUP: InjectionToken>; +export const TOOLBAR_WIDGET_GROUP: InjectionToken; // @public -export class ToolbarWidget implements OnInit, OnDestroy { +export class ToolbarWidget implements OnInit, OnDestroy { readonly active: _angular_core.Signal; readonly disabled: _angular_core.InputSignalWithTransform; readonly element: HTMLElement; - readonly _group: ToolbarWidgetGroup | null; + readonly _group: ToolbarWidgetGroup | null; readonly hardDisabled: _angular_core.Signal; readonly id: _angular_core.InputSignal; // (undocumented) ngOnDestroy(): void; // (undocumented) ngOnInit(): void; - readonly _pattern: ToolbarWidgetPattern; - readonly selected: () => boolean; - readonly _toolbarPattern: _angular_core.Signal>; - readonly value: _angular_core.InputSignal; + readonly _pattern: ToolbarWidgetPattern; + readonly _toolbarPattern: _angular_core.Signal; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngToolbarWidget]", ["ngToolbarWidget"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) - static ɵfac: _angular_core.ɵɵFactoryDeclaration, never>; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; } // @public -export class ToolbarWidgetGroup { +export class ToolbarWidgetGroup { constructor(); readonly disabled: _angular_core.InputSignalWithTransform; readonly element: HTMLElement; - readonly multi: _angular_core.InputSignalWithTransform; - readonly _pattern: ToolbarWidgetGroupPattern, V>; + readonly _pattern: ToolbarWidgetGroupPattern; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngToolbarWidgetGroup]", ["ngToolbarWidgetGroup"], { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "multi": { "alias": "multi"; "required": false; "isSignal": true; }; }, {}, ["_widgets"], never, true, never>; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) - static ɵfac: _angular_core.ɵɵFactoryDeclaration, never>; + static ɵfac: _angular_core.ɵɵFactoryDeclaration; } // (No @packageDocumentation comment for this package) diff --git a/src/aria/private/public-api.ts b/src/aria/private/public-api.ts index 213e6706b137..79fe18c8219c 100644 --- a/src/aria/private/public-api.ts +++ b/src/aria/private/public-api.ts @@ -15,7 +15,6 @@ export * from './toolbar/toolbar'; export * from './toolbar/toolbar-widget'; export * from './toolbar/toolbar-widget-group'; export * from './accordion/accordion'; -export * from './toolbar/toolbar'; export * from './tree/tree'; export * from './grid/grid'; export * from './grid/row'; diff --git a/src/aria/private/toolbar/BUILD.bazel b/src/aria/private/toolbar/BUILD.bazel index f74e0e585ce2..ef4ee78e1941 100644 --- a/src/aria/private/toolbar/BUILD.bazel +++ b/src/aria/private/toolbar/BUILD.bazel @@ -12,7 +12,8 @@ ts_project( deps = [ "//:node_modules/@angular/core", "//src/aria/private/behaviors/event-manager", - "//src/aria/private/behaviors/list", + "//src/aria/private/behaviors/list-focus", + "//src/aria/private/behaviors/list-navigation", "//src/aria/private/behaviors/signal-like", "//src/cdk/platform", ], diff --git a/src/aria/private/toolbar/toolbar-widget-group.ts b/src/aria/private/toolbar/toolbar-widget-group.ts index c59eeb231204..77bfc184ed95 100644 --- a/src/aria/private/toolbar/toolbar-widget-group.ts +++ b/src/aria/private/toolbar/toolbar-widget-group.ts @@ -6,40 +6,29 @@ * found in the LICENSE file at https://angular.dev/license */ -import {ListItem} from '../behaviors/list/list'; import {SignalLike} from '../behaviors/signal-like/signal-like'; import type {ToolbarPattern} from './toolbar'; +import type {ToolbarWidgetPattern} from './toolbar-widget'; /** Represents the required inputs for a toolbar widget group. */ -export interface ToolbarWidgetGroupInputs, V> { +export interface ToolbarWidgetGroupInputs { /** A reference to the parent toolbar. */ - toolbar: SignalLike | undefined>; + toolbar: SignalLike; /** Whether the widget group is disabled. */ disabled: SignalLike; /** The list of items within the widget group. */ - items: SignalLike; - - /** Whether the group allows multiple widgets to be selected. */ - multi: SignalLike; + items: SignalLike; } /** A group of widgets within a toolbar that provides nested navigation. */ -export class ToolbarWidgetGroupPattern, V> { +export class ToolbarWidgetGroupPattern { /** Whether the widget is disabled. */ readonly disabled = () => this.inputs.disabled(); /** A reference to the parent toolbar. */ readonly toolbar = () => this.inputs.toolbar(); - /** Whether the group allows multiple widgets to be selected. */ - readonly multi = () => this.inputs.multi(); - - readonly searchTerm = () => ''; // Unused because toolbar does not support typeahead. - readonly value = () => '' as V; // Unused because toolbar does not support selection. - readonly selectable = () => true; // Unused because toolbar does not support selection. - readonly element = () => undefined; // Unused because toolbar does not focus the group element. - - constructor(readonly inputs: ToolbarWidgetGroupInputs) {} + constructor(readonly inputs: ToolbarWidgetGroupInputs) {} } diff --git a/src/aria/private/toolbar/toolbar-widget.ts b/src/aria/private/toolbar/toolbar-widget.ts index 4239de8b2727..ba0be1dd3274 100644 --- a/src/aria/private/toolbar/toolbar-widget.ts +++ b/src/aria/private/toolbar/toolbar-widget.ts @@ -7,23 +7,30 @@ */ import {SignalLike, computed} from '../behaviors/signal-like/signal-like'; -import {ListItem} from '../behaviors/list/list'; +import {ListFocusItem} from '../behaviors/list-focus/list-focus'; +import {ListNavigationItem} from '../behaviors/list-navigation/list-navigation'; import type {ToolbarPattern} from './toolbar'; import {ToolbarWidgetGroupPattern} from './toolbar-widget-group'; /** Represents the required inputs for a toolbar widget in a toolbar. */ -export interface ToolbarWidgetInputs extends Omit< - ListItem, - 'searchTerm' | 'index' | 'selectable' -> { +export interface ToolbarWidgetInputs { + /** A unique identifier for the widget. */ + id: SignalLike; + + /** The html element that should receive focus. */ + element: SignalLike; + + /** Whether the widget is disabled. */ + disabled: SignalLike; + /** A reference to the parent toolbar. */ - toolbar: SignalLike>; + toolbar: SignalLike; /** A reference to the parent widget group. */ - group: SignalLike, V> | undefined>; + group: SignalLike; } -export class ToolbarWidgetPattern implements ListItem { +export class ToolbarWidgetPattern implements ListFocusItem, ListNavigationItem { /** A unique identifier for the widget. */ readonly id = () => this.inputs.id(); @@ -40,27 +47,13 @@ export class ToolbarWidgetPattern implements ListItem { readonly toolbar = () => this.inputs.toolbar(); /** The tabindex of the widget. */ - readonly tabIndex = computed(() => this.toolbar().listBehavior.getItemTabindex(this)); - - /** The text used by the typeahead search. */ - readonly searchTerm = () => ''; // Unused because toolbar does not support typeahead. - - /** The value associated with the widget. */ - readonly value = () => this.inputs.value(); - - /** Whether the widget is selectable. */ - readonly selectable = () => true; // Unused because toolbar does not support selection. + readonly tabIndex = computed(() => this.toolbar().focusManager.getItemTabIndex(this)); /** The position of the widget within the toolbar. */ readonly index = computed(() => this.toolbar().inputs.items().indexOf(this) ?? -1); - /** Whether the widget is selected (only relevant in a selection group). */ - readonly selected = computed(() => - this.toolbar().listBehavior.inputs.value().includes(this.value()), - ); - /** Whether the widget is currently the active one (focused). */ readonly active: SignalLike = computed(() => this.toolbar().activeItem() === this); - constructor(readonly inputs: ToolbarWidgetInputs) {} + constructor(readonly inputs: ToolbarWidgetInputs) {} } diff --git a/src/aria/private/toolbar/toolbar.spec.ts b/src/aria/private/toolbar/toolbar.spec.ts index b62ec5d01fbc..d7f714001298 100644 --- a/src/aria/private/toolbar/toolbar.spec.ts +++ b/src/aria/private/toolbar/toolbar.spec.ts @@ -19,11 +19,11 @@ import { import {ModifierKeys} from '@angular/cdk/testing'; // Test types -type TestWidget = ToolbarWidgetPattern & { +type TestWidget = ToolbarWidgetPattern & { inputs: {disabled: WritableSignalLike}; }; -type TestWidgetGroup = ToolbarWidgetGroupPattern, string> & { +type TestWidgetGroup = ToolbarWidgetGroupPattern & { disabled: WritableSignalLike; items: WritableSignalLike; }; @@ -31,8 +31,8 @@ type TestWidgetGroup = ToolbarWidgetGroupPattern, s type TestItem = TestWidget; type TestInputs = { - readonly [K in keyof ToolbarInputs]: WritableSignalLike< - ToolbarInputs[K] extends SignalLike ? T : never + readonly [K in keyof ToolbarInputs]: WritableSignalLike< + ToolbarInputs[K] extends SignalLike ? T : never >; }; @@ -46,7 +46,7 @@ const right = () => createKeyboardEvent('keydown', 39, 'ArrowRight'); const left = () => createKeyboardEvent('keydown', 37, 'ArrowLeft'); const space = () => createKeyboardEvent('keydown', 32, ' '); -function clickItem(item: ToolbarWidgetPattern, mods?: ModifierKeys) { +function clickItem(item: ToolbarWidgetPattern, mods?: ModifierKeys) { return { target: item.element(), shiftKey: mods?.shift, @@ -65,7 +65,7 @@ function getToolbarPattern( const activeItem = signal(undefined); const allItems = computed(() => { - const flatItems: ToolbarWidgetPattern[] = []; + const flatItems: ToolbarWidgetPattern[] = []; for (const item of items()) { if (item instanceof ToolbarWidgetGroupPattern) { flatItems.push(...item.inputs.items()); @@ -76,11 +76,10 @@ function getToolbarPattern( return flatItems; }); - const toolbar = new ToolbarPattern({ + const toolbar = new ToolbarPattern({ element, items, activeItem, - value: signal([]), wrap: signal(inputs.wrap ?? true), disabled: signal(inputs.disabled ?? false), softDisabled: signal(inputs.softDisabled ?? true), @@ -93,36 +92,34 @@ function getToolbarPattern( } function getWidgetPattern( - value: string, - toolbar: ToolbarPattern, - group?: ToolbarWidgetGroupPattern, string>, + id: string, + toolbar: ToolbarPattern, + group?: ToolbarWidgetGroupPattern, ): TestWidget { const element = signal(document.createElement('button')); - const widget = new ToolbarWidgetPattern({ - id: signal(`widget-${value}`), + const widget = new ToolbarWidgetPattern({ + id: signal(id), element, disabled: signal(false), - value: signal(value), group: signal(group), toolbar: signal(toolbar), }); return widget as TestWidget; } -function getWidgetGroupPattern(id: string, toolbar: ToolbarPattern): TestWidgetGroup { +function getWidgetGroupPattern(id: string, toolbar: ToolbarPattern): TestWidgetGroup { const disabled = signal(false); const items = signal([]); - const group = new ToolbarWidgetGroupPattern, string>({ + const group = new ToolbarWidgetGroupPattern({ disabled, toolbar: signal(toolbar), items, - multi: signal(false), }); - (group as TestWidgetGroup).disabled = disabled; - (group as TestWidgetGroup).items = items; - return group as TestWidgetGroup; + (group as unknown as TestWidgetGroup).disabled = disabled; + (group as unknown as TestWidgetGroup).items = items; + return group as unknown as TestWidgetGroup; } function getPatterns( @@ -159,8 +156,8 @@ function getPatterns( } describe('Toolbar Pattern', () => { - function getItem(toolbar: ToolbarPattern, value: string) { - return toolbar.inputs.items().find(item => item.value() === value)!; + function getItem(toolbar: ToolbarPattern, id: string) { + return toolbar.inputs.items().find(item => item.id() === id)!; } describe('Tabindex', () => { @@ -183,34 +180,34 @@ describe('Toolbar Pattern', () => { const {toolbar} = getPatterns(); const item5 = getItem(toolbar, 'item 5'); toolbar.onClick(clickItem(item5)); - expect(toolbar.activeItem()?.value()).toBe('item 5'); + expect(toolbar.activeItem()?.id()).toBe('item 5'); }); describe('with ltr text direction', () => { it('should navigate next on ArrowRight', () => { const {toolbar} = getPatterns(); toolbar.onKeydown(right()); // Item 0 -> Item 1 - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate prev on ArrowLeft', () => { const {toolbar} = getPatterns(); toolbar.onKeydown(right()); // Item 0 -> Item 1 toolbar.onKeydown(left()); // Item 1 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should not navigate next on ArrowDown when not in a widget group (ltr)', () => { const {toolbar} = getPatterns(); toolbar.onKeydown(down()); // Item 0 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should not navigate prev on ArrowUp when not in a widget group (ltr)', () => { const {toolbar} = getPatterns(); toolbar.onKeydown(right()); // Item 0 -> Item 1 toolbar.onKeydown(up()); // Item 1 -> Item 1 - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate next in a widget group on ArrowDown (ltr)', () => { @@ -220,7 +217,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(right()); // Item 1 -> Item 2 (Group 0) toolbar.onKeydown(down()); // Item 2 -> Item 3 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 3'); + expect(toolbar.activeItem()?.id()).toBe('item 3'); }); it('should navigate prev in a widget group on ArrowUp (ltr)', () => { @@ -231,7 +228,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 2 -> Item 3 (Group 0) toolbar.onKeydown(up()); // Item 3 -> Item 2 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); it('should navigate last to first in a widget group on ArrowDown (ltr)', () => { @@ -243,7 +240,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 3 -> Item 4 (Group 0) toolbar.onKeydown(down()); // Item 4 -> Item 2 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); it('should navigate first to last in a widget group on ArrowUp (ltr)', () => { @@ -253,7 +250,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(right()); // Item 1 -> Item 2 (Group 0) toolbar.onKeydown(up()); // Item 2 -> Item 4 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 4'); + expect(toolbar.activeItem()?.id()).toBe('item 4'); }); describe('with wrap false', () => { @@ -261,13 +258,13 @@ describe('Toolbar Pattern', () => { const {toolbar} = getPatterns({wrap: false}); toolbar.onKeydown(end()); toolbar.onKeydown(right()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should not wrap from first to last (horizontal, ltr)', () => { const {toolbar} = getPatterns({wrap: false}); toolbar.onKeydown(left()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); }); @@ -277,7 +274,7 @@ describe('Toolbar Pattern', () => { items[1].inputs.disabled.set(true); toolbar.onKeydown(right()); // Item 0 -> Item 1 (disabled) - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should not skip disabled items when navigating prev (horizontal, ltr)', () => { @@ -288,7 +285,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(right()); // Item 1 -> Item 2 toolbar.onKeydown(left()); // Item 2 -> Item 1 (disabled) - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should not skip disabled groups when navigating next', () => { @@ -297,14 +294,14 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(right()); // Item 0 -> Item 1 toolbar.onKeydown(right()); // Item 1 -> Item 2 - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); it('should not skip disabled groups when navigating prev', () => { const {toolbar, group0} = getPatterns({softDisabled: true}); group0.disabled.set(true); toolbar.onKeydown(left()); // Item 0 -> Item 8 - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should navigate to the last item on End (horizontal, ltr)', () => { @@ -312,7 +309,7 @@ describe('Toolbar Pattern', () => { items[items.length - 1].inputs.disabled.set(true); toolbar.onKeydown(end()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should navigate to the first item on Home (horizontal, ltr)', () => { @@ -322,7 +319,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); // Item 0 -> Item 8 toolbar.onKeydown(home()); // Item 8 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); describe('with wrap true', () => { @@ -333,7 +330,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(right()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should wrap from first to last (horizontal, ltr)', () => { @@ -341,7 +338,7 @@ describe('Toolbar Pattern', () => { items[items.length - 1].inputs.disabled.set(true); toolbar.onKeydown(left()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); }); }); @@ -352,7 +349,7 @@ describe('Toolbar Pattern', () => { items[1].inputs.disabled.set(true); toolbar.onClick(clickItem(items[1])); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should skip disabled items when navigating next (horizontal, ltr)', () => { @@ -360,7 +357,7 @@ describe('Toolbar Pattern', () => { items[1].inputs.disabled.set(true); toolbar.onKeydown(right()); // Item 0 -> Item 2 (skips Item 1) - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); it('should skip disabled items when navigating prev (horizontal, ltr)', () => { @@ -370,7 +367,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(right()); // Item 0 -> Item 2 toolbar.onKeydown(left()); // Item 2 -> Item 0 (skips Item 1) - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should not navigate to items in disabled groups on click', () => { @@ -378,7 +375,7 @@ describe('Toolbar Pattern', () => { group0.disabled.set(true); const item2 = getItem(toolbar, 'item 2'); toolbar.onClick(clickItem(item2)); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should skip disabled groups when navigating next', () => { @@ -388,7 +385,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(right()); // Item 0 -> Item 1 toolbar.onKeydown(right()); // Item 1 -> Item 5 (skips Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 5'); + expect(toolbar.activeItem()?.id()).toBe('item 5'); }); it('should skip disabled groups when navigating prev', () => { @@ -399,7 +396,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(left()); // Item 0 -> Item 5 (skips Group 1) toolbar.onKeydown(left()); // Item 5 -> Item 1 (skips Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate to the last focusable item on End (horizontal, ltr)', () => { @@ -407,7 +404,7 @@ describe('Toolbar Pattern', () => { items[items.length - 1].inputs.disabled.set(true); toolbar.onKeydown(end()); - expect(toolbar.activeItem()?.value()).toBe('item 7'); + expect(toolbar.activeItem()?.id()).toBe('item 7'); }); it('should navigate to the first focusable item on Home (horizontal, ltr)', () => { @@ -417,7 +414,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(home()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); describe('with wrap true', () => { @@ -428,7 +425,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(right()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should wrap from first to last focusable item (horizontal, ltr)', () => { @@ -438,7 +435,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(left()); - expect(toolbar.activeItem()?.value()).toBe('item 7'); + expect(toolbar.activeItem()?.id()).toBe('item 7'); }); }); @@ -449,7 +446,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(right()); - expect(toolbar.activeItem()?.value()).toBe('item 7'); + expect(toolbar.activeItem()?.id()).toBe('item 7'); }); it('should not wrap from first to last focusable item (horizontal, ltr)', () => { @@ -460,7 +457,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(left()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); }); }); @@ -471,33 +468,33 @@ describe('Toolbar Pattern', () => { const {toolbar} = getPatterns({textDirection: 'rtl'}); const item5 = getItem(toolbar, 'item 5'); toolbar.onClick(clickItem(item5)); - expect(toolbar.activeItem()?.value()).toBe('item 5'); + expect(toolbar.activeItem()?.id()).toBe('item 5'); }); it('should navigate next on ArrowLeft (rtl)', () => { const {toolbar} = getPatterns({textDirection: 'rtl'}); toolbar.onKeydown(left()); // Item 0 -> Item 1 - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate prev on ArrowRight (rtl)', () => { const {toolbar} = getPatterns({textDirection: 'rtl'}); toolbar.onKeydown(left()); // Item 0 -> Item 1 toolbar.onKeydown(right()); // Item 1 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should not navigate next on ArrowDown when not in a widget group (rtl)', () => { const {toolbar} = getPatterns({textDirection: 'rtl'}); toolbar.onKeydown(up()); // Item 0 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should not navigate prev on ArrowUp when not in a widget group (rtl)', () => { const {toolbar} = getPatterns({textDirection: 'rtl'}); toolbar.onKeydown(left()); // Item 0 -> Item 1 toolbar.onKeydown(down()); // Item 1 -> Item 1 - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate next in a widget group on ArrowDown (rtl)', () => { @@ -507,7 +504,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(left()); // Item 1 -> Item 2 (Group 0) toolbar.onKeydown(down()); // Item 2 -> Item 3 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 3'); + expect(toolbar.activeItem()?.id()).toBe('item 3'); }); it('should navigate prev in a widget group on ArrowUp (rtl)', () => { @@ -518,7 +515,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 2 -> Item 3 (Group 0) toolbar.onKeydown(up()); // Item 3 -> Item 2 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); it('should navigate first to last in a widget group on ArrowUp (rtl)', () => { @@ -528,7 +525,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(left()); // Item 1 -> Item 2 (Group 0) toolbar.onKeydown(up()); // Item 2 -> Item 4 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 4'); + expect(toolbar.activeItem()?.id()).toBe('item 4'); }); it('should navigate last to first in a widget group on ArrowDown (rtl)', () => { @@ -540,7 +537,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 3 -> Item 4 (Group 0) toolbar.onKeydown(down()); // Item 4 -> Item 2 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); describe('with softDisabled true', () => { @@ -549,7 +546,7 @@ describe('Toolbar Pattern', () => { items[1].inputs.disabled.set(true); toolbar.onKeydown(left()); // Item 0 -> Item 1 (disabled) - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should not skip disabled items when navigating prev (horizontal, rtl)', () => { @@ -560,7 +557,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(left()); // Item 1 -> Item 2 toolbar.onKeydown(right()); // Item 2 -> Item 1 (disabled) - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate to the last item on End (horizontal, rtl)', () => { @@ -568,7 +565,7 @@ describe('Toolbar Pattern', () => { items[items.length - 1].inputs.disabled.set(true); toolbar.onKeydown(end()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should navigate to the first item on Home (horizontal, rtl)', () => { @@ -578,7 +575,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(left()); // Item 0 -> Item 1 toolbar.onKeydown(home()); // Item 1 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); describe('with wrap true', () => { @@ -593,7 +590,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(left()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should wrap from first to last (horizontal, rtl)', () => { @@ -605,7 +602,7 @@ describe('Toolbar Pattern', () => { items[items.length - 1].inputs.disabled.set(true); toolbar.onKeydown(right()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); }); @@ -619,7 +616,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(left()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should not wrap from first to last (horizontal, rtl)', () => { @@ -631,7 +628,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(right()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); }); }); @@ -642,7 +639,7 @@ describe('Toolbar Pattern', () => { items[1].inputs.disabled.set(true); toolbar.onKeydown(left()); // Item 0 -> Item 2 (skips Item 1) - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); it('should skip disabled items when navigating prev (horizontal, rtl)', () => { @@ -652,7 +649,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(left()); // Item 0 -> Item 2 toolbar.onKeydown(right()); // Item 2 -> Item 0 (skips Item 1) - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should navigate to the last focusable item on End (horizontal, rtl)', () => { @@ -660,7 +657,7 @@ describe('Toolbar Pattern', () => { items[items.length - 1].inputs.disabled.set(true); toolbar.onKeydown(end()); - expect(toolbar.activeItem()?.value()).toBe('item 7'); + expect(toolbar.activeItem()?.id()).toBe('item 7'); }); it('should navigate to the first focusable item on Home (horizontal, rtl)', () => { @@ -670,7 +667,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(home()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); describe('with wrap true', () => { @@ -685,7 +682,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(left()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should wrap from first to last focusable item (horizontal, rtl)', () => { @@ -699,7 +696,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(right()); - expect(toolbar.activeItem()?.value()).toBe('item 7'); + expect(toolbar.activeItem()?.id()).toBe('item 7'); }); }); @@ -712,7 +709,7 @@ describe('Toolbar Pattern', () => { }); toolbar.onKeydown(end()); toolbar.onKeydown(left()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should not wrap from first to last focusable item (horizontal, rtl)', () => { @@ -726,7 +723,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(right()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); }); }); @@ -738,27 +735,27 @@ describe('Toolbar Pattern', () => { it('should navigate next on ArrowDown (vertical, ltr)', () => { const {toolbar} = getPatterns({orientation: 'vertical'}); toolbar.onKeydown(down()); // Item 0 -> Item 1 - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate prev on ArrowUp (vertical, ltr)', () => { const {toolbar} = getPatterns({orientation: 'vertical'}); toolbar.onKeydown(down()); // Item 0 -> Item 1 toolbar.onKeydown(up()); // Item 1 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should not navigate next on ArrowRight when not in a widget group', () => { const {toolbar} = getPatterns({orientation: 'vertical'}); toolbar.onKeydown(right()); // Item 0 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should not navigate prev on ArrowLeft when not in a widget group', () => { const {toolbar} = getPatterns({orientation: 'vertical'}); toolbar.onKeydown(down()); // Item 0 -> Item 1 toolbar.onKeydown(left()); // Item 1 -> Item 1 - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate next in a widget group on ArrowRight', () => { @@ -768,7 +765,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 1 -> Item 2 (Group 0) toolbar.onKeydown(right()); // Item 2 -> Item 3 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 3'); + expect(toolbar.activeItem()?.id()).toBe('item 3'); }); it('should navigate prev in a widget group on ArrowLeft', () => { @@ -779,7 +776,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(right()); // Item 2 -> Item 3 (Group 0) toolbar.onKeydown(left()); // Item 3 -> Item 2 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); it('should navigate last to first in a widget group on ArrowRight', () => { @@ -791,7 +788,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(right()); // Item 3 -> Item 4 (Group 0) toolbar.onKeydown(right()); // Item 4 -> Item 2 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); it('should navigate first to last in a widget group on ArrowLeft', () => { @@ -801,7 +798,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 1 -> Item 2 (Group 0) toolbar.onKeydown(left()); // Item 2 -> Item 4 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 4'); + expect(toolbar.activeItem()?.id()).toBe('item 4'); }); describe('with softDisabled true', () => { @@ -811,7 +808,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 0 -> Item 1 (disabled) - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should not skip disabled items when navigating prev (vertical, ltr)', () => { @@ -822,7 +819,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 1 -> Item 2 toolbar.onKeydown(up()); // Item 2 -> Item 1 (disabled) - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate to the last item on End (vertical, ltr)', () => { @@ -830,7 +827,7 @@ describe('Toolbar Pattern', () => { items[items.length - 1].inputs.disabled.set(true); toolbar.onKeydown(end()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should navigate to the first item on Home (vertical, ltr)', () => { @@ -840,7 +837,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 0 -> Item 1 toolbar.onKeydown(home()); // Item 1 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); describe('with wrap true', () => { @@ -855,7 +852,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(down()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should wrap from first to last (vertical, ltr)', () => { @@ -869,7 +866,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(up()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); }); @@ -883,7 +880,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(down()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should not wrap from first to last (vertical, ltr)', () => { @@ -895,7 +892,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(up()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); }); }); @@ -917,7 +914,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 0 -> Item 2 toolbar.onKeydown(up()); // Item 2 -> Item 0 (skips Item 1) - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should navigate to the last focusable item on End (vertical, ltr)', () => { @@ -925,7 +922,7 @@ describe('Toolbar Pattern', () => { items[items.length - 1].inputs.disabled.set(true); toolbar.onKeydown(end()); - expect(toolbar.activeItem()?.value()).toBe('item 7'); + expect(toolbar.activeItem()?.id()).toBe('item 7'); }); it('should navigate to the first focusable item on Home (vertical, ltr)', () => { @@ -935,7 +932,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(home()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); describe('with wrap true', () => { @@ -950,7 +947,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(down()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should wrap from first to last focusable item (vertical, ltr)', () => { @@ -964,7 +961,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(up()); - expect(toolbar.activeItem()?.value()).toBe('item 7'); + expect(toolbar.activeItem()?.id()).toBe('item 7'); }); }); @@ -978,7 +975,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(down()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should not wrap from first to last focusable item (vertical, ltr)', () => { @@ -992,7 +989,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(up()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); }); }); @@ -1002,14 +999,14 @@ describe('Toolbar Pattern', () => { it('should navigate next on ArrowDown (vertical, rtl)', () => { const {toolbar} = getPatterns({orientation: 'vertical', textDirection: 'rtl'}); toolbar.onKeydown(down()); // Item 0 -> Item 1 - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate prev on ArrowUp (vertical, rtl)', () => { const {toolbar} = getPatterns({orientation: 'vertical', textDirection: 'rtl'}); toolbar.onKeydown(down()); // Item 0 -> Item 1 toolbar.onKeydown(up()); // Item 1 -> Item 0 - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should navigate last to first in a widget group on ArrowLeft (vertical, rtl)', () => { @@ -1021,7 +1018,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(left()); // Item 3 -> Item 4 (Group 0) toolbar.onKeydown(left()); // Item 4 -> Item 2 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 2'); + expect(toolbar.activeItem()?.id()).toBe('item 2'); }); it('should navigate first to last in a widget group on ArrowRight (vertical, rtl)', () => { @@ -1031,7 +1028,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 1 -> Item 2 (Group 0) toolbar.onKeydown(right()); // Item 2 -> Item 4 (Group 0) - expect(toolbar.activeItem()?.value()).toBe('item 4'); + expect(toolbar.activeItem()?.id()).toBe('item 4'); }); describe('with softDisabled true', () => { @@ -1044,7 +1041,7 @@ describe('Toolbar Pattern', () => { items[1].inputs.disabled.set(true); toolbar.onKeydown(down()); // Item 0 -> Item 1 (disabled) - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should not skip disabled items when navigating prev (vertical, rtl)', () => { @@ -1059,7 +1056,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 1 -> Item 2 toolbar.onKeydown(up()); // Item 2 -> Item 1 (disabled) - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should navigate to the last item on End (vertical, rtl)', () => { @@ -1072,7 +1069,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should navigate to the first item on Home (vertical, rtl)', () => { @@ -1086,7 +1083,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 0 -> Item 1 toolbar.onKeydown(home()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); describe('with wrap true', () => { @@ -1102,7 +1099,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(down()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should wrap from first to last (vertical, rtl)', () => { @@ -1117,7 +1114,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(up()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); }); @@ -1132,7 +1129,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(down()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should not wrap from first to last (vertical, rtl)', () => { @@ -1145,7 +1142,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(up()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); }); }); @@ -1175,7 +1172,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(down()); // Item 0 -> Item 2 toolbar.onKeydown(up()); // Item 2 -> Item 0 (skips Item 1) - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); it('should navigate to the last focusable item on End (vertical, rtl)', () => { @@ -1201,7 +1198,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(home()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); describe('with wrap true', () => { @@ -1217,7 +1214,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(down()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); it('should wrap from first to last focusable item (vertical, rtl)', () => { @@ -1247,7 +1244,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(end()); toolbar.onKeydown(down()); - expect(toolbar.activeItem()?.value()).toBe('item 8'); + expect(toolbar.activeItem()?.id()).toBe('item 8'); }); it('should not wrap from first to last focusable item (vertical, rtl)', () => { @@ -1262,7 +1259,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(home()); toolbar.onKeydown(up()); - expect(toolbar.activeItem()?.value()).toBe('item 0'); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); }); }); @@ -1295,95 +1292,43 @@ describe('Toolbar Pattern', () => { }); }); - describe('Selection', () => { - it('should toggle the active item on Enter (selection)', () => { - const {toolbar} = getPatterns(); - expect(getItem(toolbar, 'item 0').selected()).toBeFalse(); - toolbar.onKeydown(enter()); - expect(getItem(toolbar, 'item 0').selected()).toBeTrue(); - toolbar.onKeydown(enter()); - expect(getItem(toolbar, 'item 0').selected()).toBeFalse(); - }); - - it('should toggle the active item on Space (selection)', () => { - const {toolbar} = getPatterns(); - expect(getItem(toolbar, 'item 0').selected()).toBeFalse(); - toolbar.onKeydown(space()); - expect(getItem(toolbar, 'item 0').selected()).toBeTrue(); - toolbar.onKeydown(space()); - expect(getItem(toolbar, 'item 0').selected()).toBeFalse(); - }); - - it('should toggle the active item on click (selection)', () => { + describe('Interactions', () => { + it('should set active item on click', () => { const {toolbar, items} = getPatterns(); - expect(getItem(toolbar, 'item 0').selected()).toBeFalse(); - toolbar.onClick(clickItem(items[0])); - expect(getItem(toolbar, 'item 0').selected()).toBeTrue(); - toolbar.onClick(clickItem(items[0])); - expect(getItem(toolbar, 'item 0').selected()).toBeFalse(); + expect(toolbar.activeItem()?.id()).toBe('item 0'); + toolbar.onClick(clickItem(items[1])); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); - it('should be able to select multiple items in the toolbar (selection)', () => { - const {toolbar} = getPatterns(); - expect(getItem(toolbar, 'item 0').selected()).toBeFalse(); - expect(getItem(toolbar, 'item 1').selected()).toBeFalse(); - - // Select first item - toolbar.onKeydown(enter()); - expect(getItem(toolbar, 'item 0').selected()).toBeTrue(); - expect(getItem(toolbar, 'item 1').selected()).toBeFalse(); - - // Navigate to and select second item - toolbar.onKeydown(right()); - toolbar.onKeydown(space()); - expect(getItem(toolbar, 'item 0').selected()).toBeTrue(); - expect(getItem(toolbar, 'item 1').selected()).toBeTrue(); + it('should not update active item on click if toolbar is disabled', () => { + const {toolbar, items} = getPatterns({disabled: true}); + toolbar.onClick(clickItem(items[1])); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); - it('should not be able to select multiple items in a group (selection)', () => { + it('should not intercept Enter or Space for toolbar-level selection', () => { const {toolbar} = getPatterns(); - expect(getItem(toolbar, 'item 2').selected()).toBeFalse(); - expect(getItem(toolbar, 'item 3').selected()).toBeFalse(); - - // Navigate to and select first item in group - toolbar.onKeydown(right()); - toolbar.onKeydown(right()); + expect(toolbar.activeItem()?.id()).toBe('item 0'); toolbar.onKeydown(enter()); - expect(getItem(toolbar, 'item 2').selected()).toBeTrue(); - expect(getItem(toolbar, 'item 3').selected()).toBeFalse(); - - // Navigate to and select second item in group - toolbar.onKeydown(right()); - toolbar.onKeydown(enter()); - expect(getItem(toolbar, 'item 2').selected()).toBeFalse(); - expect(getItem(toolbar, 'item 3').selected()).toBeTrue(); + expect(toolbar.activeItem()?.id()).toBe('item 0'); + toolbar.onKeydown(space()); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); - it('should not select disabled items (selection)', () => { - const {toolbar, items} = getPatterns(); + it('should allow clicking disabled items if softDisabled', () => { + const {toolbar, items} = getPatterns({softDisabled: true}); items[1].inputs.disabled.set(true); - // Navigate to disabled item - toolbar.onKeydown(right()); - expect(toolbar.activeItem()?.value()).toBe('item 1'); - - // Try to select disabled item - toolbar.onKeydown(enter()); - expect(getItem(toolbar, 'item 1').selected()).toBeFalse(); + toolbar.onClick(clickItem(items[1])); + expect(toolbar.activeItem()?.id()).toBe('item 1'); }); - it('should not select items in a disabled group (selection)', () => { - const {toolbar, items, group0} = getPatterns(); - group0.disabled.set(true); - - toolbar.onClick(clickItem(items[2])); - expect(toolbar.activeItem()?.value()).toBe('item 2'); - expect(getItem(toolbar, 'item 2').selected()).toBeFalse(); + it('should not activate disabled items on click if hardDisabled', () => { + const {toolbar, items} = getPatterns({softDisabled: false}); + items[1].inputs.disabled.set(true); - toolbar.onKeydown(right()); - toolbar.onKeydown(enter()); - expect(toolbar.activeItem()?.value()).toBe('item 3'); - expect(getItem(toolbar, 'item 3').selected()).toBeFalse(); + toolbar.onClick(clickItem(items[1])); + expect(toolbar.activeItem()?.id()).toBe('item 0'); }); }); // Close preceding section @@ -1392,7 +1337,7 @@ describe('Toolbar Pattern', () => { const {toolbar, items} = getPatterns(); toolbar.inputs.activeItem.set(items[1]); // Set to item 1 toolbar.setDefaultStateEffect(); - expect(toolbar.activeItem()?.value()).toBe('item 0'); // Should reset to item 0 + expect(toolbar.activeItem()?.id()).toBe('item 0'); // Should reset to item 0 }); it('should NOT set default state if keyboard interacted', () => { @@ -1401,7 +1346,7 @@ describe('Toolbar Pattern', () => { toolbar.onKeydown(right()); // Interaction (ArrowRight moves to item 1) toolbar.setDefaultStateEffect(); - expect(toolbar.activeItem()?.value()).toBe('item 1'); // Should stay on item 1 (interacted) + expect(toolbar.activeItem()?.id()).toBe('item 1'); // Should stay on item 1 (interacted) }); it('should NOT set default state if pointer interacted', () => { @@ -1410,7 +1355,7 @@ describe('Toolbar Pattern', () => { toolbar.onPointerdown(clickItem(items[1])); // Interaction toolbar.setDefaultStateEffect(); - expect(toolbar.activeItem()?.value()).toBe('item 1'); // Should stay on item 1 + expect(toolbar.activeItem()?.id()).toBe('item 1'); // Should stay on item 1 }); it('should NOT set default state if focus-in occurred', () => { @@ -1419,7 +1364,7 @@ describe('Toolbar Pattern', () => { toolbar.onFocusIn(); // Interaction toolbar.setDefaultStateEffect(); - expect(toolbar.activeItem()?.value()).toBe('item 1'); // Should stay on item 1 + expect(toolbar.activeItem()?.id()).toBe('item 1'); // Should stay on item 1 }); }); }); diff --git a/src/aria/private/toolbar/toolbar.ts b/src/aria/private/toolbar/toolbar.ts index 1fab0665c4a2..7cef4a257548 100644 --- a/src/aria/private/toolbar/toolbar.ts +++ b/src/aria/private/toolbar/toolbar.ts @@ -7,45 +7,75 @@ */ import {_getEventTarget} from '@angular/cdk/platform'; -import {computed, signal, SignalLike} from '../behaviors/signal-like/signal-like'; +import { + computed, + signal, + SignalLike, + WritableSignalLike, +} from '../behaviors/signal-like/signal-like'; import {KeyboardEventManager} from '../behaviors/event-manager'; -import {List, ListInputs} from '../behaviors/list/list'; +import {ListFocus} from '../behaviors/list-focus/list-focus'; +import {ListNavigation} from '../behaviors/list-navigation/list-navigation'; import {ToolbarWidgetPattern} from './toolbar-widget'; /** Represents the required inputs for a toolbar. */ -export type ToolbarInputs = Omit< - ListInputs, V>, - 'multi' | 'typeaheadDelay' | 'selectionMode' | 'focusMode' -> & { +export type ToolbarInputs = { + /** The html element that should receive focus. */ + element: SignalLike; + + /** The active item. */ + activeItem: WritableSignalLike; + + /** The items in the toolbar. */ + items: SignalLike; + + /** Whether disabled items in the toolbar should be focusable. */ + softDisabled: SignalLike; + + /** Whether the toolbar is disabled. */ + disabled: SignalLike; + + /** Whether the toolbar is vertically or horizontally oriented. */ + orientation: SignalLike<'vertical' | 'horizontal'>; + + /** The direction that text is read based on the users locale. */ + textDirection: SignalLike<'rtl' | 'ltr'>; + + /** Whether focus should wrap when navigating. */ + wrap: SignalLike; + /** A function that returns the toolbar item associated with a given element. */ - getItem: (e: Element) => ToolbarWidgetPattern | undefined; + getItem: (e: Element) => ToolbarWidgetPattern | undefined; }; /** Controls the state of a toolbar. */ -export class ToolbarPattern { - /** The list behavior for the toolbar. */ - readonly listBehavior: List, V>; +export class ToolbarPattern { + /** Controls focus for the toolbar. */ + readonly focusManager: ListFocus; + + /** Controls navigation for the toolbar. */ + readonly navigationBehavior: ListNavigation; /** Whether the toolbar has been interacted with. */ readonly hasBeenInteracted = signal(false); - /** Whether the tablist is vertically or horizontally oriented. */ + /** Whether the toolbar is vertically or horizontally oriented. */ readonly orientation: SignalLike<'vertical' | 'horizontal'>; /** Whether disabled items in the group should be focusable. */ readonly softDisabled: SignalLike; /** Whether the toolbar is disabled. */ - readonly disabled = computed(() => this.listBehavior.disabled()); + readonly disabled = computed(() => this.focusManager.isListDisabled()); - /** The tab index of the toolbar (if using activedescendant). */ - readonly tabIndex = computed(() => this.listBehavior.tabIndex()); + /** The tab index of the toolbar. */ + readonly tabIndex = computed(() => this.focusManager.getListTabIndex()); - /** The id of the current active widget (if using activedescendant). */ - readonly activeDescendant = computed(() => this.listBehavior.activeDescendant()); + /** The id of the current active widget. */ + readonly activeDescendant = computed(() => this.focusManager.getActiveDescendant()); /** The currently active item in the toolbar. */ - readonly activeItem = () => this.listBehavior.inputs.activeItem(); + readonly activeItem = () => this.inputs.activeItem(); /** The key used to navigate to the previous widget. */ private readonly _prevKey = computed(() => { @@ -82,30 +112,35 @@ export class ToolbarPattern { /** The keydown event manager for the toolbar. */ private readonly _keydown = computed(() => { const manager = new KeyboardEventManager(); + const activeItem = this.inputs.activeItem(); + + manager + .on(this._nextKey, () => this.navigationBehavior.next(), {ignoreRepeat: false}) + .on(this._prevKey, () => this.navigationBehavior.prev(), {ignoreRepeat: false}) + .on('Home', () => this.navigationBehavior.first()) + .on('End', () => this.navigationBehavior.last()); + + if (activeItem?.group()) { + manager + .on(this._altNextKey, () => this._groupNext(), {ignoreRepeat: false}) + .on(this._altPrevKey, () => this._groupPrev(), {ignoreRepeat: false}); + } - return manager - .on(this._nextKey, () => this.listBehavior.next(), {ignoreRepeat: false}) - .on(this._prevKey, () => this.listBehavior.prev(), {ignoreRepeat: false}) - .on(this._altNextKey, () => this._groupNext(), {ignoreRepeat: false}) - .on(this._altPrevKey, () => this._groupPrev(), {ignoreRepeat: false}) - .on(' ', () => this.select()) - .on('Enter', () => this.select()) - .on('Home', () => this.listBehavior.first()) - .on('End', () => this.listBehavior.last()); + return manager; }); /** Navigates to the next widget in a widget group. */ private _groupNext() { const currGroup = this.inputs.activeItem()?.group(); - const nextGroup = this.listBehavior.navigationBehavior.peekNext()?.group(); + const nextGroup = this.navigationBehavior.peekNext()?.group(); if (!currGroup) { return; } if (currGroup !== nextGroup) { - this.listBehavior.goto( - this.listBehavior.navigationBehavior.peekFirst({ + this.navigationBehavior.goto( + this.navigationBehavior.peekFirst({ items: currGroup.inputs.items(), })!, ); @@ -113,21 +148,21 @@ export class ToolbarPattern { return; } - this.listBehavior.next(); + this.navigationBehavior.next(); } /** Navigates to the previous widget in a widget group. */ private _groupPrev() { const currGroup = this.inputs.activeItem()?.group(); - const nextGroup = this.listBehavior.navigationBehavior.peekPrev()?.group(); + const nextGroup = this.navigationBehavior.peekPrev()?.group(); if (!currGroup) { return; } if (currGroup !== nextGroup) { - this.listBehavior.goto( - this.listBehavior.navigationBehavior.peekLast({ + this.navigationBehavior.goto( + this.navigationBehavior.peekLast({ items: currGroup.inputs.items(), })!, ); @@ -135,7 +170,7 @@ export class ToolbarPattern { return; } - this.listBehavior.prev(); + this.navigationBehavior.prev(); } /** Navigates to the widget targeted by a pointer event. */ @@ -143,45 +178,24 @@ export class ToolbarPattern { const item = this.inputs.getItem(_getEventTarget(e) as Element); if (item) { - this.listBehavior.goto(item); - this.select(); + this.navigationBehavior.goto(item); } } - select() { - const group = this.inputs.activeItem()?.group(); - - if (!group?.multi()) { - group?.inputs.items().forEach(i => this.listBehavior.deselect(i)); - } - - this.listBehavior.toggle(); - } - - constructor(readonly inputs: ToolbarInputs) { + constructor(readonly inputs: ToolbarInputs) { this.orientation = inputs.orientation; this.softDisabled = inputs.softDisabled; - this.listBehavior = new List({ + this.focusManager = new ListFocus({ ...inputs, - multi: () => true, focusMode: () => 'roving', - selectionMode: () => 'explicit', - typeaheadDelay: () => 0, // Toolbar widgets do not support typeahead. }); - } - - /** Returns a set of violations */ - validate(): string[] { - const violations: string[] = []; - const values = this.inputs.items().map(w => w.value()); - const duplicates = values.filter((val, idx) => values.indexOf(val) !== idx); - if (duplicates.length > 0) { - violations.push(`Duplicate value '${duplicates[0]}' detected inside ngToolbar.`); - } - - return violations; + this.navigationBehavior = new ListNavigation({ + ...inputs, + focusMode: () => 'roving', + focusManager: this.focusManager, + }); } /** Handles keydown events for the toolbar. */ @@ -193,7 +207,6 @@ export class ToolbarPattern { onPointerdown(event: PointerEvent) { this.hasBeenInteracted.set(true); - event.preventDefault(); } onFocusIn() { @@ -209,11 +222,10 @@ export class ToolbarPattern { /** * Sets the toolbar to its default initial state. * - * Sets the active index to the selected widget if one exists and is focusable. - * Otherwise, sets the active index to the first focusable widget. + * Sets the active index to the first focusable widget. */ setDefaultState() { - const firstItem = this.listBehavior.navigationBehavior.peekFirst({ + const firstItem = this.navigationBehavior.peekFirst({ items: this.inputs.items(), }); diff --git a/src/aria/toolbar/testing/toolbar-harness.spec.ts b/src/aria/toolbar/testing/toolbar-harness.spec.ts index daa6c5e9873a..b86d76aafe38 100644 --- a/src/aria/toolbar/testing/toolbar-harness.spec.ts +++ b/src/aria/toolbar/testing/toolbar-harness.spec.ts @@ -106,19 +106,19 @@ describe('ToolbarHarness', () => { @Component({ template: `
- - + +
- - - + + +
- - - + + +
`, @@ -128,4 +128,5 @@ class ToolbarHarnessTest { orientation = signal<'vertical' | 'horizontal'>('horizontal'); toolbarDisabled = signal(false); undoDisabled = signal(false); + undoPressed = signal(false); } diff --git a/src/aria/toolbar/toolbar-tokens.ts b/src/aria/toolbar/toolbar-tokens.ts index 3e3c083a8cb0..778482051ef4 100644 --- a/src/aria/toolbar/toolbar-tokens.ts +++ b/src/aria/toolbar/toolbar-tokens.ts @@ -10,6 +10,4 @@ import {InjectionToken} from '@angular/core'; import type {ToolbarWidgetGroup} from './toolbar-widget-group'; /** Token used to provide the `ToolbarWidgetGroup` directive. */ -export const TOOLBAR_WIDGET_GROUP = new InjectionToken>( - 'TOOLBAR_WIDGET_GROUP', -); +export const TOOLBAR_WIDGET_GROUP = new InjectionToken('TOOLBAR_WIDGET_GROUP'); diff --git a/src/aria/toolbar/toolbar-widget-group.ts b/src/aria/toolbar/toolbar-widget-group.ts index 7eeab108d668..9ef77b337e7b 100644 --- a/src/aria/toolbar/toolbar-widget-group.ts +++ b/src/aria/toolbar/toolbar-widget-group.ts @@ -16,7 +16,7 @@ import { contentChildren, afterRenderEffect, } from '@angular/core'; -import {ToolbarWidgetPattern, ToolbarWidgetGroupPattern, reportViolations} from '../private'; +import {ToolbarWidgetGroupPattern, reportViolations} from '../private'; import {Toolbar} from './toolbar'; import {ToolbarWidget} from './toolbar-widget'; import {TOOLBAR_WIDGET_GROUP} from './toolbar-tokens'; @@ -32,7 +32,7 @@ import {TOOLBAR_WIDGET_GROUP} from './toolbar-tokens'; exportAs: 'ngToolbarWidgetGroup', providers: [{provide: TOOLBAR_WIDGET_GROUP, useExisting: ToolbarWidgetGroup}], }) -export class ToolbarWidgetGroup { +export class ToolbarWidgetGroup { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); @@ -40,10 +40,10 @@ export class ToolbarWidgetGroup { readonly element = this._elementRef.nativeElement as HTMLElement; /** The parent Toolbar. */ - private readonly _toolbar = inject>(Toolbar, {optional: true}); + private readonly _toolbar = inject(Toolbar, {optional: true}); /** The list of child widgets within the group. */ - private readonly _widgets = contentChildren>(ToolbarWidget, {descendants: true}); + private readonly _widgets = contentChildren(ToolbarWidget, {descendants: true}); /** The parent Toolbar UIPattern. */ private readonly _toolbarPattern = computed(() => this._toolbar?._pattern); @@ -54,11 +54,8 @@ export class ToolbarWidgetGroup { /** The list of toolbar items within the group. */ private readonly _itemPatterns = () => this._widgets().map(w => w._pattern); - /** Whether the group allows multiple widgets to be selected. */ - readonly multi = input(false, {transform: booleanAttribute}); - /** The ToolbarWidgetGroup UIPattern. */ - readonly _pattern = new ToolbarWidgetGroupPattern, V>({ + readonly _pattern = new ToolbarWidgetGroupPattern({ ...this, items: this._itemPatterns, toolbar: this._toolbarPattern, diff --git a/src/aria/toolbar/toolbar-widget.ts b/src/aria/toolbar/toolbar-widget.ts index 4a626f3f57b4..17eb37737011 100644 --- a/src/aria/toolbar/toolbar-widget.ts +++ b/src/aria/toolbar/toolbar-widget.ts @@ -32,10 +32,10 @@ import type {ToolbarWidgetGroup} from './toolbar-widget-group'; * * The `ngToolbarWidget` directive should be applied to any native HTML element that acts * as an interactive widget within an `ngToolbar` or `ngToolbarWidgetGroup`. It enables - * keyboard navigation and selection within the toolbar. + * keyboard navigation within the toolbar. * * ```html - * * ``` @@ -56,7 +56,7 @@ import type {ToolbarWidgetGroup} from './toolbar-widget-group'; '[id]': '_pattern.id()', }, }) -export class ToolbarWidget implements OnInit, OnDestroy { +export class ToolbarWidget implements OnInit, OnDestroy { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); @@ -64,13 +64,13 @@ export class ToolbarWidget implements OnInit, OnDestroy { readonly element = this._elementRef.nativeElement as HTMLElement; /** The parent Toolbar. */ - private readonly _toolbar = inject>(Toolbar); + private readonly _toolbar = inject(Toolbar); /** A unique identifier for the widget. */ readonly id = input(inject(_IdGenerator).getId('ng-toolbar-widget-', true)); /** The parent Toolbar UIPattern. */ - readonly _toolbarPattern = computed>(() => this._toolbar._pattern); + readonly _toolbarPattern = computed(() => this._toolbar._pattern); /** Whether the widget is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); @@ -79,28 +79,20 @@ export class ToolbarWidget implements OnInit, OnDestroy { readonly hardDisabled = computed(() => this._pattern.disabled() && !this._toolbar.softDisabled()); /** The optional ToolbarWidgetGroup this widget belongs to. */ - readonly _group = inject>(TOOLBAR_WIDGET_GROUP, {optional: true}); - - /** The value associated with the widget. */ - readonly value = input.required(); + readonly _group = inject(TOOLBAR_WIDGET_GROUP, {optional: true}); /** Whether the widget is currently active (focused). */ readonly active = computed(() => this._pattern.active()); - /** Whether the widget is selected (only relevant in a selection group). */ - readonly selected = () => this._pattern.selected(); - - private readonly _groupPattern: SignalLike< - ToolbarWidgetGroupPattern, V> | undefined - > = () => this._group?._pattern; + private readonly _groupPattern: SignalLike = () => + this._group?._pattern; /** The ToolbarWidget UIPattern. */ - readonly _pattern = new ToolbarWidgetPattern({ + readonly _pattern = new ToolbarWidgetPattern({ ...this, group: this._groupPattern, toolbar: this._toolbarPattern, id: this.id, - value: this.value, element: () => this.element, }); diff --git a/src/aria/toolbar/toolbar.spec.ts b/src/aria/toolbar/toolbar.spec.ts index 5ffecc259df2..76aba4e00ab6 100644 --- a/src/aria/toolbar/toolbar.spec.ts +++ b/src/aria/toolbar/toolbar.spec.ts @@ -1,11 +1,4 @@ -import { - Component, - DebugElement, - Directive, - inject, - signal, - ChangeDetectionStrategy, -} from '@angular/core'; +import {Component, DebugElement, Directive, signal, ChangeDetectionStrategy} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {waitForMicrotasks} from '../private/testing/test-helpers'; @@ -594,67 +587,25 @@ describe('Toolbar', () => { }); }); - describe('Selection', () => { + describe('Interactions', () => { beforeEach(async () => await setupToolbar()); - it('should toggle the active item on Enter', async () => { - const item0 = getWidgetEl('item 0')!; - await click(item0); - await keydown('Enter'); - expect(item0.getAttribute('aria-pressed')).toBe('false'); - await keydown('Enter'); - expect(item0.getAttribute('aria-pressed')).toBe('true'); - }); - - it('should toggle the active item on Space', async () => { - const item0 = getWidgetEl('item 0')!; - await click(item0); - await keydown(' '); - expect(item0.getAttribute('aria-pressed')).toBe('false'); - await keydown(' '); - expect(item0.getAttribute('aria-pressed')).toBe('true'); - }); - - it('should toggle the active item on click', async () => { - const item0 = getWidgetEl('item 0')!; - await click(item0); - expect(item0.getAttribute('aria-pressed')).toBe('true'); - await click(item0); - expect(item0.getAttribute('aria-pressed')).toBe('false'); - }); - - it('should be able to select multiple items in the toolbar', async () => { - const item0 = getWidgetEl('item 0')!; + it('should set active item on click', async () => { const item1 = getWidgetEl('item 1')!; - await click(item0); await click(item1); - expect(item0.getAttribute('aria-pressed')).toBe('true'); - expect(item1.getAttribute('aria-pressed')).toBe('true'); + expect(document.activeElement).toBe(item1); }); - it('should not be able to select multiple items in a group', async () => { - const item2 = getWidgetEl('item 2')!; - const item3 = getWidgetEl('item 3')!; - await click(item2); - await click(item3); - expect(item2.getAttribute('aria-pressed')).toBe('false'); - expect(item3.getAttribute('aria-pressed')).toBe('true'); - }); + it('should not intercept Enter or Space key events for toolbar-level selection', async () => { + const item0 = getWidgetEl('item 0')!; + await click(item0); + expect(document.activeElement).toBe(item0); - it('should not select disabled items', async () => { - fixture.componentInstance.widgets[1].disabled.set(true); - await fixture.whenStable(); - const item1 = getWidgetEl('item 1')!; - await click(item1); - expect(item1.getAttribute('aria-pressed')).toBe('false'); - }); + await keydown('Enter'); + expect(document.activeElement).toBe(item0); - it('should not select items in a disabled group', async () => { - fixture.componentInstance.groups[0].disabled.set(true); - await fixture.whenStable(); - const item2 = getWidgetEl('item 2')!; - await click(item2); - expect(item2.getAttribute('aria-pressed')).toBe('false'); + await keydown(' '); + expect(document.activeElement).toBe(item0); }); }); @@ -719,17 +670,6 @@ describe('Toolbar', () => { await setupToolbar(); }); - it('should warn when duplicate values are detected inside ngToolbar', () => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - imports: [ToolbarWithDuplicateValues], - }); - const duplicateFixture = TestBed.createComponent(ToolbarWithDuplicateValues); - duplicateFixture.detectChanges(); - - expect(consoleSpy).toHaveBeenCalledWith("Duplicate value 'item0' detected inside ngToolbar."); - }); - it('should warn when ngToolbarWidgetGroup is outside ngToolbar', () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ @@ -743,6 +683,72 @@ describe('Toolbar', () => { ); }); }); + + describe('Form controls and embedded widgets', () => { + let formFixture: ComponentFixture; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [ToolbarWithFormControlsExample], + providers: [provideFakeDirectionality('ltr')], + }); + formFixture = TestBed.createComponent(ToolbarWithFormControlsExample); + fixture = formFixture as any; + await formFixture.whenStable(); + }); + + it('should not move toolbar focus on ArrowUp/ArrowDown on select in horizontal toolbar', async () => { + const selectEl = formFixture.debugElement.query(By.css('select')) + .nativeElement as HTMLElement; + await click(selectEl); + expect(document.activeElement).toBe(selectEl); + + await down(selectEl); + expect(document.activeElement).toBe(selectEl); + + await up(selectEl); + expect(document.activeElement).toBe(selectEl); + }); + + it('should navigate across toolbar on ArrowRight / ArrowLeft from select', async () => { + const selectEl = formFixture.debugElement.query(By.css('select')) + .nativeElement as HTMLElement; + const inputEl = formFixture.debugElement.query(By.css('input')).nativeElement as HTMLElement; + const buttons = formFixture.debugElement + .queryAll(By.css('button')) + .map(de => de.nativeElement as HTMLElement); + + await click(selectEl); + expect(document.activeElement).toBe(selectEl); + + await right(selectEl); + expect(document.activeElement).toBe(inputEl); + + await right(inputEl); + expect(document.activeElement).toBe(buttons[1]); // Italic button + + await left(buttons[1]); + expect(document.activeElement).toBe(inputEl); + + await left(inputEl); + expect(document.activeElement).toBe(selectEl); + + await left(selectEl); + expect(document.activeElement).toBe(buttons[0]); // Bold button + }); + + it('should not move toolbar focus on ArrowUp/ArrowDown on number input in horizontal toolbar', async () => { + const inputEl = formFixture.debugElement.query(By.css('input')).nativeElement as HTMLElement; + await click(inputEl); + expect(document.activeElement).toBe(inputEl); + + await down(inputEl); + expect(document.activeElement).toBe(inputEl); + + await up(inputEl); + expect(document.activeElement).toBe(inputEl); + }); + }); }); @Component({ @@ -756,44 +762,26 @@ describe('Toolbar', () => { > + [disabled]="widgets[0].disabled()">item 0 + [disabled]="widgets[1].disabled()">item 1
+ [disabled]="widgets[2].disabled()">item 2 + [disabled]="widgets[3].disabled()">item 3 + [disabled]="widgets[4].disabled()">item 4
+ [disabled]="widgets[5].disabled()">item 5 `, imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup], @@ -819,22 +807,19 @@ class ToolbarExample { @Directive({ selector: 'button[toolbar-button]', - hostDirectives: [{directive: ToolbarWidget, inputs: ['value', 'disabled']}], + hostDirectives: [{directive: ToolbarWidget, inputs: ['disabled']}], host: { type: 'button', class: 'example-button material-symbols-outlined', - '[aria-label]': 'widget.value()', }, }) -export class SimpleToolbarButton { - widget = inject(ToolbarWidget); -} +export class SimpleToolbarButton {} @Component({ template: `
- - + +
`, imports: [Toolbar, SimpleToolbarButton], @@ -846,7 +831,7 @@ class WrappedToolbarExample {} template: `
@for (item of items(); track item) { - + }
`, @@ -859,23 +844,28 @@ class ShuffledToolbarExample { @Component({ template: ` -
- - +
+ Widget Group Content
`, - imports: [Toolbar, ToolbarWidget], + imports: [ToolbarWidgetGroup], changeDetection: ChangeDetectionStrategy.Eager, }) -class ToolbarWithDuplicateValues {} +class ToolbarGroupOutsideToolbar {} @Component({ template: ` -
- Widget Group Content +
+ + + +
`, - imports: [ToolbarWidgetGroup], + imports: [Toolbar, ToolbarWidget], changeDetection: ChangeDetectionStrategy.Eager, }) -class ToolbarGroupOutsideToolbar {} +class ToolbarWithFormControlsExample {} diff --git a/src/aria/toolbar/toolbar.ts b/src/aria/toolbar/toolbar.ts index 0d8ad21be0cd..103d027e2aed 100644 --- a/src/aria/toolbar/toolbar.ts +++ b/src/aria/toolbar/toolbar.ts @@ -15,11 +15,10 @@ import { ElementRef, inject, input, - model, OnDestroy, signal, } from '@angular/core'; -import {ToolbarPattern, ToolbarWidgetPattern, SortedCollection, reportViolations} from '../private'; +import {ToolbarPattern, ToolbarWidgetPattern, SortedCollection} from '../private'; import {Directionality} from '@angular/cdk/bidi'; import type {ToolbarWidget} from './toolbar-widget'; @@ -30,13 +29,13 @@ import type {ToolbarWidget} from './toolbar-widget'; * * ```html *
- * - * + * + * * - *
- * - * - * + *
+ * + * + * *
*
* ``` @@ -57,7 +56,7 @@ import type {ToolbarWidget} from './toolbar-widget'; '(focusin)': '_pattern.onFocusIn()', }, }) -export class Toolbar implements OnDestroy { +export class Toolbar implements OnDestroy { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); @@ -65,13 +64,13 @@ export class Toolbar implements OnDestroy { readonly element = this._elementRef.nativeElement as HTMLElement; /** The collection of widgets in the toolbar. */ - readonly _collection = new SortedCollection>(); + readonly _collection = new SortedCollection(); /** Text direction. */ readonly textDirection = inject(Directionality).valueSignal; /** Sorted UIPatterns of the child widgets */ - readonly _itemPatterns = computed[]>(() => + readonly _itemPatterns = computed(() => this._collection.orderedItems().map(widget => widget._pattern), ); @@ -90,32 +89,19 @@ export class Toolbar implements OnDestroy { /** Whether focus should wrap when navigating. */ readonly wrap = input(true, {transform: booleanAttribute}); - /** The values of the selected widgets within the toolbar. */ - readonly value = model([]); - /** The toolbar UIPattern. */ - readonly _pattern: ToolbarPattern = new ToolbarPattern({ + readonly _pattern: ToolbarPattern = new ToolbarPattern({ ...this, items: this._itemPatterns, activeItem: signal(undefined), textDirection: this.textDirection, element: () => this._elementRef.nativeElement, getItem: e => this._getItem(e), - value: this.value, }); constructor() { afterRenderEffect({write: () => this._pattern.setDefaultStateEffect()}); - // Check for any violations after the DOM has been updated. - if (typeof ngDevMode === 'undefined' || ngDevMode) { - afterRenderEffect({ - read: () => { - reportViolations(this._pattern.validate(), this.element); - }, - }); - } - afterNextRender(() => { this._collection.startObserving(this.element); }); diff --git a/src/components-examples/aria/toolbar/index.ts b/src/components-examples/aria/toolbar/index.ts index 89582a318a90..5fe0a05c6e33 100644 --- a/src/components-examples/aria/toolbar/index.ts +++ b/src/components-examples/aria/toolbar/index.ts @@ -3,3 +3,4 @@ export {ToolbarBasicVerticalExample} from './toolbar-basic-vertical/toolbar-basi export {ToolbarConfigurableExample} from './toolbar-configurable/toolbar-configurable-example'; export {ToolbarRtlExample} from './toolbar-rtl/toolbar-rtl-example'; export {ToolbarHardDisabledExample} from './toolbar-hard-disabled/toolbar-hard-disabled-example'; +export {ToolbarSelectAndComboboxExample} from './toolbar-select-and-combobox/toolbar-select-and-combobox-example'; diff --git a/src/components-examples/aria/toolbar/simple-toolbar.ts b/src/components-examples/aria/toolbar/simple-toolbar.ts index 76094c8cb32a..cbf206f48e17 100644 --- a/src/components-examples/aria/toolbar/simple-toolbar.ts +++ b/src/components-examples/aria/toolbar/simple-toolbar.ts @@ -2,52 +2,97 @@ import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox'; import {Listbox, Option} from '@angular/aria/listbox'; import {ToolbarWidget} from '@angular/aria/toolbar'; import {Dir, Directionality} from '@angular/cdk/bidi'; -import {afterRenderEffect, Component, Directive, inject, signal, viewChild} from '@angular/core'; +import { + afterRenderEffect, + booleanAttribute, + Component, + computed, + Directive, + inject, + input, + signal, + viewChild, +} from '@angular/core'; import {OverlayModule} from '@angular/cdk/overlay'; @Directive({ selector: 'button[toolbar-button]', standalone: true, - hostDirectives: [{directive: ToolbarWidget, inputs: ['value', 'disabled']}], + hostDirectives: [{directive: ToolbarWidget, inputs: ['disabled']}], host: { type: 'button', class: 'example-button material-symbols-outlined', - '[aria-label]': 'widget.value()', }, }) -export class SimpleToolbarButton { - widget = inject(ToolbarWidget); -} +export class SimpleToolbarButton {} @Directive({ selector: 'button[toolbar-toggle-button]', standalone: true, - hostDirectives: [{directive: ToolbarWidget, inputs: ['value']}], + hostDirectives: [{directive: ToolbarWidget, inputs: ['disabled']}], host: { type: 'button', class: 'example-button material-symbols-outlined', - '[aria-pressed]': 'widget.selected()', - '[aria-label]': 'widget.value()', + '[attr.aria-pressed]': 'pressed()', + '(click)': 'toggle()', }, }) export class SimpleToolbarToggleButton { - widget = inject(ToolbarWidget); + readonly pressed = signal(false); + + toggle() { + this.pressed.set(!this.pressed()); + } +} + +@Directive({ + selector: '[toolbar-radio-group], [role="radiogroup"]', + standalone: true, +}) +export class SimpleToolbarRadioGroup { + readonly selected = signal(null); + + select(button: SimpleToolbarRadioButton) { + this.selected.set(button); + } } @Directive({ selector: 'button[toolbar-radio-button]', standalone: true, - hostDirectives: [{directive: ToolbarWidget, inputs: ['value', 'disabled']}], + hostDirectives: [{directive: ToolbarWidget, inputs: ['disabled']}], host: { role: 'radio', type: 'button', class: 'example-button material-symbols-outlined', - '[aria-checked]': 'widget.selected()', - '[aria-label]': 'widget.value()', + '[attr.aria-checked]': 'checked()', + '(click)': 'select()', }, }) export class SimpleToolbarRadioButton { - widget = inject(ToolbarWidget); + private readonly _group = inject(SimpleToolbarRadioGroup, {optional: true}); + readonly checkedInput = input(false, { + alias: 'checked', + transform: booleanAttribute, + }); + + private readonly _selfChecked = signal(null); + + readonly checked = computed(() => { + if (this._group) { + const selected = this._group.selected(); + if (selected !== null) { + return selected === this; + } + return this.checkedInput(); + } + return this._selfChecked() ?? this.checkedInput(); + }); + + select() { + this._selfChecked.set(true); + this._group?.select(this); + } } @Component({ 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 94908c0c3209..9ce939b82523 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 @@ -1,16 +1,16 @@
- - + +
- - - + + +
@@ -25,17 +25,17 @@ class="example-group" aria-label="Alignment options" > - - - + + +
- - - + + +
diff --git a/src/components-examples/aria/toolbar/toolbar-basic-horizontal/toolbar-basic-horizontal-example.ts b/src/components-examples/aria/toolbar/toolbar-basic-horizontal/toolbar-basic-horizontal-example.ts index 993b5a8a9a2d..b84e037a9c64 100644 --- a/src/components-examples/aria/toolbar/toolbar-basic-horizontal/toolbar-basic-horizontal-example.ts +++ b/src/components-examples/aria/toolbar/toolbar-basic-horizontal/toolbar-basic-horizontal-example.ts @@ -4,6 +4,7 @@ import { ToolbarCombobox, SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, } from '../simple-toolbar'; @@ -19,6 +20,7 @@ import { ToolbarCombobox, SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, ], }) diff --git a/src/components-examples/aria/toolbar/toolbar-basic-vertical/toolbar-basic-vertical-example.html b/src/components-examples/aria/toolbar/toolbar-basic-vertical/toolbar-basic-vertical-example.html index 72d7096f48f6..0d2e65a33093 100644 --- a/src/components-examples/aria/toolbar/toolbar-basic-vertical/toolbar-basic-vertical-example.html +++ b/src/components-examples/aria/toolbar/toolbar-basic-vertical/toolbar-basic-vertical-example.html @@ -6,16 +6,16 @@ aria-label="Vertical Toolbar Text Formatting Tools" >
- - + +
- - - + + +
@@ -26,9 +26,9 @@ class="example-group" aria-label="Alignment options" > - - - + + +
diff --git a/src/components-examples/aria/toolbar/toolbar-basic-vertical/toolbar-basic-vertical-example.ts b/src/components-examples/aria/toolbar/toolbar-basic-vertical/toolbar-basic-vertical-example.ts index 62afddef2fc4..98e37e394a7d 100644 --- a/src/components-examples/aria/toolbar/toolbar-basic-vertical/toolbar-basic-vertical-example.ts +++ b/src/components-examples/aria/toolbar/toolbar-basic-vertical/toolbar-basic-vertical-example.ts @@ -3,6 +3,7 @@ import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar' import { SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, } from '../simple-toolbar'; @@ -17,6 +18,7 @@ import { ToolbarWidgetGroup, SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, ], }) diff --git a/src/components-examples/aria/toolbar/toolbar-configurable/toolbar-configurable-example.html b/src/components-examples/aria/toolbar/toolbar-configurable/toolbar-configurable-example.html index 813a2b5ea388..141cd28a5486 100644 --- a/src/components-examples/aria/toolbar/toolbar-configurable/toolbar-configurable-example.html +++ b/src/components-examples/aria/toolbar/toolbar-configurable/toolbar-configurable-example.html @@ -46,18 +46,18 @@

Button

aria-label="Horizontal Toolbar Text Formatting Tools" >
- - + +
- - + -
@@ -76,13 +76,13 @@

Button

aria-label="Alignment options" [disabled]="isDisabled('Alignment options')" > - - - @@ -96,13 +96,13 @@

Button

aria-label="List options" [disabled]="isDisabled('List options')" > - - - diff --git a/src/components-examples/aria/toolbar/toolbar-configurable/toolbar-configurable-example.ts b/src/components-examples/aria/toolbar/toolbar-configurable/toolbar-configurable-example.ts index 0aed3ae7f226..875687ed4a07 100644 --- a/src/components-examples/aria/toolbar/toolbar-configurable/toolbar-configurable-example.ts +++ b/src/components-examples/aria/toolbar/toolbar-configurable/toolbar-configurable-example.ts @@ -8,6 +8,7 @@ import { ToolbarCombobox, SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, } from '../simple-toolbar'; @@ -23,6 +24,7 @@ import { ToolbarCombobox, SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, MatCheckboxModule, MatFormFieldModule, diff --git a/src/components-examples/aria/toolbar/toolbar-hard-disabled/toolbar-hard-disabled-example.html b/src/components-examples/aria/toolbar/toolbar-hard-disabled/toolbar-hard-disabled-example.html index 6e94bd79fae4..b906e7d9cdd3 100644 --- a/src/components-examples/aria/toolbar/toolbar-hard-disabled/toolbar-hard-disabled-example.html +++ b/src/components-examples/aria/toolbar/toolbar-hard-disabled/toolbar-hard-disabled-example.html @@ -6,16 +6,16 @@ aria-label="Horizontal Toolbar Text Formatting Tools" >
- - + +
- - - + + +
@@ -30,9 +30,9 @@ class="example-group" aria-label="Alignment options" > - - - + + + @@ -44,9 +44,9 @@ class="example-group" aria-label="List options" > - - - + + + diff --git a/src/components-examples/aria/toolbar/toolbar-hard-disabled/toolbar-hard-disabled-example.ts b/src/components-examples/aria/toolbar/toolbar-hard-disabled/toolbar-hard-disabled-example.ts index ffc1f14672ab..13831d19722a 100644 --- a/src/components-examples/aria/toolbar/toolbar-hard-disabled/toolbar-hard-disabled-example.ts +++ b/src/components-examples/aria/toolbar/toolbar-hard-disabled/toolbar-hard-disabled-example.ts @@ -4,6 +4,7 @@ import { ToolbarCombobox, SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, } from '../simple-toolbar'; @@ -19,6 +20,7 @@ import { ToolbarCombobox, SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, ], }) diff --git a/src/components-examples/aria/toolbar/toolbar-rtl/toolbar-rtl-example.html b/src/components-examples/aria/toolbar/toolbar-rtl/toolbar-rtl-example.html index 519f5e2eed1a..eba2f6c99225 100644 --- a/src/components-examples/aria/toolbar/toolbar-rtl/toolbar-rtl-example.html +++ b/src/components-examples/aria/toolbar/toolbar-rtl/toolbar-rtl-example.html @@ -1,16 +1,16 @@
- - + +
- - - + + +
@@ -25,17 +25,17 @@ class="example-group" aria-label="Alignment options" > - - - + + +
- - - + + +
diff --git a/src/components-examples/aria/toolbar/toolbar-rtl/toolbar-rtl-example.ts b/src/components-examples/aria/toolbar/toolbar-rtl/toolbar-rtl-example.ts index b259c40b626b..2a575188d677 100644 --- a/src/components-examples/aria/toolbar/toolbar-rtl/toolbar-rtl-example.ts +++ b/src/components-examples/aria/toolbar/toolbar-rtl/toolbar-rtl-example.ts @@ -5,6 +5,7 @@ import { ToolbarCombobox, SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, } from '../simple-toolbar'; @@ -21,6 +22,7 @@ import { ToolbarCombobox, SimpleToolbarButton, SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, SimpleToolbarToggleButton, ], }) diff --git a/src/components-examples/aria/toolbar/toolbar-select-and-combobox/toolbar-select-and-combobox-example.html b/src/components-examples/aria/toolbar/toolbar-select-and-combobox/toolbar-select-and-combobox-example.html new file mode 100644 index 000000000000..77e7fd2e97dc --- /dev/null +++ b/src/components-examples/aria/toolbar/toolbar-select-and-combobox/toolbar-select-and-combobox-example.html @@ -0,0 +1,91 @@ +
+
+ +
+ + +
+ + + + +
+ + + +
+ + + + + + + + + +
+ + +
+ + + + +
+ + +
+ + + + +
+ + + +
+
+ +
+

Selected Font: {{ selectedFont() }} | Size: {{ fontSize() }}px | Alignment: {{ alignment() }}

+
+
diff --git a/src/components-examples/aria/toolbar/toolbar-select-and-combobox/toolbar-select-and-combobox-example.ts b/src/components-examples/aria/toolbar/toolbar-select-and-combobox/toolbar-select-and-combobox-example.ts new file mode 100644 index 000000000000..0cbb2d66e912 --- /dev/null +++ b/src/components-examples/aria/toolbar/toolbar-select-and-combobox/toolbar-select-and-combobox-example.ts @@ -0,0 +1,34 @@ +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar'; +import { + ToolbarCombobox, + SimpleToolbarButton, + SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, + SimpleToolbarToggleButton, +} from '../simple-toolbar'; + +/** @title Toolbar with Select, Combobox, and Spinbutton Example */ +@Component({ + selector: 'toolbar-select-and-combobox-example', + templateUrl: 'toolbar-select-and-combobox-example.html', + styleUrl: '../toolbar-common.css', + imports: [ + FormsModule, + Toolbar, + ToolbarWidget, + ToolbarWidgetGroup, + ToolbarCombobox, + SimpleToolbarButton, + SimpleToolbarRadioButton, + SimpleToolbarRadioGroup, + SimpleToolbarToggleButton, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ToolbarSelectAndComboboxExample { + readonly selectedFont = signal('sans-serif'); + readonly fontSize = signal(16); + readonly alignment = signal('align left'); +} diff --git a/src/dev-app/aria-toolbar/toolbar-demo.html b/src/dev-app/aria-toolbar/toolbar-demo.html index b6e9553457de..3e1e2eadf7a6 100644 --- a/src/dev-app/aria-toolbar/toolbar-demo.html +++ b/src/dev-app/aria-toolbar/toolbar-demo.html @@ -16,6 +16,10 @@

Toolbar with Hard Disabled Items

Toolbar RTL

+
+

Toolbar with Select and Combobox

+ +

Configurable CDK Toolbar

diff --git a/src/dev-app/aria-toolbar/toolbar-demo.ts b/src/dev-app/aria-toolbar/toolbar-demo.ts index 08910d10af16..0ce08b04d1c4 100644 --- a/src/dev-app/aria-toolbar/toolbar-demo.ts +++ b/src/dev-app/aria-toolbar/toolbar-demo.ts @@ -13,6 +13,7 @@ import { ToolbarConfigurableExample, ToolbarRtlExample, ToolbarHardDisabledExample, + ToolbarSelectAndComboboxExample, } from '@angular/components-examples/aria/toolbar'; @Component({ @@ -23,6 +24,7 @@ import { ToolbarConfigurableExample, ToolbarRtlExample, ToolbarHardDisabledExample, + ToolbarSelectAndComboboxExample, ], styleUrl: './toolbar-demo.css', encapsulation: ViewEncapsulation.None,