Skip to content

Commit 299ab9e

Browse files
committed
feat(cdk/menu): add backdrop options to context menu and menu trigger
Fixes #31399
1 parent d02338b commit 299ab9e

File tree

5 files changed

+195
-4
lines changed

5 files changed

+195
-4
lines changed

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

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {Component, ViewChild, ElementRef, ViewChildren, QueryList} from '@angular/core';
2-
import {TestBed, ComponentFixture} from '@angular/core/testing';
3-
import {CdkMenu} from './menu';
2+
import {TestBed, ComponentFixture, fakeAsync, tick} from '@angular/core/testing';
3+
import {CDK_MENU_DEFAULT_OPTIONS, CdkMenu} from './menu';
44
import {CdkContextMenuTrigger} from './context-menu-trigger';
55
import {dispatchKeyboardEvent, dispatchMouseEvent} from '../testing/private';
66
import {By} from '@angular/platform-browser';
77
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';
1112

1213
describe('CdkContextMenuTrigger', () => {
1314
describe('with simple context menu trigger', () => {
@@ -380,6 +381,77 @@ describe('CdkContextMenuTrigger', () => {
380381
});
381382
});
382383

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

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {merge, partition} from 'rxjs';
2828
import {skip, takeUntil, skipWhile} from 'rxjs/operators';
2929
import {MENU_STACK, MenuStack} from './menu-stack';
3030
import {CdkMenuTriggerBase, MENU_TRIGGER, MenuTracker} from './menu-trigger-base';
31+
import {CDK_MENU_DEFAULT_OPTIONS, CdkMenuDefaultOptions} from './menu';
32+
import {coerceArray} from '../coercion';
3133

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

7981
private readonly _changeDetectorRef = inject(ChangeDetectorRef);
8082

83+
private _defaults = inject<CdkMenuDefaultOptions | null>(CDK_MENU_DEFAULT_OPTIONS, {
84+
optional: true,
85+
});
86+
87+
/** Classes to apply to the panel. */
88+
readonly _overlayPanelClass = coerceArray(this._defaults?.overlayPanelClass || []);
89+
8190
/** Whether the context menu is disabled. */
8291
@Input({alias: 'cdkContextMenuDisabled', transform: booleanAttribute}) disabled: boolean = false;
8392

@@ -137,6 +146,11 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
137146
positionStrategy: this._getOverlayPositionStrategy(coordinates),
138147
scrollStrategy: this.menuScrollStrategy(),
139148
direction: this._directionality || undefined,
149+
...(this.menuStack.isEmpty() && {
150+
hasBackdrop: this._defaults?.hasBackdrop,
151+
panelClass: this._overlayPanelClass,
152+
backdropClass: this._defaults?.backdropClass || 'cdk-overlay-transparent-backdrop',
153+
}),
140154
});
141155
}
142156

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import {Component, ElementRef, QueryList, ViewChild, ViewChildren} from '@angula
33
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
44
import {By} from '@angular/platform-browser';
55
import {dispatchKeyboardEvent} from '../../cdk/testing/private';
6-
import {CdkMenu} from './menu';
6+
import {CDK_MENU_DEFAULT_OPTIONS, CdkMenu} from './menu';
77
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';
1112

1213
describe('MenuTrigger', () => {
1314
describe('on CdkMenuItem', () => {
@@ -515,6 +516,65 @@ describe('MenuTrigger', () => {
515516
});
516517
});
517518

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

src/cdk/menu/menu-trigger.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import {PARENT_OR_NEW_MENU_STACK_PROVIDER} from './menu-stack';
4545
import {MENU_AIM} from './menu-aim';
4646
import {CdkMenuTriggerBase, MENU_TRIGGER, MenuTracker} from './menu-trigger-base';
4747
import {eventDispatchesNativeClick} from './event-detection';
48+
import {CDK_MENU_DEFAULT_OPTIONS, CdkMenuDefaultOptions} from './menu';
49+
import {coerceArray} from '../coercion';
4850

4951
/**
5052
* A directive that turns its host element into a trigger for a popup menu.
@@ -86,6 +88,13 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD
8688
private readonly _injector = inject(Injector);
8789
private _cleanupMouseenter: () => void;
8890

91+
private _defaults = inject<CdkMenuDefaultOptions | null>(CDK_MENU_DEFAULT_OPTIONS, {
92+
optional: true,
93+
});
94+
95+
/** Classes to apply to the panel. */
96+
readonly _overlayPanelClass = coerceArray(this._defaults?.overlayPanelClass || []);
97+
8998
/** The app's menu tracking registry */
9099
private readonly _menuTracker = inject(MenuTracker);
91100

@@ -276,6 +285,11 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD
276285
positionStrategy: this._getOverlayPositionStrategy(),
277286
scrollStrategy: this.menuScrollStrategy(),
278287
direction: this._directionality || undefined,
288+
...(this.menuStack.isEmpty() && {
289+
hasBackdrop: this._defaults?.hasBackdrop,
290+
panelClass: this._overlayPanelClass,
291+
backdropClass: this._defaults?.backdropClass || 'cdk-overlay-transparent-backdrop',
292+
}),
279293
});
280294
}
281295

src/cdk/menu/menu.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {AfterContentInit, Directive, EventEmitter, inject, OnDestroy, Output} from '@angular/core';
9+
import {
10+
AfterContentInit,
11+
Directive,
12+
EventEmitter,
13+
inject,
14+
InjectionToken,
15+
OnDestroy,
16+
Output,
17+
} from '@angular/core';
1018
import {ESCAPE, hasModifierKey, LEFT_ARROW, RIGHT_ARROW, TAB} from '../keycodes';
1119
import {takeUntil} from 'rxjs/operators';
1220
import {CdkMenuGroup} from './menu-group';
@@ -15,6 +23,29 @@ import {FocusNext, PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER} from './menu-stack'
1523
import {MENU_TRIGGER} from './menu-trigger-base';
1624
import {CdkMenuBase} from './menu-base';
1725

26+
/** Default `cdk-menu` options that can be overridden. */
27+
export interface CdkMenuDefaultOptions {
28+
/** Class to be applied to the menu's backdrop. */
29+
backdropClass?: string;
30+
31+
/** Whether the menu has a backdrop. */
32+
hasBackdrop?: boolean;
33+
34+
/** Class or list of classes to be applied to the menu's overlay panel. */
35+
overlayPanelClass?: string | string[];
36+
}
37+
38+
/** Injection token to be used to override the default options for `cdk-menu`. */
39+
export const CDK_MENU_DEFAULT_OPTIONS = new InjectionToken<CdkMenuDefaultOptions>(
40+
'cdk-menu-default-options',
41+
{
42+
providedIn: 'root',
43+
factory: () => ({
44+
hasBackdrop: false,
45+
}),
46+
},
47+
);
48+
1849
/**
1950
* Directive which configures the element as a Menu which should contain child elements marked as
2051
* CdkMenuItem or CdkMenuGroup. Sets the appropriate role and aria-attributes for a menu and

0 commit comments

Comments
 (0)