/
githubmirror
/
components
Обзор
Документация
Войти
/
githubmirror
/
components
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/aria/menu/menu.ts
215 строк
7 KB
Kristiyan Kostadinov
fix(aria/menu): unable to set softDisabled (#33265)
16 май 2026, 11:36
Не верифицирован
16 май 2026, 11:36
905aeb5
Код
Авторство
О чём код?
/** * @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, output, Signal, signal, untracked, } from '@angular/core'; import {MenuPattern, DeferredContentAware, SortedCollection, reportViolations} from '../private'; import {_IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {MenuTrigger} from './menu-trigger'; import {MenuItem} from './menu-item'; import {MenuBar} from './menu-bar'; import {MENU_COMPONENT} from './menu-tokens'; /** * A list of menu items. * * A `ngMenu` is used to offer a list of menu item choices to users. Menus can be nested * within other menus to create sub-menus. It works in conjunction with `ngMenuTrigger` * and `ngMenuItem` directives. * * ```html * <button ngMenuTrigger [menu]="myMenu">Options</button> * * <div ngMenu #myMenu="ngMenu"> * <div ngMenuItem value="Star">Star</div> * <div ngMenuItem value="Edit">Edit</div> * <div ngMenuItem value="More" [submenu]="subMenu">More</div> * </div> * * <div ngMenu #subMenu="ngMenu"> * <div ngMenuItem value="Sub Item 1">Sub Item 1</div> * <div ngMenuItem value="Sub Item 2">Sub Item 2</div> * </div> * ``` * * @see [Menu](guide/aria/menu) * @see [MenuBar](guide/aria/menubar) */ @Directive({ selector: '[ngMenu]', exportAs: 'ngMenu', host: { 'role': 'menu', '[attr.id]': '_pattern.id()', '[attr.aria-disabled]': '_pattern.disabled()', '[attr.tabindex]': 'tabIndex()', '[attr.data-visible]': 'visible()', '(keydown)': '_pattern.onKeydown($event)', '(mouseover)': '_pattern.onMouseOver($event)', '(mouseout)': '_pattern.onMouseOut($event)', '(focusout)': '_pattern.onFocusOut($event)', '(focusin)': '_pattern.onFocusIn()', '(click)': '_pattern.onClick($event)', }, hostDirectives: [ { directive: DeferredContentAware, inputs: ['preserveContent'], }, ], providers: [{provide: MENU_COMPONENT, useExisting: Menu}], }) export class Menu<V> implements OnDestroy { /** The DeferredContentAware host directive. */ private readonly _deferredContentAware = inject(DeferredContentAware, {optional: true}); /** The collection of menu items. */ readonly _collection = new SortedCollection<MenuItem<V>>(); /** The menu items that are direct children of this menu. */ readonly _items: Signal<MenuItem<V>[]> = computed(() => this._collection.orderedItems().filter(i => i.parent === this), ); /** 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 directionality (LTR / RTL) context for the application (or a subtree of it). */ readonly textDirection = inject(Directionality).valueSignal; /** The unique ID of the menu. */ readonly id = input(inject(_IdGenerator).getId('ng-menu-', true)); /** Whether the menu should wrap its items. */ readonly wrap = input(true, {transform: booleanAttribute}); /** The delay in milliseconds before the typeahead buffer is cleared. */ readonly typeaheadDelay = input<number>(500); // Picked arbitrarily. /** Whether the menu is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); /** A reference to the parent menu item or menu trigger. */ readonly parent = signal<MenuTrigger<V> | MenuItem<V> | undefined>(undefined); /** Whether the menu is soft disabled. */ readonly softDisabled = input(true, {transform: booleanAttribute}); /** The menu ui pattern instance. */ readonly _pattern: MenuPattern<V>; /** * The menu item patterns for the menu items that are direct children of this menu, passed * to the menu pattern. * * Note: contentChildren has an issue where it will return a successively smaller list * each time that the menu is open and closed, eventually resulting in an empty list. * The workaround is to trigger a recomputation of this signal whenever the menu is opened * or closed, by calling this._pattern.visible() in the signal body. Otherwise, computed could * not be used and would have to rebuild the list each time this method is called. */ private readonly _itemPatterns = computed(() => { // Only needed to force a recompute. this._pattern.visible(); return this._items().map(i => i._pattern); }); /** Whether the menu is visible. */ readonly visible = computed(() => this._pattern.visible()); /** The tab index of the menu. */ readonly tabIndex = computed(() => this._pattern.tabIndex()); /** A callback function triggered when a menu item is selected. */ readonly itemSelected = output<V>(); /** The delay in milliseconds before expanding sub-menus on hover. */ readonly expansionDelay = input<number>(100); // Arbitrarily chosen. constructor() { this._pattern = new MenuPattern({ ...this, parent: computed(() => this.parent()?._pattern), items: this._itemPatterns, multi: () => false, focusMode: () => 'roving', orientation: () => 'vertical', selectionMode: () => 'explicit', activeItem: signal(undefined), element: computed(() => this._elementRef.nativeElement), itemSelected: (value: V) => this.itemSelected.emit(value), }); afterRenderEffect({ write: () => { const parent = this.parent(); if (parent instanceof MenuItem && parent.parent instanceof MenuBar) { this._deferredContentAware?.contentVisible.set(true); } else { this._deferredContentAware?.contentVisible.set( this._pattern.visible() || !!this.parent()?._pattern.hasBeenInteracted(), ); } }, }); // Focuses an active menu item when the menu becomes visible. This is needed to // properly restore focus to the active item when returning to a menu, and to // focus the first item when navigating into a submenu with hover. afterRenderEffect({ write: () => { if (this.visible()) { const activeItem = untracked(() => this._pattern.inputs.activeItem()); this._pattern.listBehavior.goto(activeItem!); } }, }); 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); }); } ngOnDestroy() { this._collection.stopObserving(); } /** Closes the menu. */ close() { this._pattern.close(); } }