diff --git a/goldens/aria/menu/index.api.md b/goldens/aria/menu/index.api.md index 79630b052dba..f2174e0d5b12 100644 --- a/goldens/aria/menu/index.api.md +++ b/goldens/aria/menu/index.api.md @@ -89,9 +89,10 @@ export class MenuItem implements OnInit, OnDestroy { readonly role: _angular_core.InputSignal<"menuitem" | "menuitemradio" | "menuitemcheckbox">; readonly searchTerm: _angular_core.ModelSignal; readonly submenu: _angular_core.InputSignal | undefined>; + readonly submenuData: _angular_core.InputSignal; readonly value: _angular_core.InputSignal; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngMenuItem]", ["ngMenuItem"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": true; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "searchTerm": { "alias": "searchTerm"; "required": false; "isSignal": true; }; "role": { "alias": "role"; "required": false; "isSignal": true; }; "submenu": { "alias": "submenu"; "required": false; "isSignal": true; }; }, { "searchTerm": "searchTermChange"; }, never, never, true, never>; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngMenuItem]", ["ngMenuItem"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": true; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "searchTerm": { "alias": "searchTerm"; "required": false; "isSignal": true; }; "role": { "alias": "role"; "required": false; "isSignal": true; }; "submenu": { "alias": "submenu"; "required": false; "isSignal": true; }; "submenuData": { "alias": "submenuData"; "required": false; "isSignal": true; }; }, { "searchTerm": "searchTermChange"; }, never, never, true, never>; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration, never>; } @@ -105,12 +106,13 @@ export class MenuTrigger { readonly expanded: _angular_core.Signal; readonly hasPopup: _angular_core.Signal; readonly menu: _angular_core.InputSignal | undefined>; + readonly menuData: _angular_core.InputSignal; open(): void; readonly _pattern: MenuTriggerPattern; readonly softDisabled: _angular_core.InputSignalWithTransform; readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngMenuTrigger]", ["ngMenuTrigger"], { "menu": { "alias": "menu"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngMenuTrigger]", ["ngMenuTrigger"], { "menu": { "alias": "menu"; "required": false; "isSignal": true; }; "menuData": { "alias": "menuData"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration, never>; } diff --git a/goldens/aria/private/index.api.md b/goldens/aria/private/index.api.md index 9b459d94906c..38df07e89ac7 100644 --- a/goldens/aria/private/index.api.md +++ b/goldens/aria/private/index.api.md @@ -148,6 +148,8 @@ export class DeferredContentAware { // (undocumented) readonly contentVisible: _angular_core.WritableSignal; // (undocumented) + readonly context: _angular_core.WritableSignal; + // (undocumented) readonly preserveContent: _angular_core.ModelSignal; // (undocumented) static ɵdir: _angular_core.ɵɵDirectiveDeclaration; diff --git a/src/aria/menu/menu-item.ts b/src/aria/menu/menu-item.ts index f19e11a25e82..76b5d9f447ad 100644 --- a/src/aria/menu/menu-item.ts +++ b/src/aria/menu/menu-item.ts @@ -82,6 +82,9 @@ export class MenuItem implements OnInit, OnDestroy { /** The submenu associated with the menu item. */ readonly submenu = input | undefined>(undefined); + /** Context data to be passed to the submenu's template. */ + readonly submenuData = input(null); + /** Whether the menu item is active. */ readonly active = computed(() => this._pattern.active()); diff --git a/src/aria/menu/menu-trigger.ts b/src/aria/menu/menu-trigger.ts index 7ed57871e656..5cc71065f4c6 100644 --- a/src/aria/menu/menu-trigger.ts +++ b/src/aria/menu/menu-trigger.ts @@ -67,6 +67,9 @@ export class MenuTrigger { /** The menu associated with the trigger. */ readonly menu = input | undefined>(undefined); + /** Context data to be passed to the menu's template. */ + readonly menuData = input(null); + /** Whether the menu is expanded. */ readonly expanded = computed(() => this._pattern.expanded()); diff --git a/src/aria/menu/menu.spec.ts b/src/aria/menu/menu.spec.ts index 9ba7d23d562c..4d012b6243ad 100644 --- a/src/aria/menu/menu.spec.ts +++ b/src/aria/menu/menu.spec.ts @@ -771,6 +771,20 @@ describe('Menu Trigger Pattern', () => { expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Apple'); }); }); + + it('should pass template context data to the menu and submenu', () => { + setupMenu(); + fixture.componentInstance.menuData.set({$implicit: 'Trigger Context'}); + fixture.componentInstance.submenuData.set({$implicit: 'Submenu Context'}); + fixture.detectChanges(); + + click(getTrigger()); + expect(getItem('Apple Trigger Context')).toBeTruthy(); + + click(getItem('Berries')!); + const blueberryItem = getItem('Blueberry Submenu Context'); + expect(blueberryItem).toBeTruthy(); + }); }); describe('CDK Overlay Menu Pattern', () => { @@ -1263,17 +1277,17 @@ class StandaloneMenuExample { @Component({ template: ` - +
- -
Apple
+ +
Apple {{data}}
Banana
-
Berries
+
Berries
- -
Blueberry
+ +
Blueberry {{subdata}}
Blackberry
Strawberry
@@ -1288,6 +1302,8 @@ class StandaloneMenuExample { }) class MenuTriggerExample { itemSelected(value: string) {} + menuData = signal(null); + submenuData = signal(null); } @Component({ diff --git a/src/aria/menu/menu.ts b/src/aria/menu/menu.ts index a4b79e76a307..0858ce386168 100644 --- a/src/aria/menu/menu.ts +++ b/src/aria/menu/menu.ts @@ -166,11 +166,19 @@ export class Menu implements OnDestroy { afterRenderEffect({ write: () => { const parent = this.parent(); + const deferredContentAware = this._deferredContentAware; + + if (parent) { + deferredContentAware?.context.set( + parent instanceof MenuItem ? parent.submenuData() : parent.menuData(), + ); + } + if (parent instanceof MenuItem && parent.parent instanceof MenuBar) { - this._deferredContentAware?.contentVisible.set(true); + deferredContentAware?.contentVisible.set(true); } else { - this._deferredContentAware?.contentVisible.set( - this._pattern.visible() || !!this.parent()?._pattern.hasBeenInteracted(), + deferredContentAware?.contentVisible.set( + this._pattern.visible() || !!parent?._pattern.hasBeenInteracted(), ); } }, diff --git a/src/aria/private/deferred-content/deferred-content.spec.ts b/src/aria/private/deferred-content/deferred-content.spec.ts index 28da5f1cfcec..abbf0c30e0d0 100644 --- a/src/aria/private/deferred-content/deferred-content.spec.ts +++ b/src/aria/private/deferred-content/deferred-content.spec.ts @@ -20,18 +20,26 @@ describe('DeferredContent', () => { collapsible = fixture.debugElement.query(By.directive(Collapsible)); }); - it('removes the content when hidden.', async () => { + it('removes the content when hidden', async () => { collapsible.injector.get(Collapsible).contentVisible.set(false); await fixture.whenStable(); expect(collapsible.nativeElement.innerText).toBe(''); }); - it('creates the content when the container becomes visible.', async () => { + it('creates the content when the container becomes visible', async () => { collapsible.injector.get(Collapsible).contentVisible.set(true); await fixture.whenStable(); expect(collapsible.nativeElement.innerText).toBe('Lazy Content'); }); + it('creates renders the content with the provided context', async () => { + const instance = collapsible.injector.get(Collapsible); + instance.context.set({context: 'with context'}); + instance.contentVisible.set(true); + await fixture.whenStable(); + expect(collapsible.nativeElement.innerText).toBe('Lazy Content with context'); + }); + describe('with preserveContent', () => { let component: TestComponent; @@ -40,19 +48,19 @@ describe('DeferredContent', () => { component.preserveContent.set(true); }); - it('does not create the content until first visible.', async () => { + it('does not create the content until first visible', async () => { collapsible.injector.get(Collapsible).contentVisible.set(false); await fixture.whenStable(); expect(collapsible.nativeElement.innerText).toBe(''); }); - it('creates the content when first visible with preserveContent.', async () => { + it('creates the content when first visible with preserveContent', async () => { collapsible.injector.get(Collapsible).contentVisible.set(true); await fixture.whenStable(); expect(collapsible.nativeElement.innerText).toBe('Lazy Content'); }); - it('does not remove the content when hidden.', async () => { + it('does not remove the content when hidden', async () => { collapsible.injector.get(Collapsible).contentVisible.set(true); await fixture.whenStable(); collapsible.injector.get(Collapsible).contentVisible.set(false); @@ -70,9 +78,13 @@ class Collapsible { private readonly _deferredContentAware = inject(DeferredContentAware); contentVisible = signal(true); + context = signal(null); constructor() { - effect(() => this._deferredContentAware.contentVisible.set(this.contentVisible())); + effect(() => { + this._deferredContentAware.context.set(this.context()); + this._deferredContentAware.contentVisible.set(this.contentVisible()); + }); } } @@ -85,8 +97,8 @@ class CollapsibleContent {} @Component({ template: `
- - Lazy Content + + Lazy Content {{context}}
`, diff --git a/src/aria/private/deferred-content/deferred-content.ts b/src/aria/private/deferred-content/deferred-content.ts index 180a51dab242..9bb7f7400571 100644 --- a/src/aria/private/deferred-content/deferred-content.ts +++ b/src/aria/private/deferred-content/deferred-content.ts @@ -25,6 +25,7 @@ import { export class DeferredContentAware { readonly contentVisible = signal(false); readonly preserveContent = model(false); + readonly context = signal(null); } /** @@ -55,13 +56,21 @@ export class DeferredContent implements OnDestroy { constructor() { afterRenderEffect({ write: () => { - if (this.deferredContentAware()?.contentVisible()) { + const contentAware = this.deferredContentAware(); + const isVisible = contentAware?.contentVisible(); + const preserveContent = contentAware?.preserveContent(); + const context = contentAware?.context(); + + if (isVisible) { if (!this._isRendered) { this._destroyContent(); - this._currentViewRef = this._viewContainerRef.createEmbeddedView(this._templateRef); + this._currentViewRef = this._viewContainerRef.createEmbeddedView( + this._templateRef, + context, + ); this._isRendered = true; } - } else if (!this.deferredContentAware()?.preserveContent()) { + } else if (!preserveContent) { this._destroyContent(); this._isRendered = false; }