Skip to content

Commit e0a74b9

Browse files
authored
feat(cdk/menu): Allow setting cdkMenuTriggerFor null (#25818)
* feat(cdk/menu): Add tests for setting cdkMenuTriggerFor null * feat(cdk/menu): Allow setting cdkMenuTrigger null Add ability to set [cdkMenuTrigger]="null" as is now possible with material menu. Fixes #25782 * feat(cdk/menu): Update tests for setting cdkMenuTriggerFor null * feat(cdk/menu): Allow setting cdkMenuTrigger null - Update hasMenu - Add yarn approve-api cdk/menu - Uncomment xdescribe and simplify it only for cdkMenuTrigger * feat(cdk/menu): Allow setting cdkMenuTrigger null Remove unused import
1 parent edb2d56 commit e0a74b9

File tree

5 files changed

+96
-10
lines changed

5 files changed

+96
-10
lines changed

src/cdk/menu/menu-item.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
9393
@Output('cdkMenuItemTriggered') readonly triggered: EventEmitter<void> = new EventEmitter();
9494

9595
/** Whether the menu item opens a menu. */
96-
readonly hasMenu = !!this._menuTrigger;
96+
get hasMenu() {
97+
return this._menuTrigger?.menuTemplateRef != null;
98+
}
9799

98100
/**
99101
* The tabindex for this menu item managed internally and used for implementing roving a

src/cdk/menu/menu-trigger-base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export abstract class CdkMenuTriggerBase implements OnDestroy {
5858
readonly closed: EventEmitter<void> = new EventEmitter();
5959

6060
/** Template reference variable to the menu this trigger opens */
61-
menuTemplateRef: TemplateRef<unknown>;
61+
menuTemplateRef: TemplateRef<unknown> | null;
6262

6363
/** Context data to be passed along to the menu template */
6464
menuData: unknown;

src/cdk/menu/menu-trigger.spec.ts

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,36 @@ describe('MenuTrigger', () => {
4747
expect(menuItemElement.getAttribute('aria-disabled')).toBe('true');
4848
});
4949

50-
it('should set aria-haspopup to menu', () => {
50+
it('should set aria-haspopup based on whether a menu is assigned', () => {
5151
expect(menuItemElement.getAttribute('aria-haspopup')).toEqual('menu');
52+
53+
fixture.componentInstance.trigger.menuTemplateRef = null;
54+
fixture.detectChanges();
55+
56+
expect(menuItemElement.hasAttribute('aria-haspopup')).toBe(false);
5257
});
5358

54-
it('should have a menu', () => {
59+
it('should have a menu based on whether a menu is assigned', () => {
5560
expect(menuItem.hasMenu).toBeTrue();
61+
62+
fixture.componentInstance.trigger.menuTemplateRef = null;
63+
fixture.detectChanges();
64+
65+
expect(menuItem.hasMenu).toBeFalse();
66+
});
67+
68+
it('should set aria-controls based on whether a menu is assigned', () => {
69+
expect(menuItemElement.hasAttribute('aria-controls')).toBeFalse();
70+
});
71+
72+
it('should set aria-expanded based on whether a menu is assigned', () => {
73+
expect(menuItemElement.hasAttribute('aria-expanded')).toBeTrue();
74+
expect(menuItemElement.getAttribute('aria-expanded')).toBe('false');
75+
76+
fixture.componentInstance.trigger.menuTemplateRef = null;
77+
fixture.detectChanges();
78+
79+
expect(menuItemElement.hasAttribute('aria-expanded')).toBeFalse();
5680
});
5781
});
5882

@@ -469,6 +493,50 @@ describe('MenuTrigger', () => {
469493

470494
expect(document.querySelector('.test-menu')?.textContent).toBe('Hello!');
471495
});
496+
497+
describe('null triggerFor', () => {
498+
let fixture: ComponentFixture<TriggerWithNullValue>;
499+
500+
let nativeTrigger: HTMLElement;
501+
502+
beforeEach(waitForAsync(() => {
503+
TestBed.configureTestingModule({
504+
imports: [CdkMenuModule],
505+
declarations: [TriggerWithNullValue],
506+
}).compileComponents();
507+
}));
508+
509+
beforeEach(() => {
510+
fixture = TestBed.createComponent(TriggerWithNullValue);
511+
nativeTrigger = fixture.componentInstance.nativeTrigger.nativeElement;
512+
});
513+
514+
it('should not set aria-haspopup', () => {
515+
expect(nativeTrigger.hasAttribute('aria-haspopup')).toBeFalse();
516+
});
517+
518+
it('should not set aria-controls', () => {
519+
expect(nativeTrigger.hasAttribute('aria-controls')).toBeFalse();
520+
});
521+
522+
it('should not toggle the menu on trigger', () => {
523+
expect(fixture.componentInstance.trigger.isOpen()).toBeFalse();
524+
525+
nativeTrigger.click();
526+
fixture.detectChanges();
527+
528+
expect(fixture.componentInstance.trigger.isOpen()).toBeFalse();
529+
});
530+
531+
it('should not toggle the menu on keyboard events', () => {
532+
expect(fixture.componentInstance.trigger.isOpen()).toBeFalse();
533+
534+
dispatchKeyboardEvent(nativeTrigger, 'keydown', SPACE);
535+
fixture.detectChanges();
536+
537+
expect(fixture.componentInstance.trigger.isOpen()).toBeFalse();
538+
});
539+
});
472540
});
473541

474542
@Component({
@@ -477,7 +545,10 @@ describe('MenuTrigger', () => {
477545
<ng-template #noop><div cdkMenu></div></ng-template>
478546
`,
479547
})
480-
class TriggerForEmptyMenu {}
548+
class TriggerForEmptyMenu {
549+
@ViewChild(CdkMenuTrigger) trigger: CdkMenuTrigger;
550+
@ViewChild(CdkMenuTrigger, {read: ElementRef}) nativeTrigger: ElementRef;
551+
}
481552

482553
@Component({
483554
template: `
@@ -602,3 +673,16 @@ class StandaloneTriggerWithInlineMenu {
602673
class TriggerWithData {
603674
menuData: unknown;
604675
}
676+
677+
@Component({
678+
template: `
679+
<button [cdkMenuTriggerFor]="null">First</button>
680+
`,
681+
})
682+
class TriggerWithNullValue {
683+
@ViewChild(CdkMenuTrigger, {static: true})
684+
trigger: CdkMenuTrigger;
685+
686+
@ViewChild(CdkMenuTrigger, {static: true, read: ElementRef})
687+
nativeTrigger: ElementRef<HTMLElement>;
688+
}

src/cdk/menu/menu-trigger.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ import {CdkMenuTriggerBase, MENU_TRIGGER} from './menu-trigger-base';
4545
standalone: true,
4646
host: {
4747
'class': 'cdk-menu-trigger',
48-
'aria-haspopup': 'menu',
49-
'[attr.aria-expanded]': 'isOpen()',
48+
'[attr.aria-haspopup]': 'menuTemplateRef ? "menu" : null',
49+
'[attr.aria-expanded]': 'menuTemplateRef == null ? null : isOpen()',
5050
'(focusin)': '_setHasFocus(true)',
5151
'(focusout)': '_setHasFocus(false)',
5252
'(keydown)': '_toggleOnKeydown($event)',
@@ -99,7 +99,7 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy {
9999

100100
/** Open the attached menu. */
101101
open() {
102-
if (!this.isOpen()) {
102+
if (!this.isOpen() && this.menuTemplateRef != null) {
103103
this.opened.next();
104104

105105
this.overlayRef = this.overlayRef || this._overlay.create(this._getOverlayConfig());

tools/public_api_guard/cdk/menu.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
127127
getLabel(): string;
128128
getMenu(): Menu | undefined;
129129
getMenuTrigger(): CdkMenuTrigger | null;
130-
readonly hasMenu: boolean;
130+
get hasMenu(): boolean;
131131
isMenuOpen(): boolean;
132132
// (undocumented)
133133
ngOnDestroy(): void;
@@ -220,7 +220,7 @@ export abstract class CdkMenuTriggerBase implements OnDestroy {
220220
menuData: unknown;
221221
menuPosition: ConnectedPosition[];
222222
protected readonly menuStack: MenuStack;
223-
menuTemplateRef: TemplateRef<unknown>;
223+
menuTemplateRef: TemplateRef<unknown> | null;
224224
// (undocumented)
225225
ngOnDestroy(): void;
226226
readonly opened: EventEmitter<void>;

0 commit comments

Comments
 (0)