Skip to content
Closed
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
6 changes: 4 additions & 2 deletions goldens/aria/menu/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ export class MenuItem<V> implements OnInit, OnDestroy {
readonly role: _angular_core.InputSignal<"menuitem" | "menuitemradio" | "menuitemcheckbox">;
readonly searchTerm: _angular_core.ModelSignal<string>;
readonly submenu: _angular_core.InputSignal<Menu<V> | undefined>;
readonly submenuData: _angular_core.InputSignal<unknown>;
readonly value: _angular_core.InputSignal<V>;
// (undocumented)
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<MenuItem<any>, "[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<MenuItem<any>, "[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<MenuItem<any>, never>;
}
Expand All @@ -105,12 +106,13 @@ export class MenuTrigger<V> {
readonly expanded: _angular_core.Signal<boolean>;
readonly hasPopup: _angular_core.Signal<boolean>;
readonly menu: _angular_core.InputSignal<Menu<V> | undefined>;
readonly menuData: _angular_core.InputSignal<unknown>;
open(): void;
readonly _pattern: MenuTriggerPattern<V>;
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
// (undocumented)
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<MenuTrigger<any>, "[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<MenuTrigger<any>, "[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<MenuTrigger<any>, never>;
}
Expand Down
2 changes: 2 additions & 0 deletions goldens/aria/private/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ export class DeferredContentAware {
// (undocumented)
readonly contentVisible: _angular_core.WritableSignal<boolean>;
// (undocumented)
readonly context: _angular_core.WritableSignal<unknown>;
// (undocumented)
readonly preserveContent: _angular_core.ModelSignal<boolean>;
// (undocumented)
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<DeferredContentAware, never, never, { "preserveContent": { "alias": "preserveContent"; "required": false; "isSignal": true; }; }, { "preserveContent": "preserveContentChange"; }, never, never, true, never>;
Expand Down
3 changes: 3 additions & 0 deletions src/aria/menu/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export class MenuItem<V> implements OnInit, OnDestroy {
/** The submenu associated with the menu item. */
readonly submenu = input<Menu<V> | undefined>(undefined);

/** Context data to be passed to the submenu's template. */
readonly submenuData = input<unknown>(null);

/** Whether the menu item is active. */
readonly active = computed(() => this._pattern.active());

Expand Down
3 changes: 3 additions & 0 deletions src/aria/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export class MenuTrigger<V> {
/** The menu associated with the trigger. */
readonly menu = input<Menu<V> | undefined>(undefined);

/** Context data to be passed to the menu's template. */
readonly menuData = input<unknown>(null);

/** Whether the menu is expanded. */
readonly expanded = computed(() => this._pattern.expanded());

Expand Down
28 changes: 22 additions & 6 deletions src/aria/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -1263,17 +1277,17 @@ class StandaloneMenuExample {

@Component({
template: `
<button ngMenuTrigger [menu]="menu">Open menu</button>
<button ngMenuTrigger [menu]="menu" [menuData]="menuData()">Open menu</button>

<div ngMenu [expansionDelay]="0" #menu="ngMenu" (itemSelected)="itemSelected($event)">
<ng-template ngMenuContent>
<div ngMenuItem value='Apple' searchTerm='Apple'>Apple</div>
<ng-template ngMenuContent let-data>
<div ngMenuItem value='Apple' searchTerm='Apple'>Apple {{data}}</div>
<div ngMenuItem value='Banana' searchTerm='Banana'>Banana</div>
<div ngMenuItem value='Berries' searchTerm='Berries' [submenu]="berriesMenu">Berries</div>
<div ngMenuItem value='Berries' searchTerm='Berries' [submenu]="berriesMenu" [submenuData]="submenuData()">Berries</div>

<div ngMenu [expansionDelay]="0" #berriesMenu="ngMenu">
<ng-template ngMenuContent>
<div ngMenuItem value='Blueberry' searchTerm='Blueberry'>Blueberry</div>
<ng-template ngMenuContent let-subdata>
<div ngMenuItem value='Blueberry' searchTerm='Blueberry'>Blueberry {{subdata}}</div>
<div ngMenuItem value='Blackberry' searchTerm='Blackberry'>Blackberry</div>
<div ngMenuItem value='Strawberry' searchTerm='Strawberry'>Strawberry</div>
</ng-template>
Expand All @@ -1288,6 +1302,8 @@ class StandaloneMenuExample {
})
class MenuTriggerExample {
itemSelected(value: string) {}
menuData = signal<unknown>(null);
submenuData = signal<unknown>(null);
}

@Component({
Expand Down
14 changes: 11 additions & 3 deletions src/aria/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,19 @@ export class Menu<V> 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(),
);
}
},
Expand Down
28 changes: 20 additions & 8 deletions src/aria/private/deferred-content/deferred-content.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand All @@ -70,9 +78,13 @@ class Collapsible {
private readonly _deferredContentAware = inject(DeferredContentAware);

contentVisible = signal(true);
context = signal<unknown>(null);

constructor() {
effect(() => this._deferredContentAware.contentVisible.set(this.contentVisible()));
effect(() => {
this._deferredContentAware.context.set(this.context());
this._deferredContentAware.contentVisible.set(this.contentVisible());
});
}
}

Expand All @@ -85,8 +97,8 @@ class CollapsibleContent {}
@Component({
template: `
<div collapsible [preserveContent]="preserveContent()">
<ng-template collapsibleContent>
Lazy Content
<ng-template collapsibleContent let-context="context">
Lazy Content {{context}}
</ng-template>
</div>
`,
Expand Down
15 changes: 12 additions & 3 deletions src/aria/private/deferred-content/deferred-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
export class DeferredContentAware {
readonly contentVisible = signal(false);
readonly preserveContent = model(false);
readonly context = signal<unknown>(null);
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down
Loading