Skip to content

Commit 41f1d22

Browse files
committed
feat(cdk/menu): add backdrop options to context menu and menu trigger
Currently, we do not have backdrop option for cdk menu. This change will add the those features. Fixes #31399
1 parent d02338b commit 41f1d22

File tree

7 files changed

+211
-3
lines changed

7 files changed

+211
-3
lines changed

goldens/cdk/menu/index.api.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ import { ViewContainerRef } from '@angular/core';
3636
// @public
3737
export const CDK_MENU: InjectionToken<Menu>;
3838

39+
// @public
40+
export const CDK_MENU_DEFAULT_OPTIONS: InjectionToken<CdkMenuDefaultOptions>;
41+
3942
// @public
4043
export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestroy {
4144
constructor();
@@ -45,6 +48,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
4548
static ngAcceptInputType_disabled: unknown;
4649
open(coordinates: ContextMenuCoordinates): void;
4750
_openOnContextMenu(event: MouseEvent): void;
51+
readonly _overlayPanelClass: string[];
4852
// (undocumented)
4953
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkContextMenuTrigger, "[cdkContextMenuTriggerFor]", ["cdkContextMenuTriggerFor"], { "menuTemplateRef": { "alias": "cdkContextMenuTriggerFor"; "required": false; }; "menuPosition": { "alias": "cdkContextMenuPosition"; "required": false; }; "menuData": { "alias": "cdkContextMenuTriggerData"; "required": false; }; "disabled": { "alias": "cdkContextMenuDisabled"; "required": false; }; }, { "opened": "cdkContextMenuOpened"; "closed": "cdkContextMenuClosed"; }, never, never, true, never>;
5054
// (undocumented)
@@ -115,6 +119,13 @@ export abstract class CdkMenuBase extends CdkMenuGroup implements Menu, AfterCon
115119
static ɵfac: i0.ɵɵFactoryDeclaration<CdkMenuBase, never>;
116120
}
117121

122+
// @public
123+
export interface CdkMenuDefaultOptions {
124+
backdropClass?: string;
125+
hasBackdrop?: boolean;
126+
overlayPanelClass?: string | string[];
127+
}
128+
118129
// @public
119130
export class CdkMenuGroup {
120131
// (undocumented)
@@ -220,6 +231,7 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD
220231
// (undocumented)
221232
ngOnDestroy(): void;
222233
open(): void;
234+
readonly _overlayPanelClass: string[];
223235
_setHasFocus(hasFocus: boolean): void;
224236
toggle(): void;
225237
_toggleOnKeydown(event: KeyboardEvent): void;

src/cdk/menu/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ ng_project(
4242
"//:node_modules/rxjs",
4343
"//src/cdk/collections",
4444
"//src/cdk/keycodes",
45+
"//src/cdk/overlay",
4546
"//src/cdk/testing/private",
4647
],
4748
)

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

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, ViewChild, ElementRef, ViewChildren, QueryList} from '@angular/core';
2-
import {TestBed, ComponentFixture} from '@angular/core/testing';
2+
import {TestBed, ComponentFixture, fakeAsync, tick} from '@angular/core/testing';
33
import {CdkMenu} from './menu';
44
import {CdkContextMenuTrigger} from './context-menu-trigger';
55
import {dispatchKeyboardEvent, dispatchMouseEvent} from '../testing/private';
@@ -8,6 +8,8 @@ import {CdkMenuItem} from './menu-item';
88
import {CdkMenuTrigger} from './menu-trigger';
99
import {CdkMenuBar} from './menu-bar';
1010
import {LEFT_ARROW, RIGHT_ARROW} from '../keycodes';
11+
import {OverlayContainer} from '../overlay';
12+
import {CDK_MENU_DEFAULT_OPTIONS} from './menu-trigger-base';
1113

1214
describe('CdkContextMenuTrigger', () => {
1315
describe('with simple context menu trigger', () => {
@@ -380,6 +382,77 @@ describe('CdkContextMenuTrigger', () => {
380382
});
381383
});
382384

385+
describe('with backdrop in options', () => {
386+
let fixture: ComponentFixture<SimpleContextMenu>;
387+
let overlayContainerElement: HTMLElement;
388+
389+
beforeEach(() => {
390+
fixture = TestBed.createComponent(SimpleContextMenu);
391+
fixture.detectChanges();
392+
});
393+
394+
/** Get the context in which the context menu should trigger. */
395+
function getMenuTrigger() {
396+
return fixture.componentInstance.triggerElement.nativeElement;
397+
}
398+
399+
/** Open up the context menu and run change detection. */
400+
function openContextMenu() {
401+
// right click triggers a context menu event
402+
dispatchMouseEvent(getMenuTrigger(), 'contextmenu');
403+
fixture.detectChanges();
404+
}
405+
406+
it('should not contain backdrop by default', fakeAsync(() => {
407+
openContextMenu();
408+
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
409+
fixture.detectChanges();
410+
tick(500);
411+
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeFalsy();
412+
}));
413+
414+
it('should be able to add the backdrop using hasBackdrop option', fakeAsync(() => {
415+
TestBed.resetTestingModule();
416+
TestBed.configureTestingModule({
417+
providers: [{provide: CDK_MENU_DEFAULT_OPTIONS, useValue: {hasBackdrop: true}}],
418+
});
419+
420+
fixture = TestBed.createComponent(SimpleContextMenu);
421+
fixture.detectChanges();
422+
423+
openContextMenu();
424+
425+
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
426+
fixture.detectChanges();
427+
tick(500);
428+
429+
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeTruthy();
430+
}));
431+
432+
it('should be able to add the custom backdrop class using backdropClass option', fakeAsync(() => {
433+
TestBed.resetTestingModule();
434+
TestBed.configureTestingModule({
435+
providers: [
436+
{
437+
provide: CDK_MENU_DEFAULT_OPTIONS,
438+
useValue: {hasBackdrop: true, backdropClass: 'custom-backdrop'},
439+
},
440+
],
441+
});
442+
443+
fixture = TestBed.createComponent(SimpleContextMenu);
444+
fixture.detectChanges();
445+
446+
openContextMenu();
447+
448+
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
449+
fixture.detectChanges();
450+
tick(500);
451+
452+
expect(overlayContainerElement.querySelector('.custom-backdrop')).toBeTruthy();
453+
}));
454+
});
455+
383456
describe('with shared triggered menu', () => {
384457
it('should allow a context menu and menubar trigger share a menu', () => {
385458
const fixture = TestBed.createComponent(MenuBarAndContextTriggerShareMenu);

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,14 @@ import {_getEventTarget} from '../platform';
2727
import {merge, partition} from 'rxjs';
2828
import {skip, takeUntil, skipWhile} from 'rxjs/operators';
2929
import {MENU_STACK, MenuStack} from './menu-stack';
30-
import {CdkMenuTriggerBase, MENU_TRIGGER, MenuTracker} from './menu-trigger-base';
30+
import {
31+
CDK_MENU_DEFAULT_OPTIONS,
32+
CdkMenuDefaultOptions,
33+
CdkMenuTriggerBase,
34+
MENU_TRIGGER,
35+
MenuTracker,
36+
} from './menu-trigger-base';
37+
import {coerceArray} from '../coercion';
3138

3239
/** The preferred menu positions for the context menu. */
3340
const CONTEXT_MENU_POSITIONS = STANDARD_DROPDOWN_BELOW_POSITIONS.map(position => {
@@ -78,6 +85,13 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
7885

7986
private readonly _changeDetectorRef = inject(ChangeDetectorRef);
8087

88+
private _defaults = inject<CdkMenuDefaultOptions | null>(CDK_MENU_DEFAULT_OPTIONS, {
89+
optional: true,
90+
});
91+
92+
/** Classes to apply to the panel. */
93+
readonly _overlayPanelClass = coerceArray(this._defaults?.overlayPanelClass || []);
94+
8195
/** Whether the context menu is disabled. */
8296
@Input({alias: 'cdkContextMenuDisabled', transform: booleanAttribute}) disabled: boolean = false;
8397

@@ -137,6 +151,11 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
137151
positionStrategy: this._getOverlayPositionStrategy(coordinates),
138152
scrollStrategy: this.menuScrollStrategy(),
139153
direction: this._directionality || undefined,
154+
...(this.menuStack.isEmpty() && {
155+
hasBackdrop: this._defaults?.hasBackdrop,
156+
panelClass: this._overlayPanelClass,
157+
backdropClass: this._defaults?.backdropClass || 'cdk-overlay-transparent-backdrop',
158+
}),
140159
});
141160
}
142161

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,29 @@ export const MENU_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>(
4343
},
4444
);
4545

46+
/** Default `cdk-menu` options that can be overridden. */
47+
export interface CdkMenuDefaultOptions {
48+
/** Class to be applied to the menu's backdrop. */
49+
backdropClass?: string;
50+
51+
/** Whether the menu has a backdrop. */
52+
hasBackdrop?: boolean;
53+
54+
/** Class or list of classes to be applied to the menu's overlay panel. */
55+
overlayPanelClass?: string | string[];
56+
}
57+
58+
/** Injection token to be used to override the default options for `cdk-menu`. */
59+
export const CDK_MENU_DEFAULT_OPTIONS = new InjectionToken<CdkMenuDefaultOptions>(
60+
'cdk-menu-default-options',
61+
{
62+
providedIn: 'root',
63+
factory: () => ({
64+
hasBackdrop: false,
65+
}),
66+
},
67+
);
68+
4669
/** Tracks the last open menu trigger across the entire application. */
4770
@Injectable({providedIn: 'root'})
4871
export class MenuTracker {

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {Menu} from './menu-interface';
88
import {CdkMenuItem} from './menu-item';
99
import {CdkMenuTrigger} from './menu-trigger';
1010
import {CdkMenuBar} from './menu-bar';
11+
import {OverlayContainer} from '../overlay';
12+
import {CDK_MENU_DEFAULT_OPTIONS} from './menu-trigger-base';
1113

1214
describe('MenuTrigger', () => {
1315
describe('on CdkMenuItem', () => {
@@ -515,6 +517,65 @@ describe('MenuTrigger', () => {
515517
});
516518
});
517519

520+
describe('with backdrop in options', () => {
521+
let overlayContainerElement: HTMLElement;
522+
523+
it('should not contain backdrop by default', fakeAsync(() => {
524+
const fixture = TestBed.createComponent(MenuBarWithNestedSubMenus);
525+
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
526+
fixture.detectChanges();
527+
528+
const triggers = fixture.componentInstance.triggers.toArray();
529+
triggers[0].toggle();
530+
fixture.detectChanges();
531+
532+
tick(500);
533+
534+
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeFalsy();
535+
}));
536+
537+
it('should be able to add the backdrop using hasBackdrop option', fakeAsync(() => {
538+
TestBed.resetTestingModule();
539+
TestBed.configureTestingModule({
540+
providers: [{provide: CDK_MENU_DEFAULT_OPTIONS, useValue: {hasBackdrop: true}}],
541+
});
542+
543+
const fixture = TestBed.createComponent(MenuBarWithNestedSubMenus);
544+
fixture.detectChanges();
545+
546+
const triggers = fixture.componentInstance.triggers.toArray();
547+
triggers[0].toggle();
548+
fixture.detectChanges();
549+
550+
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
551+
tick(500);
552+
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeTruthy();
553+
}));
554+
555+
it('should be able to add the custom backdrop class using backdropClass option', fakeAsync(() => {
556+
TestBed.resetTestingModule();
557+
TestBed.configureTestingModule({
558+
providers: [
559+
{
560+
provide: CDK_MENU_DEFAULT_OPTIONS,
561+
useValue: {hasBackdrop: true, backdropClass: 'custom-backdrop'},
562+
},
563+
],
564+
});
565+
566+
const fixture = TestBed.createComponent(MenuBarWithNestedSubMenus);
567+
fixture.detectChanges();
568+
const triggers = fixture.componentInstance.triggers.toArray();
569+
triggers[0].toggle();
570+
fixture.detectChanges();
571+
572+
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
573+
tick(500);
574+
575+
expect(overlayContainerElement.querySelector('.custom-backdrop')).toBeTruthy();
576+
}));
577+
});
578+
518579
it('should focus the first item when opening on click', fakeAsync(() => {
519580
const fixture = TestBed.createComponent(TriggersWithSameMenuDifferentMenuBars);
520581
fixture.detectChanges();

src/cdk/menu/menu-trigger.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,15 @@ import {takeUntil} from 'rxjs/operators';
4343
import {CDK_MENU, Menu} from './menu-interface';
4444
import {PARENT_OR_NEW_MENU_STACK_PROVIDER} from './menu-stack';
4545
import {MENU_AIM} from './menu-aim';
46-
import {CdkMenuTriggerBase, MENU_TRIGGER, MenuTracker} from './menu-trigger-base';
46+
import {
47+
CDK_MENU_DEFAULT_OPTIONS,
48+
CdkMenuDefaultOptions,
49+
CdkMenuTriggerBase,
50+
MENU_TRIGGER,
51+
MenuTracker,
52+
} from './menu-trigger-base';
4753
import {eventDispatchesNativeClick} from './event-detection';
54+
import {coerceArray} from '../coercion';
4855

4956
/**
5057
* A directive that turns its host element into a trigger for a popup menu.
@@ -86,6 +93,13 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD
8693
private readonly _injector = inject(Injector);
8794
private _cleanupMouseenter: () => void;
8895

96+
private _defaults = inject<CdkMenuDefaultOptions | null>(CDK_MENU_DEFAULT_OPTIONS, {
97+
optional: true,
98+
});
99+
100+
/** Classes to apply to the panel. */
101+
readonly _overlayPanelClass = coerceArray(this._defaults?.overlayPanelClass || []);
102+
89103
/** The app's menu tracking registry */
90104
private readonly _menuTracker = inject(MenuTracker);
91105

@@ -276,6 +290,11 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD
276290
positionStrategy: this._getOverlayPositionStrategy(),
277291
scrollStrategy: this.menuScrollStrategy(),
278292
direction: this._directionality || undefined,
293+
...(this.menuStack.isEmpty() && {
294+
hasBackdrop: this._defaults?.hasBackdrop,
295+
panelClass: this._overlayPanelClass,
296+
backdropClass: this._defaults?.backdropClass || 'cdk-overlay-transparent-backdrop',
297+
}),
279298
});
280299
}
281300

0 commit comments

Comments
 (0)