Skip to content

Commit 4033c7d

Browse files
committed
refactor(aria/menu): split directives and resolve circular dependencies
Splits up the directives across files and resolve circular dependencies in them.
1 parent 09a617c commit 4033c7d

File tree

9 files changed

+414
-333
lines changed

9 files changed

+414
-333
lines changed

goldens/aria/menu/index.api.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class Menu<V> {
4141
// @public
4242
export class MenuBar<V> {
4343
constructor();
44-
readonly _allItems: Signal<readonly MenuItem<V>[]>;
44+
readonly _allItems: _angular_core.Signal<readonly MenuItem<V>[]>;
4545
close(): void;
4646
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
4747
readonly element: HTMLElement;
@@ -71,12 +71,12 @@ export class MenuContent {
7171
// @public
7272
export class MenuItem<V> {
7373
constructor();
74-
readonly active: Signal<boolean>;
74+
readonly active: _angular_core.Signal<boolean>;
7575
close(): void;
7676
readonly disabled: _angular_core.InputSignal<boolean>;
7777
readonly element: HTMLElement;
78-
readonly expanded: Signal<boolean | null>;
79-
readonly hasPopup: Signal<boolean>;
78+
readonly expanded: _angular_core.Signal<boolean | null>;
79+
readonly hasPopup: _angular_core.Signal<boolean>;
8080
readonly id: _angular_core.InputSignal<string>;
8181
open(): void;
8282
readonly parent: Menu<V> | MenuBar<V> | null;
@@ -96,8 +96,8 @@ export class MenuTrigger<V> {
9696
close(): void;
9797
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
9898
readonly element: HTMLElement;
99-
readonly expanded: Signal<boolean>;
100-
readonly hasPopup: Signal<boolean>;
99+
readonly expanded: _angular_core.Signal<boolean>;
100+
readonly hasPopup: _angular_core.Signal<boolean>;
101101
menu: _angular_core.InputSignal<Menu<V> | undefined>;
102102
open(): void;
103103
_pattern: MenuTriggerPattern<V>;

src/aria/menu/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export {Menu, MenuBar, MenuContent, MenuItem, MenuTrigger} from './menu';
9+
export {MenuTrigger} from './menu-trigger';
10+
export {Menu} from './menu';
11+
export {MenuBar} from './menu-bar';
12+
export {MenuItem} from './menu-item';
13+
export {MenuContent} from './menu-content';

src/aria/menu/menu-bar.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
afterRenderEffect,
11+
booleanAttribute,
12+
computed,
13+
contentChildren,
14+
Directive,
15+
ElementRef,
16+
inject,
17+
input,
18+
model,
19+
output,
20+
signal,
21+
} from '@angular/core';
22+
import {SignalLike, MenuBarPattern} from '@angular/aria/private';
23+
import {Directionality} from '@angular/cdk/bidi';
24+
import {MenuItem} from './menu-item';
25+
import {MENU_COMPONENT} from './menu-tokens';
26+
27+
/**
28+
* A menu bar of menu items.
29+
*
30+
* Like the `ngMenu`, a `ngMenuBar` is used to offer a list of menu item choices to users.
31+
* However, a menubar is used to display a persistent, top-level, always-visible set of
32+
* menu item choices, typically found at the top of an application window.
33+
*
34+
* ```html
35+
* <div ngMenuBar>
36+
* <button ngMenuTrigger [menu]="fileMenu">File</button>
37+
* <button ngMenuTrigger [menu]="editMenu">Edit</button>
38+
* </div>
39+
*
40+
* <div ngMenu #fileMenu="ngMenu">
41+
* <div ngMenuItem>New</div>
42+
* <div ngMenuItem>Open</div>
43+
* </div>
44+
*
45+
* <div ngMenu #editMenu="ngMenu">
46+
* <div ngMenuItem>Cut</div>
47+
* <div ngMenuItem>Copy</div>
48+
* </div>
49+
* ```
50+
*
51+
* @developerPreview 21.0
52+
*/
53+
@Directive({
54+
selector: '[ngMenuBar]',
55+
exportAs: 'ngMenuBar',
56+
host: {
57+
'role': 'menubar',
58+
'[attr.disabled]': '!softDisabled() && _pattern.disabled() ? true : null',
59+
'[attr.aria-disabled]': '_pattern.disabled()',
60+
'[attr.tabindex]': '_pattern.tabIndex()',
61+
'(keydown)': '_pattern.onKeydown($event)',
62+
'(mouseover)': '_pattern.onMouseOver($event)',
63+
'(click)': '_pattern.onClick($event)',
64+
'(focusin)': '_pattern.onFocusIn()',
65+
'(focusout)': '_pattern.onFocusOut($event)',
66+
},
67+
providers: [{provide: MENU_COMPONENT, useExisting: MenuBar}],
68+
})
69+
export class MenuBar<V> {
70+
/** The menu items contained in the menubar. */
71+
readonly _allItems = contentChildren<MenuItem<V>>(MenuItem, {descendants: true});
72+
73+
readonly _items: SignalLike<MenuItem<V>[]> = () =>
74+
this._allItems().filter(i => i.parent === this);
75+
76+
/** A reference to the host element. */
77+
private readonly _elementRef = inject(ElementRef);
78+
79+
/** A reference to the host element. */
80+
readonly element = this._elementRef.nativeElement as HTMLElement;
81+
82+
/** Whether the menubar is disabled. */
83+
readonly disabled = input(false, {transform: booleanAttribute});
84+
85+
/** Whether the menubar is soft disabled. */
86+
readonly softDisabled = input(true, {transform: booleanAttribute});
87+
88+
/** The directionality (LTR / RTL) context for the application (or a subtree of it). */
89+
readonly textDirection = inject(Directionality).valueSignal;
90+
91+
/** The values of the currently selected menu items. */
92+
readonly values = model<V[]>([]);
93+
94+
/** Whether the menu should wrap its items. */
95+
readonly wrap = input(true, {transform: booleanAttribute});
96+
97+
/** The delay in milliseconds before the typeahead buffer is cleared. */
98+
readonly typeaheadDelay = input<number>(500);
99+
100+
/** The menu ui pattern instance. */
101+
readonly _pattern: MenuBarPattern<V>;
102+
103+
/** The menu items as a writable signal. */
104+
private readonly _itemPatterns = signal<any[]>([]);
105+
106+
/** A callback function triggered when a menu item is selected. */
107+
onSelect = output<V>();
108+
109+
constructor() {
110+
this._pattern = new MenuBarPattern({
111+
...this,
112+
items: this._itemPatterns,
113+
multi: () => false,
114+
softDisabled: () => true,
115+
focusMode: () => 'roving',
116+
orientation: () => 'horizontal',
117+
selectionMode: () => 'explicit',
118+
onSelect: (value: V) => this.onSelect.emit(value),
119+
activeItem: signal(undefined),
120+
element: computed(() => this._elementRef.nativeElement),
121+
});
122+
123+
afterRenderEffect(() => {
124+
this._itemPatterns.set(this._items().map(i => i._pattern));
125+
});
126+
127+
afterRenderEffect(() => {
128+
if (!this._pattern.hasBeenFocused()) {
129+
this._pattern.setDefaultState();
130+
}
131+
});
132+
}
133+
134+
/** Closes the menubar. */
135+
close() {
136+
this._pattern.close();
137+
}
138+
}

src/aria/menu/menu-content.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Directive} from '@angular/core';
10+
import {DeferredContent} from '@angular/aria/private';
11+
12+
/**
13+
* Defers the rendering of the menu content.
14+
*
15+
* This structural directive should be applied to an `ng-template` within a `ngMenu`
16+
* or `ngMenuBar` to lazily render its content only when the menu is opened.
17+
*
18+
* ```html
19+
* <div ngMenu #myMenu="ngMenu">
20+
* <ng-template ngMenuContent>
21+
* <div ngMenuItem>Lazy Item 1</div>
22+
* <div ngMenuItem>Lazy Item 2</div>
23+
* </ng-template>
24+
* </div>
25+
* ```
26+
*
27+
* @developerPreview 21.0
28+
*/
29+
@Directive({
30+
selector: 'ng-template[ngMenuContent]',
31+
exportAs: 'ngMenuContent',
32+
hostDirectives: [DeferredContent],
33+
})
34+
export class MenuContent {}

src/aria/menu/menu-item.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {computed, Directive, effect, ElementRef, inject, input, model} from '@angular/core';
10+
import {MenuItemPattern} from '@angular/aria/private';
11+
import {_IdGenerator} from '@angular/cdk/a11y';
12+
import {MENU_COMPONENT} from './menu-tokens';
13+
import type {Menu} from './menu';
14+
import type {MenuBar} from './menu-bar';
15+
16+
/**
17+
* An item in a Menu.
18+
*
19+
* `ngMenuItem` directives can be used in `ngMenu` and `ngMenuBar` to represent a choice
20+
* or action a user can take. They can also act as triggers for sub-menus.
21+
*
22+
* ```html
23+
* <div ngMenuItem (onSelect)="doAction()">Action Item</div>
24+
*
25+
* <div ngMenuItem [submenu]="anotherMenu">Submenu Trigger</div>
26+
* ```
27+
*
28+
* @developerPreview 21.0
29+
*/
30+
@Directive({
31+
selector: '[ngMenuItem]',
32+
exportAs: 'ngMenuItem',
33+
host: {
34+
'role': 'menuitem',
35+
'(focusin)': '_pattern.onFocusIn()',
36+
'[attr.tabindex]': '_pattern.tabIndex()',
37+
'[attr.data-active]': 'active()',
38+
'[attr.aria-haspopup]': 'hasPopup()',
39+
'[attr.aria-expanded]': 'expanded()',
40+
'[attr.aria-disabled]': '_pattern.disabled()',
41+
'[attr.aria-controls]': '_pattern.submenu()?.id()',
42+
},
43+
})
44+
export class MenuItem<V> {
45+
/** A reference to the host element. */
46+
private readonly _elementRef = inject(ElementRef);
47+
48+
/** A reference to the host element. */
49+
readonly element = this._elementRef.nativeElement as HTMLElement;
50+
51+
/** The unique ID of the menu item. */
52+
readonly id = input(inject(_IdGenerator).getId('ng-menu-item-', true));
53+
54+
/** The value of the menu item. */
55+
readonly value = input.required<V>();
56+
57+
/** Whether the menu item is disabled. */
58+
readonly disabled = input<boolean>(false);
59+
60+
// TODO(wagnermaciel): Discuss whether all inputs should be models.
61+
62+
/** The search term associated with the menu item. */
63+
readonly searchTerm = model<string>('');
64+
65+
/** A reference to the parent menu or menubar. */
66+
readonly parent = inject<Menu<V> | MenuBar<V>>(MENU_COMPONENT, {optional: true});
67+
68+
/** The submenu associated with the menu item. */
69+
readonly submenu = input<Menu<V> | undefined>(undefined);
70+
71+
/** Whether the menu item is active. */
72+
readonly active = computed(() => this._pattern.active());
73+
74+
/** Whether the menu is expanded. */
75+
readonly expanded = computed(() => this._pattern.expanded());
76+
77+
/** Whether the menu item has a popup. */
78+
readonly hasPopup = computed(() => this._pattern.hasPopup());
79+
80+
/** The menu item ui pattern instance. */
81+
readonly _pattern: MenuItemPattern<V> = new MenuItemPattern<V>({
82+
id: this.id,
83+
value: this.value,
84+
element: computed(() => this._elementRef.nativeElement),
85+
disabled: this.disabled,
86+
searchTerm: this.searchTerm,
87+
parent: computed(() => this.parent?._pattern),
88+
submenu: computed(() => this.submenu()?._pattern),
89+
});
90+
91+
constructor() {
92+
effect(() => this.submenu()?.parent.set(this));
93+
}
94+
95+
/** Opens the submenu focusing on the first menu item. */
96+
open() {
97+
this._pattern.open({first: true});
98+
}
99+
100+
/** Closes the submenu. */
101+
close() {
102+
this._pattern.close();
103+
}
104+
}

src/aria/menu/menu-tokens.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {InjectionToken} from '@angular/core';
10+
import type {Menu} from './menu';
11+
import type {MenuBar} from './menu-bar';
12+
13+
/** Token used to expose menus to their child components. */
14+
export const MENU_COMPONENT = new InjectionToken<Menu<any> | MenuBar<any>>('MENU_COMPONENT');

0 commit comments

Comments
 (0)