diff --git a/goldens/cdk/menu/index.api.md b/goldens/cdk/menu/index.api.md index 27be93023ab9..21ea67189cf4 100644 --- a/goldens/cdk/menu/index.api.md +++ b/goldens/cdk/menu/index.api.md @@ -36,6 +36,9 @@ import { ViewContainerRef } from '@angular/core'; // @public export const CDK_MENU: InjectionToken; +// @public +export const CDK_MENU_DEFAULT_OPTIONS: InjectionToken; + // @public export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestroy { constructor(); @@ -45,6 +48,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr static ngAcceptInputType_disabled: unknown; open(coordinates: ContextMenuCoordinates): void; _openOnContextMenu(event: MouseEvent): void; + readonly _overlayPanelClass: string[]; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) @@ -115,6 +119,13 @@ export abstract class CdkMenuBase extends CdkMenuGroup implements Menu, AfterCon static ɵfac: i0.ɵɵFactoryDeclaration; } +// @public +export interface CdkMenuDefaultOptions { + backdropClass?: string; + hasBackdrop?: boolean; + overlayPanelClass?: string | string[]; +} + // @public export class CdkMenuGroup { // (undocumented) @@ -220,6 +231,7 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD // (undocumented) ngOnDestroy(): void; open(): void; + readonly _overlayPanelClass: string[]; _setHasFocus(hasFocus: boolean): void; toggle(): void; _toggleOnKeydown(event: KeyboardEvent): void; diff --git a/src/cdk/menu/BUILD.bazel b/src/cdk/menu/BUILD.bazel index 3dba25dec19e..2e64a5ac53ac 100644 --- a/src/cdk/menu/BUILD.bazel +++ b/src/cdk/menu/BUILD.bazel @@ -42,6 +42,7 @@ ng_project( "//:node_modules/rxjs", "//src/cdk/collections", "//src/cdk/keycodes", + "//src/cdk/overlay", "//src/cdk/testing/private", ], ) diff --git a/src/cdk/menu/context-menu-trigger.spec.ts b/src/cdk/menu/context-menu-trigger.spec.ts index 84a8043777af..53ed7be2b87f 100644 --- a/src/cdk/menu/context-menu-trigger.spec.ts +++ b/src/cdk/menu/context-menu-trigger.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild, ElementRef, ViewChildren, QueryList} from '@angular/core'; -import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {TestBed, ComponentFixture, fakeAsync, tick} from '@angular/core/testing'; import {CdkMenu} from './menu'; import {CdkContextMenuTrigger} from './context-menu-trigger'; import {dispatchKeyboardEvent, dispatchMouseEvent} from '../testing/private'; @@ -8,6 +8,8 @@ import {CdkMenuItem} from './menu-item'; import {CdkMenuTrigger} from './menu-trigger'; import {CdkMenuBar} from './menu-bar'; import {LEFT_ARROW, RIGHT_ARROW} from '../keycodes'; +import {OverlayContainer} from '../overlay'; +import {CDK_MENU_DEFAULT_OPTIONS} from './menu-trigger-base'; describe('CdkContextMenuTrigger', () => { describe('with simple context menu trigger', () => { @@ -380,6 +382,77 @@ describe('CdkContextMenuTrigger', () => { }); }); + describe('with backdrop in options', () => { + let fixture: ComponentFixture; + let overlayContainerElement: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(SimpleContextMenu); + fixture.detectChanges(); + }); + + /** Get the context in which the context menu should trigger. */ + function getMenuTrigger() { + return fixture.componentInstance.triggerElement.nativeElement; + } + + /** Open up the context menu and run change detection. */ + function openContextMenu() { + // right click triggers a context menu event + dispatchMouseEvent(getMenuTrigger(), 'contextmenu'); + fixture.detectChanges(); + } + + it('should not contain backdrop by default', fakeAsync(() => { + openContextMenu(); + overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement(); + fixture.detectChanges(); + tick(500); + expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeFalsy(); + })); + + it('should be able to add the backdrop using hasBackdrop option', fakeAsync(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [{provide: CDK_MENU_DEFAULT_OPTIONS, useValue: {hasBackdrop: true}}], + }); + + fixture = TestBed.createComponent(SimpleContextMenu); + fixture.detectChanges(); + + openContextMenu(); + + overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement(); + fixture.detectChanges(); + tick(500); + + expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeTruthy(); + })); + + it('should be able to add the custom backdrop class using backdropClass option', fakeAsync(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + { + provide: CDK_MENU_DEFAULT_OPTIONS, + useValue: {hasBackdrop: true, backdropClass: 'custom-backdrop'}, + }, + ], + }); + + fixture = TestBed.createComponent(SimpleContextMenu); + fixture.detectChanges(); + + openContextMenu(); + + overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement(); + fixture.detectChanges(); + tick(500); + + expect(overlayContainerElement.querySelector('.custom-backdrop')).toBeTruthy(); + })); + }); + describe('with shared triggered menu', () => { it('should allow a context menu and menubar trigger share a menu', () => { const fixture = TestBed.createComponent(MenuBarAndContextTriggerShareMenu); diff --git a/src/cdk/menu/context-menu-trigger.ts b/src/cdk/menu/context-menu-trigger.ts index bf1b5a86ea67..895ebc3973f9 100644 --- a/src/cdk/menu/context-menu-trigger.ts +++ b/src/cdk/menu/context-menu-trigger.ts @@ -27,7 +27,14 @@ import {_getEventTarget} from '../platform'; import {merge, partition} from 'rxjs'; import {skip, takeUntil, skipWhile} from 'rxjs/operators'; import {MENU_STACK, MenuStack} from './menu-stack'; -import {CdkMenuTriggerBase, MENU_TRIGGER, MenuTracker} from './menu-trigger-base'; +import { + CDK_MENU_DEFAULT_OPTIONS, + CdkMenuDefaultOptions, + CdkMenuTriggerBase, + MENU_TRIGGER, + MenuTracker, +} from './menu-trigger-base'; +import {coerceArray} from '../coercion'; /** The preferred menu positions for the context menu. */ const CONTEXT_MENU_POSITIONS = STANDARD_DROPDOWN_BELOW_POSITIONS.map(position => { @@ -78,6 +85,13 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr private readonly _changeDetectorRef = inject(ChangeDetectorRef); + private _defaults = inject(CDK_MENU_DEFAULT_OPTIONS, { + optional: true, + }); + + /** Classes to apply to the panel. */ + readonly _overlayPanelClass = coerceArray(this._defaults?.overlayPanelClass || []); + /** Whether the context menu is disabled. */ @Input({alias: 'cdkContextMenuDisabled', transform: booleanAttribute}) disabled: boolean = false; @@ -137,6 +151,11 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr positionStrategy: this._getOverlayPositionStrategy(coordinates), scrollStrategy: this.menuScrollStrategy(), direction: this._directionality || undefined, + ...(this.menuStack.isEmpty() && { + hasBackdrop: this._defaults?.hasBackdrop, + panelClass: this._overlayPanelClass, + backdropClass: this._defaults?.backdropClass || 'cdk-overlay-transparent-backdrop', + }), }); } diff --git a/src/cdk/menu/menu-trigger-base.ts b/src/cdk/menu/menu-trigger-base.ts index 7656603cc00a..e4303a5b03b7 100644 --- a/src/cdk/menu/menu-trigger-base.ts +++ b/src/cdk/menu/menu-trigger-base.ts @@ -43,6 +43,29 @@ export const MENU_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>( }, ); +/** Default `cdk-menu` options that can be overridden. */ +export interface CdkMenuDefaultOptions { + /** Class to be applied to the menu's backdrop. */ + backdropClass?: string; + + /** Whether the menu has a backdrop. */ + hasBackdrop?: boolean; + + /** Class or list of classes to be applied to the menu's overlay panel. */ + overlayPanelClass?: string | string[]; +} + +/** Injection token to be used to override the default options for `cdk-menu`. */ +export const CDK_MENU_DEFAULT_OPTIONS = new InjectionToken( + 'cdk-menu-default-options', + { + providedIn: 'root', + factory: () => ({ + hasBackdrop: false, + }), + }, +); + /** Tracks the last open menu trigger across the entire application. */ @Injectable({providedIn: 'root'}) export class MenuTracker { diff --git a/src/cdk/menu/menu-trigger.spec.ts b/src/cdk/menu/menu-trigger.spec.ts index 5ab39eb512c3..28ae08213d5c 100644 --- a/src/cdk/menu/menu-trigger.spec.ts +++ b/src/cdk/menu/menu-trigger.spec.ts @@ -8,6 +8,8 @@ import {Menu} from './menu-interface'; import {CdkMenuItem} from './menu-item'; import {CdkMenuTrigger} from './menu-trigger'; import {CdkMenuBar} from './menu-bar'; +import {OverlayContainer} from '../overlay'; +import {CDK_MENU_DEFAULT_OPTIONS} from './menu-trigger-base'; describe('MenuTrigger', () => { describe('on CdkMenuItem', () => { @@ -515,6 +517,65 @@ describe('MenuTrigger', () => { }); }); + describe('with backdrop in options', () => { + let overlayContainerElement: HTMLElement; + + it('should not contain backdrop by default', fakeAsync(() => { + const fixture = TestBed.createComponent(MenuBarWithNestedSubMenus); + overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement(); + fixture.detectChanges(); + + const triggers = fixture.componentInstance.triggers.toArray(); + triggers[0].toggle(); + fixture.detectChanges(); + + tick(500); + + expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeFalsy(); + })); + + it('should be able to add the backdrop using hasBackdrop option', fakeAsync(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [{provide: CDK_MENU_DEFAULT_OPTIONS, useValue: {hasBackdrop: true}}], + }); + + const fixture = TestBed.createComponent(MenuBarWithNestedSubMenus); + fixture.detectChanges(); + + const triggers = fixture.componentInstance.triggers.toArray(); + triggers[0].toggle(); + fixture.detectChanges(); + + overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement(); + tick(500); + expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeTruthy(); + })); + + it('should be able to add the custom backdrop class using backdropClass option', fakeAsync(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + { + provide: CDK_MENU_DEFAULT_OPTIONS, + useValue: {hasBackdrop: true, backdropClass: 'custom-backdrop'}, + }, + ], + }); + + const fixture = TestBed.createComponent(MenuBarWithNestedSubMenus); + fixture.detectChanges(); + const triggers = fixture.componentInstance.triggers.toArray(); + triggers[0].toggle(); + fixture.detectChanges(); + + overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement(); + tick(500); + + expect(overlayContainerElement.querySelector('.custom-backdrop')).toBeTruthy(); + })); + }); + it('should focus the first item when opening on click', fakeAsync(() => { const fixture = TestBed.createComponent(TriggersWithSameMenuDifferentMenuBars); fixture.detectChanges(); diff --git a/src/cdk/menu/menu-trigger.ts b/src/cdk/menu/menu-trigger.ts index d37ab81a4205..a504aa272c0b 100644 --- a/src/cdk/menu/menu-trigger.ts +++ b/src/cdk/menu/menu-trigger.ts @@ -43,8 +43,15 @@ import {takeUntil} from 'rxjs/operators'; import {CDK_MENU, Menu} from './menu-interface'; import {PARENT_OR_NEW_MENU_STACK_PROVIDER} from './menu-stack'; import {MENU_AIM} from './menu-aim'; -import {CdkMenuTriggerBase, MENU_TRIGGER, MenuTracker} from './menu-trigger-base'; +import { + CDK_MENU_DEFAULT_OPTIONS, + CdkMenuDefaultOptions, + CdkMenuTriggerBase, + MENU_TRIGGER, + MenuTracker, +} from './menu-trigger-base'; import {eventDispatchesNativeClick} from './event-detection'; +import {coerceArray} from '../coercion'; /** * 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 private readonly _injector = inject(Injector); private _cleanupMouseenter: () => void; + private _defaults = inject(CDK_MENU_DEFAULT_OPTIONS, { + optional: true, + }); + + /** Classes to apply to the panel. */ + readonly _overlayPanelClass = coerceArray(this._defaults?.overlayPanelClass || []); + /** The app's menu tracking registry */ private readonly _menuTracker = inject(MenuTracker); @@ -276,6 +290,11 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD positionStrategy: this._getOverlayPositionStrategy(), scrollStrategy: this.menuScrollStrategy(), direction: this._directionality || undefined, + ...(this.menuStack.isEmpty() && { + hasBackdrop: this._defaults?.hasBackdrop, + panelClass: this._overlayPanelClass, + backdropClass: this._defaults?.backdropClass || 'cdk-overlay-transparent-backdrop', + }), }); }