Skip to content

Commit ecdb89c

Browse files
committed
fix(aria/menu): public api cleanup (angular#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 9dc45f9 commit ecdb89c

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,
@@ -32,6 +33,7 @@ import {
3233
import {_IdGenerator} from '@angular/cdk/a11y';
3334
import {toSignal} from '@angular/core/rxjs-interop';
3435
import {Directionality} from '@angular/cdk/bidi';
36+
import {DeferredContent, DeferredContentAware} from '@angular/aria/deferred-content';
3537

3638
/**
3739
* A trigger for a menu.
@@ -47,7 +49,7 @@ import {Directionality} from '@angular/cdk/bidi';
4749
'[attr.tabindex]': '_pattern.tabindex()',
4850
'[attr.aria-haspopup]': '_pattern.hasPopup()',
4951
'[attr.aria-expanded]': '_pattern.expanded()',
50-
'[attr.aria-controls]': '_pattern.submenu()?.id()',
52+
'[attr.aria-controls]': '_pattern.menu()?.id()',
5153
'(click)': '_pattern.onClick()',
5254
'(keydown)': '_pattern.onKeydown($event)',
5355
'(focusout)': '_pattern.onFocusOut($event)',
@@ -62,18 +64,18 @@ export class MenuTrigger<V> {
6264

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

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

7170
/** The menu trigger ui pattern instance. */
72-
readonly _pattern: MenuTriggerPattern<V> = new MenuTriggerPattern({
73-
onSubmit: (value: V) => this.onSubmit.emit(value),
71+
_pattern: MenuTriggerPattern<V> = new MenuTriggerPattern({
7472
element: computed(() => this._elementRef.nativeElement),
75-
submenu: computed(() => this.submenu()?._pattern),
73+
menu: computed(() => this.menu()?._pattern),
7674
});
75+
76+
constructor() {
77+
effect(() => this.menu()?.parent.set(this));
78+
}
7779
}
7880

7981
/**
@@ -107,8 +109,17 @@ export class MenuTrigger<V> {
107109
'(focusin)': '_pattern.onFocusIn()',
108110
'(click)': '_pattern.onClick($event)',
109111
},
112+
hostDirectives: [
113+
{
114+
directive: DeferredContentAware,
115+
inputs: ['preserveContent'],
116+
},
117+
],
110118
})
111119
export class Menu<V> {
120+
/** The DeferredContentAware host directive. */
121+
private readonly _deferredContentAware = inject(DeferredContentAware, {optional: true});
122+
112123
/** The menu items contained in the menu. */
113124
readonly _allItems = contentChildren<MenuItem<V>>(MenuItem, {descendants: true});
114125

@@ -131,9 +142,6 @@ export class Menu<V> {
131142
initialValue: this._directionality.value,
132143
});
133144

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

@@ -144,7 +152,7 @@ export class Menu<V> {
144152
readonly typeaheadDelay = input<number>(0.5); // Picked arbitrarily.
145153

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

149157
/** The menu ui pattern instance. */
150158
readonly _pattern: MenuPattern<V>;
@@ -162,7 +170,7 @@ export class Menu<V> {
162170
isVisible = computed(() => this._pattern.isVisible());
163171

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

167175
constructor() {
168176
this._pattern = new MenuPattern({
@@ -175,7 +183,11 @@ export class Menu<V> {
175183
selectionMode: () => 'explicit',
176184
activeItem: signal(undefined),
177185
element: computed(() => this._elementRef.nativeElement),
178-
onSubmit: (value: V) => this.onSubmit.emit(value),
186+
onSelect: (value: V) => this.onSelect.emit(value),
187+
});
188+
189+
afterRenderEffect(() => {
190+
this._deferredContentAware?.contentVisible.set(this._pattern.isVisible());
179191
});
180192

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

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

279291
constructor() {
280292
this._pattern = new MenuBarPattern({
@@ -284,7 +296,7 @@ export class MenuBar<V> {
284296
focusMode: () => 'roving',
285297
orientation: () => 'horizontal',
286298
selectionMode: () => 'explicit',
287-
onSubmit: (value: V) => this.onSubmit.emit(value),
299+
onSelect: (value: V) => this.onSelect.emit(value),
288300
activeItem: signal(undefined),
289301
element: computed(() => this._elementRef.nativeElement),
290302
});
@@ -363,4 +375,16 @@ export class MenuItem<V> {
363375
parent: computed(() => this.parent?._pattern),
364376
submenu: computed(() => this.submenu()?._pattern),
365377
});
378+
379+
constructor() {
380+
effect(() => this.submenu()?.parent.set(this));
381+
}
366382
}
383+
384+
/** Defers the rendering of the menu content. */
385+
@Directive({
386+
selector: 'ng-template[ngMenuContent]',
387+
exportAs: 'ngMenuContent',
388+
hostDirectives: [DeferredContent],
389+
})
390+
export class MenuContent {}

0 commit comments

Comments
 (0)