/
githubmirror
/
components
Обзор
Документация
Войти
/
githubmirror
/
components
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/aria/tree/tree.ts
211 строк
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, model, OnDestroy, signal, Signal, untracked, } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import { SortedCollection, tabIndexTransform, TreeItemPattern, TreePattern, reportViolations, } from '../private'; import type {TreeItem} from './tree-item'; /** * A container that transforms nested lists into an accessible, ARIA-compliant tree structure. * It manages the overall state of the tree, including selection, expansion, and keyboard * navigation. * * ```html * <ul ngTree [(value)]="selectedItems" [multi]="true"> * <ng-template * [ngTemplateOutlet]="treeNodes" * [ngTemplateOutletContext]="{nodes: treeData, parent: tree}" * /> * </ul> * * <ng-template #treeNodes let-nodes="nodes" let-parent="parent"> * @for (node of nodes; track node.name) { * <li ngTreeItem [parent]="parent" [value]="node.name" [label]="node.name"> * {{ node.name }} * @if (node.children) { * <ul role="group"> * <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> * <ng-template * [ngTemplateOutlet]="treeNodes" * [ngTemplateOutletContext]="{nodes: node.children, parent: group}" * /> * </ng-template> * </ul> * } * </li> * } * </ng-template> * ``` * * @see [Tree](guide/aria/tree) */ @Directive({ selector: '[ngTree]', exportAs: 'ngTree', host: { 'role': 'tree', '[attr.id]': 'id()', '[attr.aria-orientation]': '_pattern.orientation()', '[attr.aria-multiselectable]': '_pattern.multi()', '[attr.aria-disabled]': '_pattern.disabled()', '[attr.aria-activedescendant]': '_pattern.activeDescendant()', '[tabindex]': 'tabIndex() !== undefined ? tabIndex() : _pattern.tabIndex()', '(keydown)': '_pattern.onKeydown($event)', '(click)': '_pattern.onClick($event)', '(focusin)': '_pattern.onFocusIn()', }, }) export class Tree<V> 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 tree items. */ readonly _collection = new SortedCollection<TreeItem<V>>(); /** A unique identifier for the tree. */ readonly id = input(inject(_IdGenerator).getId('ng-tree-', true)); /** Orientation of the tree. */ readonly orientation = input<'vertical' | 'horizontal'>('vertical'); /** Whether multi-selection is allowed. */ readonly multi = input(false, {transform: booleanAttribute}); /** Whether the tree is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); /** * The selection strategy used by the tree. * - `explicit`: Items are selected explicitly by the user (e.g., via click or spacebar). * - `follow`: The focused item is automatically selected. */ readonly selectionMode = input<'explicit' | 'follow'>('explicit'); /** * The focus strategy used by the tree. * - `roving`: Focus is moved to the active item using `tabindex`. * - `activedescendant`: Focus remains on the tree container, and `aria-activedescendant` is used to indicate the active item. */ readonly focusMode = input<'roving' | 'activedescendant'>('roving'); /** Whether navigation wraps. */ readonly wrap = input(true, {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 delay in seconds before the typeahead search is reset. */ readonly typeaheadDelay = input(500); /** The tabindex of the tree. */ readonly tabIndex = input(undefined, { alias: 'tabindex', transform: tabIndexTransform, }); /** The values of the currently selected items. */ readonly value = model<V[]>([]); /** Text direction. */ readonly textDirection = inject(Directionality).valueSignal; /** Whether the tree is in navigation mode. */ readonly nav = input(false, {transform: booleanAttribute}); /** * The `aria-current` type. It can be used in navigation trees to indicate the currently active item. * See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-current for more details. */ readonly currentType = input<'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'>( 'page', ); /** The UI pattern for the tree. */ readonly _pattern: TreePattern<V>; /** The ID of the active descendant in the tree. */ readonly activeDescendant: Signal<string | undefined>; constructor() { const inputs = { ...this, id: this.id, items: computed(() => this._collection.orderedItems().map(item => item._pattern)), activeItem: signal<TreeItemPattern<V> | undefined>(undefined), element: () => this.element, }; this._pattern = new TreePattern<V>(inputs); this.activeDescendant = computed(() => this._pattern.activeDescendant()); afterNextRender(() => { this._collection.startObserving(this.element); }); // Check for any violations after the DOM has been updated. if (typeof ngDevMode === 'undefined' || ngDevMode) { afterRenderEffect({ read: () => { reportViolations(this._pattern.validate(), this.element); }, }); } // Resets default focus based on selection state until interacted. afterRenderEffect({write: () => this._pattern.setDefaultStateEffect()}); afterRenderEffect({ write: () => { const items = inputs.items(); const activeItem = untracked(() => inputs.activeItem()); if (activeItem && !items.some(i => i === activeItem)) { this._pattern.treeBehavior.unfocus(); this._pattern.setDefaultState(); } }, }); } ngOnDestroy() { this._collection.stopObserving(); } scrollActiveItemIntoView(options: ScrollIntoViewOptions = {block: 'nearest'}) { this._pattern.inputs.activeItem()?.element()?.scrollIntoView(options); } }