/
githubmirror
/
components
Обзор
Документация
Войти
/
githubmirror
/
components
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/aria/private/grid/widget.ts
198 строк
6 KB
Cheng-Hsuan Tsai
refactor(aria/grid): consolidate widget focus logic and activation handling (#33203)
06 май 2026, 00:43
Не верифицирован
06 май 2026, 00:43
f4e9a87
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {KeyboardEventManager, Modifier} from '../behaviors/event-manager'; import { SignalLike, computed, signal, WritableSignalLike, } from '../behaviors/signal-like/signal-like'; import type {GridCellPattern} from './cell'; import {ElementResolver, resolveElement} from '../utils/element-resolver'; /** The inputs for the `GridCellWidgetPattern`. */ export interface GridCellWidgetInputs { /** Whether the widget is disabled. */ disabled: SignalLike<boolean>; /** The `GridCellPattern` that this widget belongs to. */ cell: SignalLike<GridCellPattern>; /** The html element that should receive focus. */ element: SignalLike<HTMLElement>; /** The type of widget, which determines how it is activated. */ widgetType: SignalLike<'simple' | 'complex' | 'editable'>; /** The element that will receive focus when the widget is activated. */ focusTarget: SignalLike<ElementResolver<HTMLElement>>; /** Callback hook used to notify parents or directives upon interaction. */ onActivate?: (event: KeyboardEvent | FocusEvent | undefined) => void; /** Callback hook used to notify parents or directives upon exit. */ onDeactivate?: (event: KeyboardEvent | FocusEvent | undefined) => void; } /** The UI pattern for a widget inside a grid cell. */ export class GridCellWidgetPattern { /** The html element that should receive focus. */ readonly element: SignalLike<HTMLElement> = () => this.inputs.element(); /** The element that should receive focus. */ readonly widgetHost: SignalLike<HTMLElement> = () => resolveElement(this.inputs.focusTarget(), this.element()) ?? this.element(); /** Whether the widget is disabled. */ readonly disabled: SignalLike<boolean> = computed( () => this.inputs.disabled() || this.inputs.cell().disabled(), ); /** The tab index for the widget. */ readonly tabIndex: SignalLike<-1 | 0> = computed(() => { if (this.inputs.focusTarget()) { return -1; } return this.inputs.cell().widgetTabIndex(); }); /** Whether the widget is the active widget in the cell. */ readonly active: SignalLike<boolean> = computed( () => this.inputs.cell().active() && this.inputs.cell().widget() === this, ); /** Whether the widget is currently activated. */ readonly isActivated: WritableSignalLike<boolean> = signal(false); /** The last event that caused the widget to be activated. */ readonly lastActivateEvent: WritableSignalLike<KeyboardEvent | FocusEvent | undefined> = signal(undefined); /** The last event that caused the widget to be deactivated. */ readonly lastDeactivateEvent: WritableSignalLike<KeyboardEvent | FocusEvent | undefined> = signal(undefined); /** The keyboard event manager for the widget. */ readonly keydown = computed(() => { const manager = new KeyboardEventManager(); // Simple widgets emit notification on interaction without capturing event flow if (this.inputs.widgetType() === 'simple') { return manager .on('Enter', e => this.inputs.onActivate?.(e), { preventDefault: false, stopPropagation: false, }) .on(' ', e => this.inputs.onActivate?.(e), { preventDefault: false, stopPropagation: false, }); } // If a widget is activated, only listen to events that exits activate state. if (this.isActivated()) { manager.on('Escape', e => this.deactivate(e)); if (this.inputs.widgetType() === 'editable') { manager.on('Enter', e => this.deactivate(e)); } return manager; } // Enter key is used to activate widget for both complex and editable type. manager.on('Enter', e => this.activate(e)); if (this.inputs.widgetType() === 'editable') { manager.on([Modifier.Shift, Modifier.None], /^[a-zA-Z0-9]$/, e => this.activate(e), { preventDefault: false, }); } return manager; }); constructor(readonly inputs: GridCellWidgetInputs) {} /** Handles keydown events for the widget. */ onKeydown(event: KeyboardEvent): void { if (this.disabled()) return; this.keydown().handle(event); } /** Handles focusin events for the widget. */ onFocusIn(event: FocusEvent): void { // Simple widget does not have activate state. if (this.inputs.widgetType() === 'simple') return; // Set activate state if the focus is inside of widget. const focusTarget = event.target as Element; if (this.widgetHost().contains(focusTarget) && this.widgetHost() !== focusTarget) { this.activate(event); } } /** Handles focusout events for the widget. */ onFocusOut(event: FocusEvent): void { const focusTarget = event.relatedTarget as Element; if (this.widgetHost().contains(focusTarget)) return; // Reset states when focus leaving widget. this.deactivate(event); } /** Focuses the widget's host element. */ focus(): void { this.widgetHost().focus(); } /** Side-effect executed whenever the widget activates. Runs in the write phase. */ activationEffect(): void { if (this.isActivated()) { const event = this.lastActivateEvent(); this.inputs.onActivate?.(event); // Only automatically redirect focus if explicit configuration was supplied. if (this.inputs.focusTarget()) { this.focus(); } } } /** Side-effect executed whenever the widget deactivates. Runs in the write phase. */ deactivationEffect(): void { const event = this.lastDeactivateEvent(); if (event) { this.inputs.onDeactivate?.(event); // Only automatically restore focus if the deactivation was triggered by user keyboard interaction. if (event instanceof KeyboardEvent) { this.focus(); } } } /** Activates the widget. */ activate(event?: KeyboardEvent | FocusEvent): void { if (this.isActivated()) return; if (this.inputs.widgetType() === 'simple') return; this.isActivated.set(true); this.lastActivateEvent.set(event); } /** Deactivates the widget and restores focus to the widget's host element. */ deactivate(event?: KeyboardEvent | FocusEvent): void { if (!this.isActivated()) return; this.isActivated.set(false); this.lastDeactivateEvent.set(event); } }