diff --git a/src/aria/menu/menu.ts b/src/aria/menu/menu.ts index 5c5ba845fa95..a605254fd975 100644 --- a/src/aria/menu/menu.ts +++ b/src/aria/menu/menu.ts @@ -44,9 +44,9 @@ import {DeferredContent, DeferredContentAware} from '@angular/aria/deferred-cont exportAs: 'ngMenuTrigger', host: { 'class': 'ng-menu-trigger', - '[attr.tabindex]': '_pattern.tabindex()', - '[attr.aria-haspopup]': '_pattern.hasPopup()', - '[attr.aria-expanded]': '_pattern.expanded()', + '[attr.tabindex]': 'tabindex()', + '[attr.aria-haspopup]': 'hasPopup()', + '[attr.aria-expanded]': 'expanded()', '[attr.aria-controls]': '_pattern.menu()?.id()', '(click)': '_pattern.onClick()', '(keydown)': '_pattern.onKeydown($event)', @@ -69,6 +69,15 @@ export class MenuTrigger { /** Whether the menu item has been focused. */ readonly hasBeenFocused = signal(false); + /** Whether the menu is expanded. */ + readonly expanded = computed(() => this._pattern.expanded()); + + /** Whether the menu trigger has a popup. */ + readonly hasPopup = computed(() => this._pattern.hasPopup()); + + /** The tabindex of the menu trigger. */ + readonly tabindex = computed(() => this._pattern.tabindex()); + /** The menu trigger ui pattern instance. */ _pattern: MenuTriggerPattern = new MenuTriggerPattern({ element: computed(() => this._elementRef.nativeElement), @@ -83,6 +92,16 @@ export class MenuTrigger { onFocusIn() { this.hasBeenFocused.set(true); } + + /** Opens the menu. */ + open(opts?: {first?: boolean; last?: boolean}) { + this._pattern.open(opts); + } + + /** Closes the menu. */ + close(opts: {refocus?: boolean} = {}) { + this._pattern.close(opts); + } } /** @@ -108,7 +127,7 @@ export class MenuTrigger { 'role': 'menu', 'class': 'ng-menu', '[attr.id]': '_pattern.id()', - '[attr.data-visible]': '_pattern.isVisible()', + '[attr.data-visible]': 'isVisible()', '(keydown)': '_pattern.onKeydown($event)', '(mouseover)': '_pattern.onMouseOver($event)', '(mouseout)': '_pattern.onMouseOut($event)', @@ -173,8 +192,14 @@ export class Menu { */ readonly items = () => this._items().map(i => i._pattern); + /** Whether the menu or any of its child elements are currently focused. */ + readonly isFocused = computed(() => this._pattern.isFocused()); + + /** Whether the menu has received focus. */ + readonly hasBeenFocused = computed(() => this._pattern.hasBeenFocused()); + /** Whether the menu is visible. */ - isVisible = computed(() => this._pattern.isVisible()); + readonly isVisible = computed(() => this._pattern.isVisible()); /** A callback function triggered when a menu item is selected. */ onSelect = output(); @@ -222,24 +247,34 @@ export class Menu { }); } - // TODO(wagnermaciel): Author close, closeAll, and open methods for each directive. + /** Focuses the previous menu item. */ + prev() { + this._pattern.prev(); + } + + /** Focuses the next menu item. */ + next() { + this._pattern.next(); + } + + /** Focuses the first menu item. */ + first() { + this._pattern.first(); + } + + /** Focuses the last menu item. */ + last() { + this._pattern.last(); + } /** Closes the menu. */ close(opts?: {refocus?: boolean}) { - this._pattern.inputs.parent()?.close(opts); + this._pattern.close(opts); } /** Closes all parent menus. */ closeAll(opts?: {refocus?: boolean}) { - const root = this._pattern.root(); - - if (root instanceof MenuTriggerPattern) { - root.close(opts); - } - - if (root instanceof MenuPattern || root instanceof MenuBarPattern) { - root.inputs.activeItem()?.close(opts); - } + this._pattern.closeAll(opts); } } @@ -293,6 +328,12 @@ export class MenuBar { /** The delay in seconds before the typeahead buffer is cleared. */ readonly typeaheadDelay = input(0.5); + /** Whether the menubar or any of its child elements are currently focused. */ + readonly isFocused = computed(() => this._pattern.isFocused()); + + /** Whether the menu has received focus. */ + readonly hasBeenFocused = computed(() => this._pattern.hasBeenFocused()); + /** The menu ui pattern instance. */ readonly _pattern: MenuBarPattern; @@ -325,6 +366,21 @@ export class MenuBar { } }); } + + /** Focuses the previous menu item. */ + prev() { + this._pattern.prev(); + } + + /** Focuses the next menu item. */ + next() { + this._pattern.next(); + } + + /** Closes the menubar and refocuses the root menu bar item. */ + close(opts?: {refocus?: boolean}) { + this._pattern.close(opts); + } } /** @@ -339,11 +395,11 @@ export class MenuBar { 'role': 'menuitem', 'class': 'ng-menu-item', '(focusin)': 'onFocusIn()', - '[attr.tabindex]': '_pattern.tabindex()', - '[attr.data-active]': '_pattern.isActive()', - '[attr.aria-haspopup]': '_pattern.hasPopup()', - '[attr.aria-expanded]': '_pattern.expanded()', - '[attr.aria-disabled]': '_pattern.disabled()', + '[attr.tabindex]': 'tabindex()', + '[attr.data-active]': 'isActive()', + '[attr.aria-haspopup]': 'hasPopup()', + '[attr.aria-expanded]': 'expanded()', + '[attr.aria-disabled]': 'disabled()', '[attr.aria-controls]': '_pattern.submenu()?.id()', }, }) @@ -380,9 +436,21 @@ export class MenuItem { /** The submenu associated with the menu item. */ readonly submenu = input | undefined>(undefined); + /** Whether the menu item is active. */ + readonly isActive = computed(() => this._pattern.isActive()); + /** Whether the menu item has been focused. */ readonly hasBeenFocused = signal(false); + /** Whether the menuis expanded. */ + readonly expanded = computed(() => this._pattern.expanded()); + + /** Whether the menu item has a popup. */ + readonly hasPopup = computed(() => this._pattern.hasPopup()); + + /** The tabindex of the menu item. */ + readonly tabindex = computed(() => this._pattern.tabindex()); + /** The menu item ui pattern instance. */ readonly _pattern: MenuItemPattern = new MenuItemPattern({ id: this.id, @@ -402,6 +470,16 @@ export class MenuItem { onFocusIn() { this.hasBeenFocused.set(true); } + + /** Opens the submenu. */ + open(opts?: {first?: boolean; last?: boolean}) { + this._pattern.open(opts); + } + + /** Closes the submenu. */ + close(opts: {refocus?: boolean} = {}) { + this._pattern.close(opts); + } } /** Defers the rendering of the menu content. */ diff --git a/src/aria/private/menu/menu.ts b/src/aria/private/menu/menu.ts index e49dfa1e5dbc..82b05e90d263 100644 --- a/src/aria/private/menu/menu.ts +++ b/src/aria/private/menu/menu.ts @@ -136,7 +136,7 @@ export class MenuPattern { .on('Home', () => this.first()) .on('End', () => this.last()) .on('Enter', () => this.trigger()) - .on('Escape', () => this.closeAll()) + .on('Escape', () => this.closeAll({refocus: true})) .on(this._expandKey, () => this.expand()) .on(this._collapseKey, () => this.collapse()) .on(this.dynamicSpaceKey, () => this.trigger()) @@ -345,12 +345,17 @@ export class MenuPattern { } } + /** Closes the menu. */ + close(opts?: {refocus?: boolean}) { + this.inputs.parent()?.close(opts); + } + /** Closes the menu and all parent menus. */ - closeAll() { + closeAll(opts?: {refocus?: boolean}) { const root = this.root(); if (root instanceof MenuTriggerPattern) { - root.close({refocus: true}); + root.close(opts); } if (root instanceof MenuBarPattern) { @@ -358,7 +363,7 @@ export class MenuPattern { } if (root instanceof MenuPattern) { - root.inputs.activeItem()?.close({refocus: true}); + root.inputs.activeItem()?.close(opts); } } } @@ -496,8 +501,10 @@ export class MenuBarPattern { } /** Closes the menubar and refocuses the root menu bar item. */ - close() { - this.inputs.activeItem()?.close({refocus: this.isFocused()}); + close(opts?: {refocus?: boolean}) { + opts ??= {refocus: this.isFocused()}; + + this.inputs.activeItem()?.close(opts); } } diff --git a/src/components-examples/aria/menu/menu-context/menu-context-example.ts b/src/components-examples/aria/menu/menu-context/menu-context-example.ts index 8e5c07bdae5a..1e9709ea1e5e 100644 --- a/src/components-examples/aria/menu/menu-context/menu-context-example.ts +++ b/src/components-examples/aria/menu/menu-context/menu-context-example.ts @@ -48,7 +48,7 @@ export class MenuContextExample { menu.element.style.top = `${event.clientY}px`; menu.element.style.left = `${event.clientX}px`; - setTimeout(() => menu._pattern.first()); + setTimeout(() => menu.first()); } } }