1- import { TestBed , async } from '@angular/core/testing' ;
1+ import { TestBed , async , ComponentFixture } from '@angular/core/testing' ;
22import { By } from '@angular/platform-browser' ;
33import {
44 Component ,
55 ElementRef ,
66 EventEmitter ,
7+ Input ,
78 Output ,
89 TemplateRef ,
910 ViewChild
@@ -18,6 +19,7 @@ import {
1819import { OverlayContainer } from '../core/overlay/overlay-container' ;
1920import { ViewportRuler } from '../core/overlay/position/viewport-ruler' ;
2021import { Dir , LayoutDirection } from '../core/rtl/dir' ;
22+ import { extendObject } from '../core/util/object-extend' ;
2123
2224describe ( 'MdMenu' , ( ) => {
2325 let overlayContainerElement : HTMLElement ;
@@ -27,7 +29,7 @@ describe('MdMenu', () => {
2729 dir = 'ltr' ;
2830 TestBed . configureTestingModule ( {
2931 imports : [ MdMenuModule . forRoot ( ) ] ,
30- declarations : [ SimpleMenu , PositionedMenu , CustomMenuPanel , CustomMenu ] ,
32+ declarations : [ SimpleMenu , PositionedMenu , OverlapMenu , CustomMenuPanel , CustomMenu ] ,
3133 providers : [
3234 { provide : OverlayContainer , useFactory : ( ) => {
3335 overlayContainerElement = document . createElement ( 'div' ) ;
@@ -256,6 +258,106 @@ describe('MdMenu', () => {
256258 }
257259 } ) ;
258260
261+ describe ( 'overlapping trigger' , ( ) => {
262+ /**
263+ * This test class is used to create components containing a menu.
264+ * It provides helpers to reposition the trigger, open the menu,
265+ * and access the trigger and overlay positions.
266+ * Additionally it can take any inputs for the menu wrapper component.
267+ *
268+ * Basic usage:
269+ * const subject = new OverlapSubject(MyComponent);
270+ * subject.openMenu();
271+ */
272+ class OverlapSubject < T extends TestableMenu > {
273+ private readonly fixture : ComponentFixture < T > ;
274+ private readonly trigger : any ;
275+
276+ constructor ( ctor : { new ( ) : T ; } , inputs : { [ key : string ] : any } = { } ) {
277+ this . fixture = TestBed . createComponent ( ctor ) ;
278+ extendObject ( this . fixture . componentInstance , inputs ) ;
279+ this . fixture . detectChanges ( ) ;
280+ this . trigger = this . fixture . componentInstance . triggerEl . nativeElement ;
281+ }
282+
283+ openMenu ( ) {
284+ this . fixture . componentInstance . trigger . openMenu ( ) ;
285+ this . fixture . detectChanges ( ) ;
286+ }
287+
288+ updateTriggerStyle ( style : any ) {
289+ return extendObject ( this . trigger . style , style ) ;
290+ }
291+
292+ get overlayRect ( ) {
293+ return this . overlayPane . getBoundingClientRect ( ) ;
294+ }
295+
296+ get triggerRect ( ) {
297+ return this . trigger . getBoundingClientRect ( ) ;
298+ }
299+
300+ get menuPanel ( ) {
301+ return overlayContainerElement . querySelector ( '.md-menu-panel' ) ;
302+ }
303+
304+ private get overlayPane ( ) {
305+ return overlayContainerElement . querySelector ( '.cdk-overlay-pane' ) as HTMLElement ;
306+ }
307+ }
308+
309+ let subject : OverlapSubject < OverlapMenu > ;
310+ describe ( 'explicitly overlapping' , ( ) => {
311+ beforeEach ( ( ) => {
312+ subject = new OverlapSubject ( OverlapMenu , { overlapTrigger : true } ) ;
313+ } ) ;
314+
315+ it ( 'positions the overlay below the trigger' , ( ) => {
316+ subject . openMenu ( ) ;
317+
318+ // Since the menu is overlaying the trigger, the overlay top should be the trigger top.
319+ expect ( Math . round ( subject . overlayRect . top ) )
320+ . toBe ( Math . round ( subject . triggerRect . top ) ,
321+ `Expected menu to open in default "below" position.` ) ;
322+ } ) ;
323+ } ) ;
324+
325+ describe ( 'not overlapping' , ( ) => {
326+ beforeEach ( ( ) => {
327+ subject = new OverlapSubject ( OverlapMenu , { overlapTrigger : false } ) ;
328+ } ) ;
329+
330+ it ( 'positions the overlay below the trigger' , ( ) => {
331+ subject . openMenu ( ) ;
332+
333+ // Since the menu is below the trigger, the overlay top should be the trigger bottom.
334+ expect ( Math . round ( subject . overlayRect . top ) )
335+ . toBe ( Math . round ( subject . triggerRect . bottom ) ,
336+ `Expected menu to open directly below the trigger.` ) ;
337+ } ) ;
338+
339+ it ( 'supports above position fall back' , ( ) => {
340+ // Push trigger to the bottom part of viewport, so it doesn't have space to open
341+ // in its default "below" position below the trigger.
342+ subject . updateTriggerStyle ( { position : 'relative' , top : '650px' } ) ;
343+ subject . openMenu ( ) ;
344+
345+ // Since the menu is above the trigger, the overlay bottom should be the trigger top.
346+ expect ( Math . round ( subject . overlayRect . bottom ) )
347+ . toBe ( Math . round ( subject . triggerRect . top ) ,
348+ `Expected menu to open in "above" position if "below" position wouldn't fit.` ) ;
349+ } ) ;
350+
351+ it ( 'repositions the origin to be below, so the menu opens from the trigger' , ( ) => {
352+ subject . openMenu ( ) ;
353+
354+ expect ( subject . menuPanel . classList ) . toContain ( 'md-menu-below' ) ;
355+ expect ( subject . menuPanel . classList ) . not . toContain ( 'md-menu-above' ) ;
356+ } ) ;
357+
358+ } ) ;
359+ } ) ;
360+
259361 describe ( 'animations' , ( ) => {
260362 it ( 'should include the ripple on items by default' , ( ) => {
261363 const fixture = TestBed . createComponent ( SimpleMenu ) ;
@@ -311,6 +413,23 @@ class PositionedMenu {
311413 @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
312414}
313415
416+ interface TestableMenu {
417+ trigger : MdMenuTrigger ;
418+ triggerEl : ElementRef ;
419+ }
420+ @Component ( {
421+ template : `
422+ <button [mdMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
423+ <md-menu [overlapTrigger]="overlapTrigger" #menu="mdMenu">
424+ <button md-menu-item> Not overlapped Content </button>
425+ </md-menu>
426+ `
427+ } )
428+ class OverlapMenu implements TestableMenu {
429+ @Input ( ) overlapTrigger : boolean ;
430+ @ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
431+ @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
432+ }
314433
315434@Component ( {
316435 selector : 'custom-menu' ,
@@ -325,6 +444,7 @@ class PositionedMenu {
325444class CustomMenuPanel implements MdMenuPanel {
326445 positionX : MenuPositionX = 'after' ;
327446 positionY : MenuPositionY = 'below' ;
447+ overlapTrigger : true ;
328448
329449 @ViewChild ( TemplateRef ) templateRef : TemplateRef < any > ;
330450 @Output ( ) close = new EventEmitter < void > ( ) ;
0 commit comments