Skip to content
Open
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
12 changes: 12 additions & 0 deletions goldens/cdk/menu/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ import { ViewContainerRef } from '@angular/core';
// @public
export const CDK_MENU: InjectionToken<Menu>;

// @public
export const CDK_MENU_DEFAULT_OPTIONS: InjectionToken<CdkMenuDefaultOptions>;

// @public
export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestroy {
constructor();
Expand All @@ -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<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>;
// (undocumented)
Expand Down Expand Up @@ -115,6 +119,13 @@ export abstract class CdkMenuBase extends CdkMenuGroup implements Menu, AfterCon
static ɵfac: i0.ɵɵFactoryDeclaration<CdkMenuBase, never>;
}

// @public
export interface CdkMenuDefaultOptions {
backdropClass?: string;
hasBackdrop?: boolean;
overlayPanelClass?: string | string[];
}

// @public
export class CdkMenuGroup {
// (undocumented)
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/cdk/menu/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ng_project(
"//:node_modules/rxjs",
"//src/cdk/collections",
"//src/cdk/keycodes",
"//src/cdk/overlay",
"//src/cdk/testing/private",
],
)
Expand Down
75 changes: 74 additions & 1 deletion src/cdk/menu/context-menu-trigger.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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', () => {
Expand Down Expand Up @@ -380,6 +382,77 @@ describe('CdkContextMenuTrigger', () => {
});
});

describe('with backdrop in options', () => {
let fixture: ComponentFixture<SimpleContextMenu>;
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);
Expand Down
21 changes: 20 additions & 1 deletion src/cdk/menu/context-menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -78,6 +85,13 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr

private readonly _changeDetectorRef = inject(ChangeDetectorRef);

private _defaults = inject<CdkMenuDefaultOptions | null>(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;

Expand Down Expand Up @@ -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',
}),
});
}

Expand Down
23 changes: 23 additions & 0 deletions src/cdk/menu/menu-trigger-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CdkMenuDefaultOptions>(
'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 {
Expand Down
61 changes: 61 additions & 0 deletions src/cdk/menu/menu-trigger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
Expand Down
21 changes: 20 additions & 1 deletion src/cdk/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -86,6 +93,13 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD
private readonly _injector = inject(Injector);
private _cleanupMouseenter: () => void;

private _defaults = inject<CdkMenuDefaultOptions | null>(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);

Expand Down Expand Up @@ -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',
}),
});
}

Expand Down
Loading