/
githubmirror
/
components
Обзор
Документация
Войти
/
githubmirror
/
components
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/aria/grid/grid.ts
199 строк
6 KB
Andrey Dolgachev
test(multiple): check for incorrect usage of Angular Aria directives and log violations (#33195)
12 май 2026, 23:40
Не верифицирован
12 май 2026, 23:40
33593e7
Код
Авторство
О чём код?
/** * @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 { afterNextRender, afterRenderEffect, booleanAttribute, computed, Directive, ElementRef, inject, input, OnDestroy, Signal, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import { GridPattern, GridCellPattern, GridRowPattern, SortedCollection, tabIndexTransform, reportViolations, } from '../private'; import {GridRow} from './grid-row'; import {GRID} from './grid-tokens'; /** * The container for a grid. It provides keyboard navigation and focus management for the grid's * rows and cells. It manages the overall behavior of the grid, including focus * wrapping, selection, and disabled states. * * ```html * <table ngGrid [multi]="true" [enableSelection]="true"> * @for (row of gridData; track row) { * <tr ngGridRow> * @for (cell of row; track cell) { * <td ngGridCell [disabled]="cell.disabled"> * {{cell.value}} * </td> * } * </tr> * } * </table> * ``` * * @see [Grid](guide/aria/grid) */ @Directive({ selector: '[ngGrid]', exportAs: 'ngGrid', host: { 'role': 'grid', '[tabindex]': 'tabIndex() !== undefined ? tabIndex() : _pattern.tabIndex()', '[attr.aria-disabled]': '_pattern.disabled()', '[attr.aria-multiselectable]': '_pattern.multiSelectable()', '[attr.aria-activedescendant]': '_pattern.activeDescendant()', '(keydown)': '_pattern.onKeydown($event)', '(click)': '_pattern.onClick($event)', '(focusin)': '_pattern.onFocusIn($event)', '(focusout)': '_pattern.onFocusOut($event)', }, providers: [{provide: GRID, useExisting: Grid}], }) export class Grid implements OnDestroy { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); /** A reference to the host element. */ readonly element = this._elementRef.nativeElement as HTMLElement; /** The collection of rows in the grid. */ readonly _collection = new SortedCollection<GridRow>(); /** The UI patterns for the rows in the grid. */ private readonly _rowPatterns: Signal<GridRowPattern[]> = computed(() => this._collection.orderedItems().map(r => r._pattern), ); /** Text direction. */ readonly textDirection = inject(Directionality).valueSignal; /** Whether selection is enabled for the grid. */ readonly enableSelection = input(false, {transform: booleanAttribute}); /** Whether the grid is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); /** * Whether to allow disabled items to receive focus. When `true`, disabled items are * focusable but not interactive. When `false`, disabled items are skipped during navigation. */ readonly softDisabled = input(true, {transform: booleanAttribute}); /** * The focus strategy used by the grid. * - `roving`: Focus is moved to the active cell using `tabindex`. * - `activedescendant`: Focus remains on the grid container, and `aria-activedescendant` is used to indicate the active cell. */ readonly focusMode = input<'roving' | 'activedescendant'>('roving'); /** * The wrapping behavior for keyboard navigation along the row axis. * - `continuous`: Navigation wraps from the last row to the first, and vice-versa. * - `loop`: Navigation wraps within the current row. * - `nowrap`: Navigation stops at the first/last item in the row. */ readonly rowWrap = input<'continuous' | 'loop' | 'nowrap'>('loop'); /** * The wrapping behavior for keyboard navigation along the column axis. * - `continuous`: Navigation wraps from the last column to the first, and vice-versa. * - `loop`: Navigation wraps within the current column. * - `nowrap`: Navigation stops at the first/last item in the column. */ readonly colWrap = input<'continuous' | 'loop' | 'nowrap'>('loop'); /** Whether multiple cells in the grid can be selected. */ readonly multi = input(false, {transform: booleanAttribute}); /** * The selection strategy used by the grid. * - `follow`: The focused cell is automatically selected. * - `explicit`: Cells are selected explicitly by the user (e.g., via click or spacebar). */ readonly selectionMode = input<'follow' | 'explicit'>('follow'); /** The tabindex of the grid. */ readonly tabIndex = input(undefined, { alias: 'tabindex', transform: tabIndexTransform, }); /** The UI pattern for the grid. */ readonly _pattern = new GridPattern({ ...this, rows: this._rowPatterns, getCell: e => this._getCell(e), element: () => this.element, }); /** The ID of the active descendant in the grid. */ readonly activeDescendant = computed(() => this._pattern.activeDescendant()); constructor() { // Use Write mode for all direct DOM focus management actions. afterRenderEffect({write: () => this._pattern.setDefaultStateEffect()}); afterRenderEffect({write: () => this._pattern.resetStateEffect()}); afterRenderEffect({write: () => this._pattern.resetFocusEffect()}); afterRenderEffect({write: () => this._pattern.restoreFocusEffect()}); afterRenderEffect({write: () => this._pattern.focusEffect()}); // 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); }); } ngOnDestroy() { this._collection.stopObserving(); } /** Scrolls the active cell into view. */ scrollActiveCellIntoView(options: ScrollIntoViewOptions = {block: 'nearest'}) { this._pattern.activeCell()?.element().scrollIntoView(options); } /** Gets the cell pattern for a given element. */ private _getCell(element: Element | null | undefined): GridCellPattern | undefined { let target = element; while (target) { for (const row of this._rowPatterns()) { for (const cell of row.inputs.cells()) { if (cell.element() === target) { return cell; } } } target = target.parentElement?.closest('[ngGridCell]'); } return undefined; } }