diff --git a/goldens/aria/grid/index.api.md b/goldens/aria/grid/index.api.md index 4eb62d5cb7d4..7cee477daecf 100644 --- a/goldens/aria/grid/index.api.md +++ b/goldens/aria/grid/index.api.md @@ -39,7 +39,6 @@ export class GridCell { readonly disabled: _angular_core.InputSignalWithTransform; readonly element: HTMLElement; readonly id: _angular_core.InputSignal; - readonly orientation: _angular_core.InputSignal<"vertical" | "horizontal">; readonly _pattern: GridCellPattern; readonly role: _angular_core.InputSignal<"gridcell" | "columnheader" | "rowheader">; readonly rowIndex: _angular_core.InputSignal; @@ -49,9 +48,8 @@ export class GridCell { protected readonly _tabIndex: Signal; readonly tabindex: _angular_core.InputSignal; readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>; - readonly wrap: _angular_core.InputSignalWithTransform; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration; } diff --git a/goldens/aria/private/index.api.md b/goldens/aria/private/index.api.md index 096a41c60b6b..66fa2233a5bb 100644 --- a/goldens/aria/private/index.api.md +++ b/goldens/aria/private/index.api.md @@ -292,20 +292,19 @@ export class DeferredContentAware { } // @public -export interface GridCellInputs extends GridCell, Omit, 'focusMode' | 'items' | 'activeItem' | 'softDisabled' | 'element'> { +export interface GridCellInputs extends GridCell { colIndex: SignalLike; getWidget: (e: Element | null) => GridCellWidgetPattern | undefined; grid: SignalLike; row: SignalLike; rowIndex: SignalLike; - widgets: SignalLike; + widget: SignalLike; } // @public export class GridCellPattern implements GridCell { constructor(inputs: GridCellInputs); readonly active: SignalLike; - readonly activeWidget: WritableSignalLike; readonly anchor: SignalLike; readonly ariaColIndex: SignalLike; readonly ariaRowIndex: SignalLike; @@ -314,43 +313,33 @@ export class GridCellPattern implements GridCell { readonly disabled: SignalLike; readonly element: SignalLike; focus(): void; - readonly focusBehavior: ListFocus; readonly id: SignalLike; // (undocumented) readonly inputs: GridCellInputs; readonly isActivated: SignalLike; readonly isFocused: WritableSignalLike; - readonly keydown: SignalLike>; - readonly multiWidgetMode: SignalLike; - readonly navigationActivated: WritableSignalLike; - readonly navigationBehavior: ListNavigation; - readonly navigationDisabled: SignalLike; - readonly nextKey: SignalLike<"ArrowRight" | "ArrowLeft" | "ArrowDown">; onFocusIn(event: FocusEvent): void; onFocusOut(event: FocusEvent): void; onKeydown(event: KeyboardEvent): void; - readonly prevKey: SignalLike<"ArrowUp" | "ArrowRight" | "ArrowLeft">; readonly rowSpan: SignalLike; readonly selectable: SignalLike; readonly selected: WritableSignalLike; - readonly singleWidgetMode: SignalLike; - startNavigation(): void; - stopNavigation(): void; readonly tabIndex: SignalLike<-1 | 0>; - readonly widgetActivated: SignalLike; + readonly widget: SignalLike; widgetTabIndex(): -1 | 0; } // @public -export interface GridCellWidgetInputs extends Omit { +export interface GridCellWidgetInputs { cell: SignalLike; + disabled: SignalLike; element: SignalLike; focusTarget: SignalLike; widgetType: SignalLike<'simple' | 'complex' | 'editable'>; } // @public -export class GridCellWidgetPattern implements ListNavigationItem { +export class GridCellWidgetPattern { constructor(inputs: GridCellWidgetInputs); activate(event?: KeyboardEvent | FocusEvent): void; readonly active: SignalLike; @@ -358,8 +347,6 @@ export class GridCellWidgetPattern implements ListNavigationItem { readonly disabled: SignalLike; readonly element: SignalLike; focus(): void; - readonly id: SignalLike; - readonly index: SignalLike; // (undocumented) readonly inputs: GridCellWidgetInputs; readonly isActivated: WritableSignalLike; diff --git a/src/aria/grid/grid-cell.ts b/src/aria/grid/grid-cell.ts index 2148a58723dd..606e9bee0dd5 100644 --- a/src/aria/grid/grid-cell.ts +++ b/src/aria/grid/grid-cell.ts @@ -11,7 +11,7 @@ import { afterRenderEffect, booleanAttribute, computed, - contentChildren, + contentChild, Directive, ElementRef, inject, @@ -21,7 +21,7 @@ import { Renderer2, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; -import {GridCellPattern} from '../private'; +import {GridCellPattern, GridCellWidgetPattern} from '../private'; import {GridCellWidget} from './grid-cell-widget'; import {GRID_CELL, GRID_ROW} from './grid-tokens'; @@ -55,12 +55,12 @@ export class GridCell { /** Whether the cell is currently active (focused). */ readonly active = computed(() => this._pattern.active()); - /** The widgets contained within this cell, if any. */ - private readonly _widgets = contentChildren(GridCellWidget, {descendants: true}); + /** The widget contained within this cell, if any. */ + private readonly _widget = contentChild(GridCellWidget, {descendants: true}); /** The UI pattern for the widget in this cell. */ - private readonly _widgetPatterns: Signal = computed(() => - this._widgets().map(w => w._pattern), + private readonly _widgetPattern: Signal = computed( + () => this._widget()?._pattern, ); /** The parent row. */ @@ -96,12 +96,6 @@ export class GridCell { /** Whether the cell is selectable. */ readonly selectable = input(true); - /** Orientation of the widgets in the cell. */ - readonly orientation = input<'vertical' | 'horizontal'>('horizontal'); - - /** Whether widgets navigation wraps. */ - readonly wrap = input(true, {transform: booleanAttribute}); - /** The tabindex override. */ readonly tabindex = input(); @@ -118,7 +112,7 @@ export class GridCell { ...this, grid: this._row._gridPattern, row: () => this._row._pattern, - widgets: this._widgetPatterns, + widget: this._widgetPattern, getWidget: e => this._getWidget(e), element: () => this.element, }); @@ -160,11 +154,13 @@ export class GridCell { /** Gets the cell widget pattern for a given element. */ private _getWidget(element: Element | null | undefined): any | undefined { let target = element; + const widget = this._widgetPattern(); + + if (!widget) return undefined; while (target) { - const pattern = this._widgetPatterns().find(w => w.element() === target); - if (pattern) { - return pattern; + if (widget.element() === target) { + return widget; } target = target.parentElement?.closest('[ngGridCellWidget]'); diff --git a/src/aria/private/grid/cell.ts b/src/aria/private/grid/cell.ts index 946d15def1e9..121295985649 100644 --- a/src/aria/private/grid/cell.ts +++ b/src/aria/private/grid/cell.ts @@ -6,13 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import {KeyboardEventManager} from '../behaviors/event-manager'; -import {ListFocus} from '../behaviors/list-focus/list-focus'; -import {ListNavigation, ListNavigationInputs} from '../behaviors/list-navigation/list-navigation'; import { computed, signal, - linkedSignal, SignalLike, WritableSignalLike, } from '../behaviors/signal-like/signal-like'; @@ -22,21 +18,15 @@ import type {GridRowPattern} from './row'; import {GridCellWidgetPattern} from './widget'; /** The inputs for the `GridCellPattern`. */ -export interface GridCellInputs - extends - GridCell, - Omit< - ListNavigationInputs, - 'focusMode' | 'items' | 'activeItem' | 'softDisabled' | 'element' - > { +export interface GridCellInputs extends GridCell { /** The `GridPattern` that this cell belongs to. */ grid: SignalLike; /** The `GridRowPattern` that this cell belongs to. */ row: SignalLike; - /** The widget patterns contained within this cell, if any. */ - widgets: SignalLike; + /** The widget pattern contained within this cell, if any. */ + widget: SignalLike; /** The index of this cell's row within the grid. */ rowIndex: SignalLike; @@ -107,139 +97,26 @@ export class GridCellPattern implements GridCell { /** The tab index for the cell. If the cell contains a widget, the cell's tab index is -1. */ readonly tabIndex: SignalLike<-1 | 0> = computed(() => { - if (this.singleWidgetMode() || this.navigationActivated()) { + if (this.inputs.widget()) { return -1; } return this._tabIndex(); }); - // Single/Multi Widget Navigation Setup - - /** Whether the cell contains a single widget. */ - readonly singleWidgetMode: SignalLike = computed( - () => this.inputs.widgets().length === 1, - ); - - /** Whether the cell contains multiple widgets. */ - readonly multiWidgetMode: SignalLike = computed(() => this.inputs.widgets().length > 1); - - /** Whether navigation between widgets is disabled. */ - readonly navigationDisabled: SignalLike = computed( - () => !this.multiWidgetMode() || !this.active() || this.inputs.disabled(), - ); - - /** The focus behavior for the widgets in the cell. */ - readonly focusBehavior: ListFocus; - - /** The navigation behavior for the widgets in the cell. */ - readonly navigationBehavior: ListNavigation; - - /** The currently active widget in the cell. */ - readonly activeWidget: WritableSignalLike = linkedSignal(() => - this.inputs.widgets().length > 0 ? this.inputs.widgets()[0] : undefined, - ); - - /** Whether navigation between widgets is activated. */ - readonly navigationActivated: WritableSignalLike = signal(false); - - /** Whether any widget within the cell is activated. */ - readonly widgetActivated: SignalLike = computed(() => - this.inputs.widgets().some(w => w.isActivated()), - ); + /** The widget in the cell. */ + readonly widget: SignalLike = () => this.inputs.widget(); /** Whether the cell or widget inside the cell is activated. */ - readonly isActivated: SignalLike = computed( - () => this.navigationActivated() || this.widgetActivated(), - ); - - /** The key used to navigate to the previous widget. */ - readonly prevKey = computed(() => { - if (this.inputs.orientation() === 'vertical') { - return 'ArrowUp'; - } - return this.inputs.textDirection() === 'rtl' ? 'ArrowRight' : 'ArrowLeft'; - }); - - /** The key used to navigate to the next widget. */ - readonly nextKey = computed(() => { - if (this.inputs.orientation() === 'vertical') { - return 'ArrowDown'; - } - return this.inputs.textDirection() === 'rtl' ? 'ArrowLeft' : 'ArrowRight'; - }); - - /** The keyboard event manager for the cell. */ - readonly keydown = computed(() => { - const manager = new KeyboardEventManager(); - - // Before start list navigation. - if (!this.navigationActivated()) { - manager.on('Enter', () => this.startNavigation()); - return manager; - } - - // Start list navigation. - manager - .on('Escape', () => this.stopNavigation()) - .on( - this.prevKey(), - () => this._advance(() => this.navigationBehavior.prev({focusElement: false})), - {ignoreRepeat: false}, - ) - .on( - this.nextKey(), - () => this._advance(() => this.navigationBehavior.next({focusElement: false})), - {ignoreRepeat: false}, - ) - .on('Home', () => this._advance(() => this.navigationBehavior.next({focusElement: false}))) - .on('End', () => this._advance(() => this.navigationBehavior.next({focusElement: false}))); - - return manager; - }); + readonly isActivated: SignalLike = computed(() => this.widget()?.isActivated() ?? false); constructor(readonly inputs: GridCellInputs) { this.selected = inputs.selected; - - const listNavigationInputs: ListNavigationInputs = { - ...inputs, - items: inputs.widgets, - activeItem: this.activeWidget, - disabled: this.navigationDisabled, - focusMode: () => 'roving', - softDisabled: () => true, - }; - - this.focusBehavior = new ListFocus(listNavigationInputs); - this.navigationBehavior = new ListNavigation({ - ...listNavigationInputs, - focusManager: this.focusBehavior, - }); } /** Handles keydown events for the cell. */ onKeydown(event: KeyboardEvent): void { - if (this.disabled() || this.inputs.widgets().length === 0) return; - - // No navigation needed if single widget. - if (this.singleWidgetMode()) { - this.activeWidget()!.onKeydown(event); - return; - } - - // Focus is on the cell before the navigation starts. - if (!this.navigationActivated()) { - this.keydown().handle(event); - return; - } - - // Widget activate state can be changed during the widget keydown handling. - const widgetActivated = this.widgetActivated(); - - this.activeWidget()!.onKeydown(event); - - if (!widgetActivated) { - this.keydown().handle(event); - } + if (this.disabled()) return; + this.widget()?.onKeydown(event); } /** Handles focusin events for the cell. */ @@ -252,19 +129,6 @@ export class GridCellPattern implements GridCell { // Pass down focusin event to the widget. widget.onFocusIn(event); - - // Update internal states if the widget(or anything within the widget) is - // receiving focus by tabbing, pointer, or any programmatic control. - - // Update current active widget. - if (widget !== this.activeWidget()) { - this.navigationBehavior.goto(widget, {focusElement: false}); - } - - // Start widget navigation if multi widget. - if (this.multiWidgetMode()) { - this.navigationActivated.set(true); - } } /** Handles focusout events for the cell. */ @@ -279,14 +143,13 @@ export class GridCellPattern implements GridCell { if (this.element().contains(focusTarget)) return; this.isFocused.set(false); - // Reset navigation state when focus leaving cell. - this.navigationActivated.set(false); } /** Focuses the cell or the active widget. */ focus(): void { - if (this.singleWidgetMode()) { - this.activeWidget()?.focus(); + const widget = this.widget(); + if (widget) { + widget.focus(); } else { this.element().focus(); } @@ -294,33 +157,6 @@ export class GridCellPattern implements GridCell { /** Gets the tab index for the widget within the cell. */ widgetTabIndex(): -1 | 0 { - if (this.singleWidgetMode()) { - return this._tabIndex(); - } - return this.navigationActivated() ? 0 : -1; - } - - /** Starts navigation between widgets. */ - startNavigation(): void { - if (this.navigationActivated()) return; - - this.navigationActivated.set(true); - this.navigationBehavior.first(); - } - - /** Stops navigation between widgets and restores focus to the cell. */ - stopNavigation(): void { - if (!this.navigationActivated()) return; - - this.navigationActivated.set(false); - this.element().focus(); - } - - /** Executes a navigation operation and focuses the new active widget. */ - private _advance(op: () => boolean): void { - const success = op(); - if (success) { - this.activeWidget()?.focus(); - } + return this._tabIndex(); } } diff --git a/src/aria/private/grid/grid.spec.ts b/src/aria/private/grid/grid.spec.ts index 906e2261a01a..68dc3c3e950b 100644 --- a/src/aria/private/grid/grid.spec.ts +++ b/src/aria/private/grid/grid.spec.ts @@ -57,14 +57,13 @@ function createClickEvent(element: HTMLElement, mods?: ModifierKeys): PointerEve } interface TestWidgetData { - id?: string; widgetType?: 'simple' | 'complex' | 'editable'; disabled?: boolean; } interface TestCellData { id?: string; - widgets?: TestWidgetData[]; + widget?: TestWidgetData; selectable?: boolean; disabled?: boolean; rowSpan?: number; @@ -95,8 +94,7 @@ function createGridRows(grid: GridPattern, data: TestRowData[]) { element: signal(document.createElement('div')), grid: signal(grid), row: signal(row), - widgets: signal([]), - wrap: signal(false), + widget: signal(undefined), rowIndex: signal(cellData.rowIndex), colIndex: signal(cellData.colIndex), selectable: signal(cellData.selectable ?? true), @@ -104,27 +102,23 @@ function createGridRows(grid: GridPattern, data: TestRowData[]) { rowSpan: signal(cellData.rowSpan ?? 1), colSpan: signal(cellData.colSpan ?? 1), selected: signal(cellData.selected ?? false), - orientation: signal('vertical'), - textDirection: signal('ltr'), getWidget: (el: Element | null) => builtWidgets.find(w => w.element() === el), }; const cell = new GridCellPattern(cellInputs); - const cellWidgets = (cellData.widgets ?? []).map((widgetData, widgetIndex) => { + if (cellData.widget) { const widgetInputs: TestGridCellWidgetInputs = { - id: signal(widgetData.id ?? `widget-${rowIndex}-${colIndex}-${widgetIndex}`), cell: signal(cell), element: signal(document.createElement('div')), - widgetType: signal(widgetData.widgetType ?? 'simple'), + widgetType: signal(cellData.widget.widgetType ?? 'simple'), focusTarget: signal(undefined), - disabled: signal(widgetData.disabled ?? false), + disabled: signal(cellData.widget.disabled ?? false), }; const widget = new GridCellWidgetPattern(widgetInputs); builtWidgets.push(widget); - return widget; - }); + cellInputs.widget.set(widget); + } - cellInputs.widgets.set(cellWidgets); return cell; }); @@ -183,9 +177,8 @@ describe('Grid', () => { }); it('should have correct initial properties for a widget pattern', () => { - const {grid} = createGrid([{cells: [{widgets: [{id: 'test-id'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; - expect(widget.id()).toBe('test-id'); + const {grid} = createGrid([{cells: [{widget: {}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; expect(widget.isActivated()).toBe(false); expect(widget.disabled()).toBe(false); }); @@ -193,8 +186,8 @@ describe('Grid', () => { it('should compute element and widgetHost correctly', () => { const element = document.createElement('div'); const focusTarget = document.createElement('button'); - const {grid} = createGrid([{cells: [{widgets: [{widgetType: 'simple'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const {grid} = createGrid([{cells: [{widget: {widgetType: 'simple'}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; const widgetInputs = widget.inputs as TestGridCellWidgetInputs; widgetInputs.element.set(element); widgetInputs.focusTarget.set(focusTarget); @@ -206,20 +199,10 @@ describe('Grid', () => { expect(widget.widgetHost()).toBe(element); }); - it('should compute index correctly', () => { - const {grid} = createGrid( - [{cells: [{widgets: [{id: 'widget-1'}, {id: 'widget-2'}]}]}], - gridInputs, - ); - const widgets = grid.cells()[0][0].inputs.widgets(); - expect(widgets[0].index()).toBe(0); - expect(widgets[1].index()).toBe(1); - }); - it('should be disabled if cell is disabled', () => { - const {grid} = createGrid([{cells: [{widgets: [{disabled: false}]}]}], gridInputs); + const {grid} = createGrid([{cells: [{widget: {disabled: false}}]}], gridInputs); const cell = grid.cells()[0][0]; - const widget = cell.inputs.widgets()[0]; + const widget = cell.inputs.widget()!; const cellInputs = cell.inputs as TestGridCellInputs; cellInputs.disabled.set(true); @@ -228,8 +211,8 @@ describe('Grid', () => { describe('Activation', () => { it('should activate and deactivate manually', () => { - const {grid} = createGrid([{cells: [{widgets: [{widgetType: 'complex'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const {grid} = createGrid([{cells: [{widget: {widgetType: 'complex'}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; widget.activate(); expect(widget.isActivated()).toBe(true); widget.deactivate(); @@ -237,8 +220,8 @@ describe('Grid', () => { }); it('should not activate if widgetType is simple', () => { - const {grid} = createGrid([{cells: [{widgets: [{widgetType: 'simple'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const {grid} = createGrid([{cells: [{widget: {widgetType: 'simple'}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; widget.activate(); expect(widget.isActivated()).toBe(false); }); @@ -247,8 +230,8 @@ describe('Grid', () => { const host = document.createElement('div'); const inner = document.createElement('button'); host.appendChild(inner); - const {grid} = createGrid([{cells: [{widgets: [{widgetType: 'complex'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const {grid} = createGrid([{cells: [{widget: {widgetType: 'complex'}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; (widget.inputs as TestGridCellWidgetInputs).focusTarget.set(host); widget.onFocusIn({target: inner} as unknown as FocusEvent); @@ -257,8 +240,8 @@ describe('Grid', () => { it('should deactivate on focusout if focus leaves widgetHost', () => { const host = document.createElement('div'); - const {grid} = createGrid([{cells: [{widgets: [{widgetType: 'complex'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const {grid} = createGrid([{cells: [{widget: {widgetType: 'complex'}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; (widget.inputs as TestGridCellWidgetInputs).focusTarget.set(host); widget.activate(); @@ -269,41 +252,41 @@ describe('Grid', () => { describe('Keyboard Events', () => { it('should activate on Enter for complex widget', () => { - const {grid} = createGrid([{cells: [{widgets: [{widgetType: 'complex'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const {grid} = createGrid([{cells: [{widget: {widgetType: 'complex'}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; widget.onKeydown(enter()); expect(widget.isActivated()).toBe(true); }); it('should deactivate on Escape when activated', () => { - const {grid} = createGrid([{cells: [{widgets: [{widgetType: 'complex'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const {grid} = createGrid([{cells: [{widget: {widgetType: 'complex'}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; widget.activate(); widget.onKeydown(escape()); expect(widget.isActivated()).toBe(false); }); it('should deactivate on Enter for editable widget when activated', () => { - const {grid} = createGrid([{cells: [{widgets: [{widgetType: 'editable'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const {grid} = createGrid([{cells: [{widget: {widgetType: 'editable'}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; widget.activate(); widget.onKeydown(enter()); expect(widget.isActivated()).toBe(false); }); it('should activate on character key for editable widget', () => { - const {grid} = createGrid([{cells: [{widgets: [{widgetType: 'editable'}]}]}], gridInputs); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const {grid} = createGrid([{cells: [{widget: {widgetType: 'editable'}}]}], gridInputs); + const widget = grid.cells()[0][0].inputs.widget()!; widget.onKeydown(a()); expect(widget.isActivated()).toBe(true); }); it('should not activate if disabled', () => { const {grid} = createGrid( - [{cells: [{widgets: [{widgetType: 'complex', disabled: true}]}]}], + [{cells: [{widget: {widgetType: 'complex', disabled: true}}]}], gridInputs, ); - const widget = grid.cells()[0][0].inputs.widgets()[0]; + const widget = grid.cells()[0][0].inputs.widget()!; widget.onKeydown(enter()); expect(widget.isActivated()).toBe(false); }); @@ -370,123 +353,25 @@ describe('Grid', () => { expect(cell.tabIndex()).toBe(0); }); - it('should be -1 if navigation is activated', () => { - const {grid} = createGrid([{cells: [{widgets: [{}, {}]}]}], gridInputs); + it('should be -1 if cell contains a widget', () => { + const {grid} = createGrid([{cells: [{widget: {}}]}], gridInputs); const cell = grid.cells()[0][0]; grid.setDefaultStateEffect(); - cell.navigationActivated.set(true); expect(cell.tabIndex()).toBe(-1); }); - - it('should be -1 if in single widget mode', () => { - const {grid} = createGrid([{cells: [{widgets: [{}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - grid.setDefaultStateEffect(); - expect(cell.tabIndex()).toBe(-1); - }); - }); - - describe('Widget Modes', () => { - it('should detect single widget mode', () => { - const {grid} = createGrid([{cells: [{widgets: [{}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - expect(cell.singleWidgetMode()).toBe(true); - expect(cell.multiWidgetMode()).toBe(false); - }); - - it('should detect multi widget mode', () => { - const {grid} = createGrid([{cells: [{widgets: [{}, {}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - expect(cell.singleWidgetMode()).toBe(false); - expect(cell.multiWidgetMode()).toBe(true); - }); - }); - - describe('Navigation', () => { - it('should start and stop navigation', () => { - const {grid} = createGrid([{cells: [{widgets: [{}, {}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - cell.startNavigation(); - expect(cell.navigationActivated()).toBe(true); - cell.stopNavigation(); - expect(cell.navigationActivated()).toBe(false); - }); - - it('should focus element on stop navigation', () => { - const element = document.createElement('div'); - spyOn(element, 'focus'); - const {grid} = createGrid([{cells: [{widgets: [{}, {}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - (cell.inputs as TestGridCellInputs).element.set(element); - - cell.startNavigation(); - cell.stopNavigation(); - expect(element.focus).toHaveBeenCalled(); - }); }); describe('Keyboard Events', () => { - it('should start navigation on Enter in multi-widget mode', () => { - const {grid} = createGrid([{cells: [{widgets: [{}, {}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - cell.onKeydown(enter()); - expect(cell.navigationActivated()).toBe(true); - }); - - it('should stop navigation on Escape', () => { - const {grid} = createGrid([{cells: [{widgets: [{}, {}]}]}], gridInputs); + it('should delegate to widget', () => { + const {grid} = createGrid([{cells: [{widget: {}}]}], gridInputs); const cell = grid.cells()[0][0]; - cell.startNavigation(); - cell.onKeydown(escape()); - expect(cell.navigationActivated()).toBe(false); - }); - - it('should delegate to active widget in single widget mode', () => { - const {grid} = createGrid([{cells: [{widgets: [{}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - const widget = cell.inputs.widgets()[0]; + const widget = cell.inputs.widget()!; spyOn(widget, 'onKeydown'); const event = enter(); cell.onKeydown(event); expect(widget.onKeydown).toHaveBeenCalledWith(event); }); - - it('should navigate widgets on arrow keys during navigation', () => { - const {grid} = createGrid([{cells: [{widgets: [{}, {}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - const widget = cell.inputs.widgets()[1]; - spyOn(widget, 'focus'); - - grid.gridBehavior.focusBehavior.focusCell(cell); - cell.startNavigation(); - cell.onKeydown(down()); - expect(cell.activeWidget()).toBe(widget); - expect(widget.focus).toHaveBeenCalled(); - }); - }); - - describe('Focus Events', () => { - it('should update active widget and start navigation on focusin', () => { - const {grid} = createGrid([{cells: [{widgets: [{}, {}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - const w2 = cell.inputs.widgets()[1]; - spyOn(w2, 'onFocusIn'); - - grid.gridBehavior.focusBehavior.focusCell(cell); - cell.onFocusIn({target: w2.element()} as unknown as FocusEvent); - expect(cell.activeWidget()).toBe(w2); - expect(cell.navigationActivated()).toBe(true); - expect(w2.onFocusIn).toHaveBeenCalled(); - }); - - it('should reset navigation state on focusout', () => { - const {grid} = createGrid([{cells: [{widgets: [{}, {}]}]}], gridInputs); - const cell = grid.cells()[0][0]; - cell.startNavigation(); - cell.onFocusOut({target: document.createElement('div')} as unknown as FocusEvent); - expect(cell.navigationActivated()).toBe(false); - }); }); }); diff --git a/src/aria/private/grid/widget.ts b/src/aria/private/grid/widget.ts index f51278638e71..779e03640b73 100644 --- a/src/aria/private/grid/widget.ts +++ b/src/aria/private/grid/widget.ts @@ -7,7 +7,6 @@ */ import {KeyboardEventManager, Modifier} from '../behaviors/event-manager'; -import {ListNavigationItem} from '../behaviors/list-navigation/list-navigation'; import { SignalLike, computed, @@ -17,7 +16,10 @@ import { import type {GridCellPattern} from './cell'; /** The inputs for the `GridCellWidgetPattern`. */ -export interface GridCellWidgetInputs extends Omit { +export interface GridCellWidgetInputs { + /** Whether the widget is disabled. */ + disabled: SignalLike; + /** The `GridCellPattern` that this widget belongs to. */ cell: SignalLike; @@ -32,10 +34,7 @@ export interface GridCellWidgetInputs extends Omit } /** The UI pattern for a widget inside a grid cell. */ -export class GridCellWidgetPattern implements ListNavigationItem { - /** A unique identifier for the widget. */ - readonly id: SignalLike = () => this.inputs.id(); - +export class GridCellWidgetPattern { /** The html element that should receive focus. */ readonly element: SignalLike = () => this.inputs.element(); @@ -44,11 +43,6 @@ export class GridCellWidgetPattern implements ListNavigationItem { () => this.inputs.focusTarget() ?? this.element(), ); - /** The index of the widget within the cell. */ - readonly index: SignalLike = computed(() => - this.inputs.cell().inputs.widgets().indexOf(this), - ); - /** Whether the widget is disabled. */ readonly disabled: SignalLike = computed( () => this.inputs.disabled() || this.inputs.cell().disabled(), @@ -57,9 +51,9 @@ export class GridCellWidgetPattern implements ListNavigationItem { /** The tab index for the widget. */ readonly tabIndex: SignalLike<-1 | 0> = computed(() => this.inputs.cell().widgetTabIndex()); - /** Whether the widget is the active item in the widget list. */ + /** Whether the widget is the active widget in the cell. */ readonly active: SignalLike = computed( - () => this.inputs.cell().active() && this.inputs.cell().activeWidget() === this, + () => this.inputs.cell().active() && this.inputs.cell().widget() === this, ); /** Whether the widget is currently activated. */ diff --git a/src/components-examples/aria/grid/grid-table/grid-table-example.html b/src/components-examples/aria/grid/grid-table/grid-table-example.html index 637e79dff9ce..47df385faaee 100644 --- a/src/components-examples/aria/grid/grid-table/grid-table-example.html +++ b/src/components-examples/aria/grid/grid-table/grid-table-example.html @@ -72,26 +72,14 @@ -
-
- -
- +
+
diff --git a/src/components-examples/aria/grid/grid-table/grid-table-example.ts b/src/components-examples/aria/grid/grid-table/grid-table-example.ts index 30f10fa539f6..55de8f359bec 100644 --- a/src/components-examples/aria/grid/grid-table/grid-table-example.ts +++ b/src/components-examples/aria/grid/grid-table/grid-table-example.ts @@ -95,16 +95,6 @@ export class GridTableExample { this.tasks().forEach(t => t.selected.set(checked)); } - addTag(event: KeyboardEvent | FocusEvent | undefined, task: TaskRow, inputEl: HTMLInputElement) { - if (event instanceof KeyboardEvent && event.key === 'Enter') { - const value = inputEl.value; - if (value.length > 0) { - task.tags.set([...task.tags(), value]); - } - } - inputEl.value = ''; - } - private _createRows(): TaskRow[] { return [ {