Skip to content

Commit d945b27

Browse files
authored
feat(cdk-experimental/menu): add context menu trigger directive (#20144)
Add a directive which opens an attached context menu when a user right clicks within the triggering element. The context menu trigger also considers nested context menu triggers within an element opting to open the lowest level non-disabled context menu.
1 parent 338c02f commit d945b27

File tree

4 files changed

+649
-0
lines changed

4 files changed

+649
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
import {Component, ViewChild, ElementRef} from '@angular/core';
2+
import {CdkMenuModule} from './menu-module';
3+
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
4+
import {CdkMenu} from './menu';
5+
import {CdkContextMenuTrigger} from './context-menu';
6+
import {dispatchMouseEvent} from '@angular/cdk/testing/private';
7+
import {By} from '@angular/platform-browser';
8+
import {CdkMenuItem} from './menu-item';
9+
import {CdkMenuItemTrigger} from './menu-item-trigger';
10+
11+
describe('CdkContextMenuTrigger', () => {
12+
describe('with simple context menu trigger', () => {
13+
let fixture: ComponentFixture<SimpleContextMenu>;
14+
15+
beforeEach(async(() => {
16+
TestBed.configureTestingModule({
17+
imports: [CdkMenuModule],
18+
declarations: [SimpleContextMenu],
19+
}).compileComponents();
20+
}));
21+
22+
beforeEach(() => {
23+
fixture = TestBed.createComponent(SimpleContextMenu);
24+
fixture.detectChanges();
25+
});
26+
27+
/** Get the menu opened by the context menu trigger. */
28+
function getContextMenu() {
29+
return fixture.componentInstance.menu;
30+
}
31+
32+
/** Return a reference to the context menu native element. */
33+
function getNativeContextMenu() {
34+
return fixture.componentInstance.nativeMenu?.nativeElement;
35+
}
36+
37+
/** Get the context in which the context menu should trigger. */
38+
function getMenuContext() {
39+
return fixture.componentInstance.trigger.nativeElement;
40+
}
41+
42+
/** Open up the context menu and run change detection. */
43+
function openContextMenu() {
44+
// right click triggers a context menu event
45+
dispatchMouseEvent(getMenuContext(), 'contextmenu');
46+
fixture.detectChanges();
47+
}
48+
49+
it('should display context menu on right click inside of context component', () => {
50+
expect(getContextMenu()).not.toBeDefined();
51+
openContextMenu();
52+
expect(getContextMenu()).toBeDefined();
53+
});
54+
55+
it('should close out the context menu when clicking in the context', () => {
56+
openContextMenu();
57+
58+
getMenuContext().click();
59+
fixture.detectChanges();
60+
61+
expect(getContextMenu()).not.toBeDefined();
62+
});
63+
64+
it('should close out the context menu when clicking on element outside of the context', () => {
65+
openContextMenu();
66+
67+
fixture.nativeElement.querySelector('#other').click();
68+
fixture.detectChanges();
69+
70+
expect(getContextMenu()).not.toBeDefined();
71+
});
72+
73+
it('should close out the context menu when clicking a menu item', () => {
74+
openContextMenu();
75+
76+
fixture.debugElement.query(By.directive(CdkMenuItem)).injector.get(CdkMenuItem).trigger();
77+
fixture.detectChanges();
78+
79+
expect(getContextMenu()).not.toBeDefined();
80+
});
81+
82+
it('should re-open the same menu when right clicking twice in the context', () => {
83+
openContextMenu();
84+
openContextMenu();
85+
86+
const menus = fixture.debugElement.queryAll(By.directive(CdkMenu));
87+
expect(menus.length)
88+
.withContext('two context menu triggers should result in a single context menu')
89+
.toBe(1);
90+
});
91+
92+
it('should retain the context menu on right click inside the open menu', () => {
93+
openContextMenu();
94+
95+
dispatchMouseEvent(getNativeContextMenu()!, 'contextmenu');
96+
fixture.detectChanges();
97+
98+
expect(getContextMenu()).toBeDefined();
99+
});
100+
});
101+
102+
describe('nested context menu triggers', () => {
103+
let fixture: ComponentFixture<NestedContextMenu>;
104+
105+
beforeEach(async(() => {
106+
TestBed.configureTestingModule({
107+
imports: [CdkMenuModule],
108+
declarations: [NestedContextMenu],
109+
}).compileComponents();
110+
}));
111+
112+
beforeEach(() => {
113+
fixture = TestBed.createComponent(NestedContextMenu);
114+
fixture.detectChanges();
115+
});
116+
117+
/** Get the cut context menu. */
118+
function getCutMenu() {
119+
return fixture.componentInstance.cutMenu;
120+
}
121+
122+
/** Get the copy context menu. */
123+
function getCopyMenu() {
124+
return fixture.componentInstance.copyMenu;
125+
}
126+
127+
/** Get the context in which the cut context menu should trigger. */
128+
function getCutMenuContext() {
129+
return fixture.componentInstance.cutContext.nativeElement;
130+
}
131+
132+
/** Get the context in which the copy context menu should trigger. */
133+
function getCopyMenuContext() {
134+
return fixture.componentInstance.copyContext.nativeElement;
135+
}
136+
137+
/** Open up the cut context menu and run change detection. */
138+
function openCutContextMenu() {
139+
// right click triggers a context menu event
140+
dispatchMouseEvent(getCutMenuContext(), 'contextmenu');
141+
fixture.detectChanges();
142+
}
143+
144+
/** Open up the copy context menu and run change detection. */
145+
function openCopyContextMenu() {
146+
// right click triggers a context menu event
147+
dispatchMouseEvent(getCopyMenuContext(), 'contextmenu');
148+
fixture.detectChanges();
149+
}
150+
151+
it('should open the cut context menu only when right clicked in its trigger context', () => {
152+
openCutContextMenu();
153+
154+
expect(getCutMenu()).toBeDefined();
155+
expect(getCopyMenu()).not.toBeDefined();
156+
});
157+
158+
it('should open the nested copy context menu only when right clicked in nested context', () => {
159+
openCopyContextMenu();
160+
161+
expect(getCopyMenu()).toBeDefined();
162+
expect(getCutMenu()).not.toBeDefined();
163+
});
164+
165+
it(
166+
'should open the parent context menu only when right clicked in nested context and nested' +
167+
' is disabled',
168+
() => {
169+
fixture.componentInstance.copyMenuDisabled = true;
170+
fixture.detectChanges();
171+
openCopyContextMenu();
172+
173+
expect(getCopyMenu()).not.toBeDefined();
174+
expect(getCutMenu()).toBeDefined();
175+
}
176+
);
177+
178+
it('should close nested context menu when parent is opened', () => {
179+
openCopyContextMenu();
180+
181+
openCutContextMenu();
182+
183+
expect(getCopyMenu()).not.toBeDefined();
184+
expect(getCutMenu()).toBeDefined();
185+
});
186+
187+
it('should close the parent context menu when nested is open', () => {
188+
openCutContextMenu();
189+
190+
openCopyContextMenu();
191+
192+
expect(getCopyMenu()).toBeDefined();
193+
expect(getCutMenu()).not.toBeDefined();
194+
});
195+
196+
it('should close nested context menu when clicking in parent', () => {
197+
openCopyContextMenu();
198+
199+
getCutMenuContext().click();
200+
fixture.detectChanges();
201+
202+
expect(getCopyMenu()).not.toBeDefined();
203+
});
204+
205+
it('should close parent context menu when clicking in nested menu', () => {
206+
openCutContextMenu();
207+
208+
getCopyMenuContext().click();
209+
fixture.detectChanges();
210+
211+
expect(getCutMenu()).not.toBeDefined();
212+
});
213+
});
214+
215+
describe('with context menu that has submenu', () => {
216+
let fixture: ComponentFixture<ContextMenuWithSubmenu>;
217+
let instance: ContextMenuWithSubmenu;
218+
219+
beforeEach(async(() => {
220+
TestBed.configureTestingModule({
221+
imports: [CdkMenuModule],
222+
declarations: [ContextMenuWithSubmenu],
223+
}).compileComponents();
224+
}));
225+
226+
beforeEach(() => {
227+
fixture = TestBed.createComponent(ContextMenuWithSubmenu);
228+
fixture.detectChanges();
229+
230+
instance = fixture.componentInstance;
231+
});
232+
233+
it('should open context menu submenu without closing context menu', () => {
234+
dispatchMouseEvent(instance.context.nativeElement, 'contextmenu');
235+
fixture.detectChanges();
236+
237+
instance.triggerNativeElement.nativeElement.click();
238+
fixture.detectChanges();
239+
240+
expect(instance.cutMenu).toBeDefined();
241+
expect(instance.copyMenu).toBeDefined();
242+
});
243+
});
244+
});
245+
246+
@Component({
247+
template: `
248+
<div [cdkContextMenuTriggerFor]="context"></div>
249+
<div id="other"></div>
250+
251+
<ng-template cdkMenuPanel #context="cdkMenuPanel">
252+
<div cdkMenu [cdkMenuPanel]="context">
253+
<button cdkMenuItem></button>
254+
</div>
255+
</ng-template>
256+
`,
257+
})
258+
class SimpleContextMenu {
259+
@ViewChild(CdkContextMenuTrigger, {read: ElementRef}) trigger: ElementRef<HTMLElement>;
260+
@ViewChild(CdkMenu) menu?: CdkMenu;
261+
@ViewChild(CdkMenu, {read: ElementRef}) nativeMenu?: ElementRef<HTMLElement>;
262+
}
263+
264+
@Component({
265+
template: `
266+
<div #cut_trigger [cdkContextMenuTriggerFor]="cut">
267+
<div
268+
#copy_trigger
269+
[cdkContextMenuDisabled]="copyMenuDisabled"
270+
[cdkContextMenuTriggerFor]="copy"
271+
></div>
272+
</div>
273+
274+
<ng-template cdkMenuPanel #cut="cdkMenuPanel">
275+
<div #cut_menu cdkMenu [cdkMenuPanel]="cut"></div>
276+
</ng-template>
277+
278+
<ng-template cdkMenuPanel #copy="cdkMenuPanel">
279+
<div #copy_menu cdkMenu [cdkMenuPanel]="copy"></div>
280+
</ng-template>
281+
`,
282+
})
283+
class NestedContextMenu {
284+
@ViewChild('cut_trigger', {read: ElementRef}) cutContext: ElementRef<HTMLElement>;
285+
@ViewChild('copy_trigger', {read: ElementRef}) copyContext: ElementRef<HTMLElement>;
286+
287+
@ViewChild('cut_menu', {read: CdkMenu}) cutMenu: CdkMenu;
288+
@ViewChild('copy_menu', {read: CdkMenu}) copyMenu: CdkMenu;
289+
290+
copyMenuDisabled = false;
291+
}
292+
293+
@Component({
294+
template: `
295+
<div [cdkContextMenuTriggerFor]="cut"></div>
296+
297+
<ng-template cdkMenuPanel #cut="cdkMenuPanel">
298+
<div #cut_menu cdkMenu [cdkMenuPanel]="cut">
299+
<button cdkMenuItem [cdkMenuTriggerFor]="copy"></button>
300+
</div>
301+
</ng-template>
302+
303+
<ng-template cdkMenuPanel #copy="cdkMenuPanel">
304+
<div #copy_menu cdkMenu [cdkMenuPanel]="copy"></div>
305+
</ng-template>
306+
`,
307+
})
308+
class ContextMenuWithSubmenu {
309+
@ViewChild(CdkContextMenuTrigger, {read: ElementRef}) context: ElementRef<HTMLElement>;
310+
@ViewChild(CdkMenuItemTrigger, {read: ElementRef}) triggerNativeElement: ElementRef<HTMLElement>;
311+
312+
@ViewChild('cut_menu', {read: CdkMenu}) cutMenu: CdkMenu;
313+
@ViewChild('copy_menu', {read: CdkMenu}) copyMenu: CdkMenu;
314+
}

0 commit comments

Comments
 (0)