Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 99 additions & 21 deletions src/aria/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand All @@ -69,6 +69,15 @@ export class MenuTrigger<V> {
/** 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<V> = new MenuTriggerPattern({
element: computed(() => this._elementRef.nativeElement),
Expand All @@ -83,6 +92,16 @@ export class MenuTrigger<V> {
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);
}
}

/**
Expand All @@ -108,7 +127,7 @@ export class MenuTrigger<V> {
'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)',
Expand Down Expand Up @@ -173,8 +192,14 @@ export class Menu<V> {
*/
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<V>();
Expand Down Expand Up @@ -222,24 +247,34 @@ export class Menu<V> {
});
}

// 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);
}
}

Expand Down Expand Up @@ -293,6 +328,12 @@ export class MenuBar<V> {
/** The delay in seconds before the typeahead buffer is cleared. */
readonly typeaheadDelay = input<number>(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<V>;

Expand Down Expand Up @@ -325,6 +366,21 @@ export class MenuBar<V> {
}
});
}

/** 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);
}
}

/**
Expand All @@ -339,11 +395,11 @@ export class MenuBar<V> {
'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()',
},
})
Expand Down Expand Up @@ -380,9 +436,21 @@ export class MenuItem<V> {
/** The submenu associated with the menu item. */
readonly submenu = input<Menu<V> | 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<V> = new MenuItemPattern<V>({
id: this.id,
Expand All @@ -402,6 +470,16 @@ export class MenuItem<V> {
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. */
Expand Down
19 changes: 13 additions & 6 deletions src/aria/private/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class MenuPattern<V> {
.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())
Expand Down Expand Up @@ -345,20 +345,25 @@ export class MenuPattern<V> {
}
}

/** 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) {
root.close();
}

if (root instanceof MenuPattern) {
root.inputs.activeItem()?.close({refocus: true});
root.inputs.activeItem()?.close(opts);
}
}
}
Expand Down Expand Up @@ -496,8 +501,10 @@ export class MenuBarPattern<V> {
}

/** 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
Loading