Skip to content

Commit d15fbb0

Browse files
authored
fix(aria/menu): public api cleanup (#32189)
* fix(aria/menu): remove onSubmit from MenuTrigger * refactor(aria/menu): rename submenu to menu for MenuTrigger * fix(aria/menu): defer rendering of menu content * refactor(aria/menu): remove parent input * refactor(aria/menu): remove submenu input * refactor(aria/menu): rename onSubmit to onSelect * refactor(aria/menu): remove onSelect from MenuTrigger
1 parent 401a768 commit d15fbb0

File tree

15 files changed

+530
-450
lines changed

15 files changed

+530
-450
lines changed

src/aria/menu/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ng_project(
1111
),
1212
deps = [
1313
"//:node_modules/@angular/core",
14+
"//src/aria/deferred-content",
1415
"//src/aria/private",
1516
"//src/cdk/a11y",
1617
"//src/cdk/bidi",

src/aria/menu/index.ts

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

9-
export {Menu, MenuBar, MenuItem, MenuTrigger} from './menu';
9+
export {Menu, MenuBar, MenuContent, MenuItem, MenuTrigger} from './menu';

src/aria/menu/menu.spec.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -151,49 +151,49 @@ describe('Standalone Menu Pattern', () => {
151151

152152
it('should select an item on click', () => {
153153
const banana = getItem('Banana');
154-
spyOn(fixture.componentInstance, 'onSubmit');
154+
spyOn(fixture.componentInstance, 'onSelect');
155155

156156
click(banana!);
157-
expect(fixture.componentInstance.onSubmit).toHaveBeenCalledWith('Banana');
157+
expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith('Banana');
158158
});
159159

160160
it('should select an item on enter', () => {
161161
const banana = getItem('Banana');
162-
spyOn(fixture.componentInstance, 'onSubmit');
162+
spyOn(fixture.componentInstance, 'onSelect');
163163

164164
keydown(document.activeElement!, 'ArrowDown'); // Move focus to Banana
165165
expect(document.activeElement).toBe(banana);
166166

167167
keydown(banana!, 'Enter');
168-
expect(fixture.componentInstance.onSubmit).toHaveBeenCalledWith('Banana');
168+
expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith('Banana');
169169
});
170170

171171
it('should select an item on space', () => {
172172
const banana = getItem('Banana');
173-
spyOn(fixture.componentInstance, 'onSubmit');
173+
spyOn(fixture.componentInstance, 'onSelect');
174174

175175
keydown(document.activeElement!, 'ArrowDown'); // Move focus to Banana
176176
expect(document.activeElement).toBe(banana);
177177

178178
keydown(banana!, ' ');
179-
expect(fixture.componentInstance.onSubmit).toHaveBeenCalledWith('Banana');
179+
expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith('Banana');
180180
});
181181

182182
it('should not select a disabled item', () => {
183183
const cherry = getItem('Cherry');
184-
spyOn(fixture.componentInstance, 'onSubmit');
184+
spyOn(fixture.componentInstance, 'onSelect');
185185

186186
click(cherry!);
187-
expect(fixture.componentInstance.onSubmit).not.toHaveBeenCalled();
187+
expect(fixture.componentInstance.onSelect).not.toHaveBeenCalled();
188188

189189
keydown(document.activeElement!, 'End');
190190
expect(document.activeElement).toBe(cherry);
191191

192192
keydown(cherry!, 'Enter');
193-
expect(fixture.componentInstance.onSubmit).not.toHaveBeenCalled();
193+
expect(fixture.componentInstance.onSelect).not.toHaveBeenCalled();
194194

195195
keydown(cherry!, ' ');
196-
expect(fixture.componentInstance.onSubmit).not.toHaveBeenCalled();
196+
expect(fixture.componentInstance.onSelect).not.toHaveBeenCalled();
197197
});
198198
});
199199

@@ -316,18 +316,18 @@ describe('Standalone Menu Pattern', () => {
316316
}));
317317

318318
it('should close on selecting an item on click', () => {
319-
spyOn(fixture.componentInstance, 'onSubmit');
319+
spyOn(fixture.componentInstance, 'onSelect');
320320
click(getItem('Berries')!); // open submenu
321321
expect(isSubmenuExpanded()).toBe(true);
322322

323323
click(getItem('Blueberry')!);
324324

325-
expect(fixture.componentInstance.onSubmit).toHaveBeenCalledWith('Blueberry');
325+
expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith('Blueberry');
326326
expect(isSubmenuExpanded()).toBe(false);
327327
});
328328

329329
it('should close on selecting an item on enter', () => {
330-
spyOn(fixture.componentInstance, 'onSubmit');
330+
spyOn(fixture.componentInstance, 'onSelect');
331331
const apple = getItem('Apple');
332332
const banana = getItem('Banana');
333333
const berries = getItem('Berries');
@@ -341,12 +341,12 @@ describe('Standalone Menu Pattern', () => {
341341

342342
keydown(blueberry!, 'Enter');
343343

344-
expect(fixture.componentInstance.onSubmit).toHaveBeenCalledWith('Blueberry');
344+
expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith('Blueberry');
345345
expect(isSubmenuExpanded()).toBe(false);
346346
});
347347

348348
it('should close on selecting an item on space', () => {
349-
spyOn(fixture.componentInstance, 'onSubmit');
349+
spyOn(fixture.componentInstance, 'onSelect');
350350
const apple = getItem('Apple');
351351
const banana = getItem('Banana');
352352
const berries = getItem('Berries');
@@ -360,7 +360,7 @@ describe('Standalone Menu Pattern', () => {
360360

361361
keydown(blueberry!, ' ');
362362

363-
expect(fixture.componentInstance.onSubmit).toHaveBeenCalledWith('Blueberry');
363+
expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith('Blueberry');
364364
expect(isSubmenuExpanded()).toBe(false);
365365
});
366366

@@ -877,12 +877,12 @@ describe('Menu Bar Pattern', () => {
877877

878878
@Component({
879879
template: `
880-
<div ngMenu (onSubmit)="onSubmit($event)">
880+
<div ngMenu (onSelect)="onSelect($event)">
881881
<div ngMenuItem value='Apple' searchTerm='Apple'>Apple</div>
882882
<div ngMenuItem value='Banana' searchTerm='Banana'>Banana</div>
883-
<div ngMenuItem value='Berries' searchTerm='Berries' #berriesItem="ngMenuItem" [submenu]="berriesMenu">Berries</div>
883+
<div ngMenuItem value='Berries' searchTerm='Berries' [submenu]="berriesMenu">Berries</div>
884884
885-
<div ngMenu [parent]="berriesItem" #berriesMenu="ngMenu">
885+
<div ngMenu #berriesMenu="ngMenu">
886886
<div ngMenuItem value='Blueberry' searchTerm='Blueberry'>Blueberry</div>
887887
<div ngMenuItem value='Blackberry' searchTerm='Blackberry'>Blackberry</div>
888888
<div ngMenuItem value='Strawberry' searchTerm='Strawberry'>Strawberry</div>
@@ -894,19 +894,19 @@ describe('Menu Bar Pattern', () => {
894894
imports: [Menu, MenuItem],
895895
})
896896
class StandaloneMenuExample {
897-
onSubmit(value: string) {}
897+
onSelect(value: string) {}
898898
}
899899

900900
@Component({
901901
template: `
902-
<button ngMenuTrigger #menuTrigger="ngMenuTrigger" [submenu]="menu">Open menu</button>
902+
<button ngMenuTrigger [menu]="menu">Open menu</button>
903903
904-
<div ngMenu #menu="ngMenu" [parent]="menuTrigger">
904+
<div ngMenu #menu="ngMenu">
905905
<div ngMenuItem value='Apple' searchTerm='Apple'>Apple</div>
906906
<div ngMenuItem value='Banana' searchTerm='Banana'>Banana</div>
907-
<div ngMenuItem value='Berries' searchTerm='Berries' #berriesItem="ngMenuItem" [submenu]="berriesMenu">Berries</div>
907+
<div ngMenuItem value='Berries' searchTerm='Berries' [submenu]="berriesMenu">Berries</div>
908908
909-
<div ngMenu [parent]="berriesItem" #berriesMenu="ngMenu">
909+
<div ngMenu #berriesMenu="ngMenu">
910910
<div ngMenuItem value='Blueberry' searchTerm='Blueberry'>Blueberry</div>
911911
<div ngMenuItem value='Blackberry' searchTerm='Blackberry'>Blackberry</div>
912912
<div ngMenuItem value='Strawberry' searchTerm='Strawberry'>Strawberry</div>
@@ -923,24 +923,24 @@ class MenuTriggerExample {}
923923
template: `
924924
<div ngMenuBar>
925925
<div ngMenuItem value='File' searchTerm='File'>File</div>
926-
<div ngMenuItem value='Edit' searchTerm='Edit' [submenu]="editMenu" #editItem="ngMenuItem">Edit</div>
926+
<div ngMenuItem value='Edit' searchTerm='Edit' [submenu]="editMenu">Edit</div>
927927
928-
<div ngMenu [parent]="editItem" #editMenu="ngMenu">
928+
<div ngMenu #editMenu="ngMenu">
929929
<div ngMenuItem value='Undo' searchTerm='Undo'>Undo</div>
930930
<div ngMenuItem value='Redo' searchTerm='Redo'>Redo</div>
931931
</div>
932932
933-
<div ngMenuItem #viewItem="ngMenuItem" [submenu]="viewMenu" value='View' searchTerm='View'>View</div>
933+
<div ngMenuItem [submenu]="viewMenu" value='View' searchTerm='View'>View</div>
934934
935-
<div ngMenu [parent]="viewItem" #viewMenu="ngMenu">
935+
<div ngMenu #viewMenu="ngMenu">
936936
<div ngMenuItem value='Zoom In' searchTerm='Zoom In'>Zoom In</div>
937937
<div ngMenuItem value='Zoom Out' searchTerm='Zoom Out'>Zoom Out</div>
938938
<div ngMenuItem value='Full Screen' searchTerm='Full Screen'>Full Screen</div>
939939
</div>
940940
941-
<div ngMenuItem #helpItem="ngMenuItem" [submenu]="helpMenu" value='Help' searchTerm='Help'>Help</div>
941+
<div ngMenuItem [submenu]="helpMenu" value='Help' searchTerm='Help'>Help</div>
942942
943-
<div ngMenu [parent]="helpItem" #helpMenu="ngMenu">
943+
<div ngMenu #helpMenu="ngMenu">
944944
<div ngMenuItem value='Documentation' searchTerm='Documentation'>Documentation</div>
945945
<div ngMenuItem value='About' searchTerm='About'>About</div>
946946
</div>

src/aria/menu/menu.ts

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
computed,
1212
contentChildren,
1313
Directive,
14+
effect,
1415
ElementRef,
1516
inject,
1617
input,
@@ -30,6 +31,7 @@ import {
3031
import {_IdGenerator} from '@angular/cdk/a11y';
3132
import {toSignal} from '@angular/core/rxjs-interop';
3233
import {Directionality} from '@angular/cdk/bidi';
34+
import {DeferredContent, DeferredContentAware} from '@angular/aria/deferred-content';
3335

3436
/**
3537
* A trigger for a menu.
@@ -45,7 +47,7 @@ import {Directionality} from '@angular/cdk/bidi';
4547
'[attr.tabindex]': '_pattern.tabindex()',
4648
'[attr.aria-haspopup]': '_pattern.hasPopup()',
4749
'[attr.aria-expanded]': '_pattern.expanded()',
48-
'[attr.aria-controls]': '_pattern.submenu()?.id()',
50+
'[attr.aria-controls]': '_pattern.menu()?.id()',
4951
'(click)': '_pattern.onClick()',
5052
'(keydown)': '_pattern.onKeydown($event)',
5153
'(focusout)': '_pattern.onFocusOut($event)',
@@ -60,18 +62,18 @@ export class MenuTrigger<V> {
6062

6163
// TODO(wagnermaciel): See we can remove the need to pass in a submenu.
6264

63-
/** The submenu associated with the menu trigger. */
64-
submenu = input<Menu<V> | undefined>(undefined);
65-
66-
/** A callback function triggered when a menu item is selected. */
67-
onSubmit = output<V>();
65+
/** The menu associated with the trigger. */
66+
menu = input<Menu<V> | undefined>(undefined);
6867

6968
/** The menu trigger ui pattern instance. */
70-
readonly _pattern: MenuTriggerPattern<V> = new MenuTriggerPattern({
71-
onSubmit: (value: V) => this.onSubmit.emit(value),
69+
_pattern: MenuTriggerPattern<V> = new MenuTriggerPattern({
7270
element: computed(() => this._elementRef.nativeElement),
73-
submenu: computed(() => this.submenu()?._pattern),
71+
menu: computed(() => this.menu()?._pattern),
7472
});
73+
74+
constructor() {
75+
effect(() => this.menu()?.parent.set(this));
76+
}
7577
}
7678

7779
/**
@@ -105,8 +107,17 @@ export class MenuTrigger<V> {
105107
'(focusin)': '_pattern.onFocusIn()',
106108
'(click)': '_pattern.onClick($event)',
107109
},
110+
hostDirectives: [
111+
{
112+
directive: DeferredContentAware,
113+
inputs: ['preserveContent'],
114+
},
115+
],
108116
})
109117
export class Menu<V> {
118+
/** The DeferredContentAware host directive. */
119+
private readonly _deferredContentAware = inject(DeferredContentAware, {optional: true});
120+
110121
/** The menu items contained in the menu. */
111122
readonly _allItems = contentChildren<MenuItem<V>>(MenuItem, {descendants: true});
112123

@@ -129,9 +140,6 @@ export class Menu<V> {
129140
initialValue: this._directionality.value,
130141
});
131142

132-
/** The submenu associated with the menu. */
133-
readonly submenu = input<Menu<V> | undefined>(undefined);
134-
135143
/** The unique ID of the menu. */
136144
readonly id = input<string>(inject(_IdGenerator).getId('ng-menu-', true));
137145

@@ -142,7 +150,7 @@ export class Menu<V> {
142150
readonly typeaheadDelay = input<number>(0.5); // Picked arbitrarily.
143151

144152
/** A reference to the parent menu item or menu trigger. */
145-
readonly parent = input<MenuTrigger<V> | MenuItem<V>>();
153+
readonly parent = signal<MenuTrigger<V> | MenuItem<V> | undefined>(undefined);
146154

147155
/** The menu ui pattern instance. */
148156
readonly _pattern: MenuPattern<V>;
@@ -160,7 +168,7 @@ export class Menu<V> {
160168
isVisible = computed(() => this._pattern.isVisible());
161169

162170
/** A callback function triggered when a menu item is selected. */
163-
onSubmit = output<V>();
171+
onSelect = output<V>();
164172

165173
constructor() {
166174
this._pattern = new MenuPattern({
@@ -173,7 +181,11 @@ export class Menu<V> {
173181
selectionMode: () => 'explicit',
174182
activeItem: signal(undefined),
175183
element: computed(() => this._elementRef.nativeElement),
176-
onSubmit: (value: V) => this.onSubmit.emit(value),
184+
onSelect: (value: V) => this.onSelect.emit(value),
185+
});
186+
187+
afterRenderEffect(() => {
188+
this._deferredContentAware?.contentVisible.set(this._pattern.isVisible());
177189
});
178190

179191
// TODO(wagnermaciel): This is a redundancy needed for if the user uses display: none to hide
@@ -272,7 +284,7 @@ export class MenuBar<V> {
272284
readonly items = signal<MenuItemPattern<V>[]>([]);
273285

274286
/** A callback function triggered when a menu item is selected. */
275-
onSubmit = output<V>();
287+
onSelect = output<V>();
276288

277289
constructor() {
278290
this._pattern = new MenuBarPattern({
@@ -282,7 +294,7 @@ export class MenuBar<V> {
282294
focusMode: () => 'roving',
283295
orientation: () => 'horizontal',
284296
selectionMode: () => 'explicit',
285-
onSubmit: (value: V) => this.onSubmit.emit(value),
297+
onSelect: (value: V) => this.onSelect.emit(value),
286298
activeItem: signal(undefined),
287299
element: computed(() => this._elementRef.nativeElement),
288300
});
@@ -361,4 +373,16 @@ export class MenuItem<V> {
361373
parent: computed(() => this.parent?._pattern),
362374
submenu: computed(() => this.submenu()?._pattern),
363375
});
376+
377+
constructor() {
378+
effect(() => this.submenu()?.parent.set(this));
379+
}
364380
}
381+
382+
/** Defers the rendering of the menu content. */
383+
@Directive({
384+
selector: 'ng-template[ngMenuContent]',
385+
exportAs: 'ngMenuContent',
386+
hostDirectives: [DeferredContent],
387+
})
388+
export class MenuContent {}

0 commit comments

Comments
 (0)