@@ -2,6 +2,7 @@ import {TestBed, async} from '@angular/core/testing';
22import { By } from '@angular/platform-browser' ;
33import {
44 Component ,
5+ ElementRef ,
56 EventEmitter ,
67 Output ,
78 TemplateRef ,
@@ -15,6 +16,7 @@ import {
1516 MenuPositionY
1617} from './menu' ;
1718import { OverlayContainer } from '../core/overlay/overlay-container' ;
19+ import { ViewportRuler } from '../core/overlay/position/viewport-ruler' ;
1820
1921describe ( 'MdMenu' , ( ) => {
2022 let overlayContainerElement : HTMLElement ;
@@ -26,14 +28,23 @@ describe('MdMenu', () => {
2628 providers : [
2729 { provide : OverlayContainer , useFactory : ( ) => {
2830 overlayContainerElement = document . createElement ( 'div' ) ;
31+ overlayContainerElement . style . position = 'fixed' ;
32+ overlayContainerElement . style . top = '0' ;
33+ overlayContainerElement . style . left = '0' ;
34+ document . body . appendChild ( overlayContainerElement ) ;
2935 return { getContainerElement : ( ) => overlayContainerElement } ;
30- } }
36+ } } ,
37+ { provide : ViewportRuler , useClass : FakeViewportRuler }
3138 ]
3239 } ) ;
3340
3441 TestBed . compileComponents ( ) ;
3542 } ) ) ;
3643
44+ afterEach ( ( ) => {
45+ document . body . removeChild ( overlayContainerElement ) ;
46+ } ) ;
47+
3748 it ( 'should open the menu as an idempotent operation' , ( ) => {
3849 const fixture = TestBed . createComponent ( SimpleMenu ) ;
3950 fixture . detectChanges ( ) ;
@@ -42,8 +53,8 @@ describe('MdMenu', () => {
4253 fixture . componentInstance . trigger . openMenu ( ) ;
4354 fixture . componentInstance . trigger . openMenu ( ) ;
4455
45- expect ( overlayContainerElement . textContent ) . toContain ( 'Simple Content ' ) ;
46- expect ( overlayContainerElement . textContent ) . toContain ( 'Disabled Content ' ) ;
56+ expect ( overlayContainerElement . textContent ) . toContain ( 'Item ' ) ;
57+ expect ( overlayContainerElement . textContent ) . toContain ( 'Disabled' ) ;
4758 } ) . not . toThrowError ( ) ;
4859 } ) ;
4960
@@ -110,6 +121,123 @@ describe('MdMenu', () => {
110121 expect ( panel . classList ) . not . toContain ( 'md-menu-below' ) ;
111122 } ) ;
112123
124+ describe ( 'fallback positions' , ( ) => {
125+
126+ it ( 'should fall back to "before" mode if "after" mode would not fit on screen' , ( ) => {
127+ const fixture = TestBed . createComponent ( SimpleMenu ) ;
128+ fixture . detectChanges ( ) ;
129+ const trigger = fixture . componentInstance . triggerEl . nativeElement ;
130+
131+ // Push trigger to the right side of viewport, so it doesn't have space to open
132+ // in its default "after" position on the right side.
133+ trigger . style . position = 'relative' ;
134+ trigger . style . left = '900px' ;
135+
136+ fixture . componentInstance . trigger . openMenu ( ) ;
137+ fixture . detectChanges ( ) ;
138+ const overlayPane = overlayContainerElement . children [ 0 ] as HTMLElement ;
139+ const triggerRect = trigger . getBoundingClientRect ( ) ;
140+ const overlayRect = overlayPane . getBoundingClientRect ( ) ;
141+
142+ // In "before" position, the right sides of the overlay and the origin are aligned.
143+ // To find the overlay left, subtract the menu width from the origin's right side.
144+ const expectedLeft = triggerRect . right - overlayRect . width ;
145+ expect ( overlayRect . left . toFixed ( 2 ) )
146+ . toEqual ( expectedLeft . toFixed ( 2 ) ,
147+ `Expected menu to open in "before" position if "after" position wouldn't fit.` ) ;
148+
149+ // The y-position of the overlay should be unaffected, as it can already fit vertically
150+ expect ( overlayRect . top . toFixed ( 2 ) )
151+ . toEqual ( triggerRect . top . toFixed ( 2 ) ,
152+ `Expected menu top position to be unchanged if it can fit in the viewport.` ) ;
153+ } ) ;
154+
155+ it ( 'should fall back to "above" mode if "below" mode would not fit on screen' , ( ) => {
156+ const fixture = TestBed . createComponent ( SimpleMenu ) ;
157+ fixture . detectChanges ( ) ;
158+ const trigger = fixture . componentInstance . triggerEl . nativeElement ;
159+
160+ // Push trigger to the bottom part of viewport, so it doesn't have space to open
161+ // in its default "below" position below the trigger.
162+ trigger . style . position = 'relative' ;
163+ trigger . style . top = '600px' ;
164+
165+ fixture . componentInstance . trigger . openMenu ( ) ;
166+ fixture . detectChanges ( ) ;
167+ const overlayPane = overlayContainerElement . children [ 0 ] as HTMLElement ;
168+ const triggerRect = trigger . getBoundingClientRect ( ) ;
169+ const overlayRect = overlayPane . getBoundingClientRect ( ) ;
170+
171+ // In "above" position, the bottom edges of the overlay and the origin are aligned.
172+ // To find the overlay top, subtract the menu height from the origin's bottom edge.
173+ const expectedTop = triggerRect . bottom - overlayRect . height ;
174+ expect ( overlayRect . top . toFixed ( 2 ) )
175+ . toEqual ( expectedTop . toFixed ( 2 ) ,
176+ `Expected menu to open in "above" position if "below" position wouldn't fit.` ) ;
177+
178+ // The x-position of the overlay should be unaffected, as it can already fit horizontally
179+ expect ( overlayRect . left . toFixed ( 2 ) )
180+ . toEqual ( triggerRect . left . toFixed ( 2 ) ,
181+ `Expected menu x position to be unchanged if it can fit in the viewport.` ) ;
182+ } ) ;
183+
184+ it ( 'should re-position menu on both axes if both defaults would not fit' , ( ) => {
185+ const fixture = TestBed . createComponent ( SimpleMenu ) ;
186+ fixture . detectChanges ( ) ;
187+ const trigger = fixture . componentInstance . triggerEl . nativeElement ;
188+
189+ // push trigger to the bottom, right part of viewport, so it doesn't have space to open
190+ // in its default "after below" position.
191+ trigger . style . position = 'relative' ;
192+ trigger . style . left = '900px' ;
193+ trigger . style . top = '600px' ;
194+
195+ fixture . componentInstance . trigger . openMenu ( ) ;
196+ fixture . detectChanges ( ) ;
197+ const overlayPane = overlayContainerElement . children [ 0 ] as HTMLElement ;
198+ const triggerRect = trigger . getBoundingClientRect ( ) ;
199+ const overlayRect = overlayPane . getBoundingClientRect ( ) ;
200+
201+ const expectedLeft = triggerRect . right - overlayRect . width ;
202+ const expectedTop = triggerRect . bottom - overlayRect . height ;
203+
204+ expect ( overlayRect . left . toFixed ( 2 ) )
205+ . toEqual ( expectedLeft . toFixed ( 2 ) ,
206+ `Expected menu to open in "before" position if "after" position wouldn't fit.` ) ;
207+
208+ expect ( overlayRect . top . toFixed ( 2 ) )
209+ . toEqual ( expectedTop . toFixed ( 2 ) ,
210+ `Expected menu to open in "above" position if "below" position wouldn't fit.` ) ;
211+ } ) ;
212+
213+ it ( 'should re-position a menu with custom position set' , ( ) => {
214+ const fixture = TestBed . createComponent ( PositionedMenu ) ;
215+ fixture . detectChanges ( ) ;
216+ const trigger = fixture . componentInstance . triggerEl . nativeElement ;
217+
218+ fixture . componentInstance . trigger . openMenu ( ) ;
219+ fixture . detectChanges ( ) ;
220+ const overlayPane = overlayContainerElement . children [ 0 ] as HTMLElement ;
221+ const triggerRect = trigger . getBoundingClientRect ( ) ;
222+ const overlayRect = overlayPane . getBoundingClientRect ( ) ;
223+
224+ // As designated "before" position won't fit on screen, the menu should fall back
225+ // to "after" mode, where the left sides of the overlay and trigger are aligned.
226+ expect ( overlayRect . left . toFixed ( 2 ) )
227+ . toEqual ( triggerRect . left . toFixed ( 2 ) ,
228+ `Expected menu to open in "after" position if "before" position wouldn't fit.` ) ;
229+
230+ // As designated "above" position won't fit on screen, the menu should fall back
231+ // to "below" mode, where the top edges of the overlay and trigger are aligned.
232+ expect ( overlayRect . top . toFixed ( 2 ) )
233+ . toEqual ( triggerRect . top . toFixed ( 2 ) ,
234+ `Expected menu to open in "below" position if "above" position wouldn't fit.` ) ;
235+ } ) ;
236+
237+ } ) ;
238+
239+
240+
113241 } ) ;
114242
115243 describe ( 'animations' , ( ) => {
@@ -142,27 +270,29 @@ describe('MdMenu', () => {
142270
143271@Component ( {
144272 template : `
145- <button [md-menu-trigger-for]="menu">Toggle menu</button>
273+ <button [md-menu-trigger-for]="menu" #triggerEl >Toggle menu</button>
146274 <md-menu #menu="mdMenu">
147- <button md-menu-item> Simple Content </button>
148- <button md-menu-item disabled> Disabled Content </button>
275+ <button md-menu-item> Item </button>
276+ <button md-menu-item disabled> Disabled </button>
149277 </md-menu>
150278 `
151279} )
152280class SimpleMenu {
153281 @ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
282+ @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
154283}
155284
156285@Component ( {
157286 template : `
158- <button [md-menu-trigger-for]="menu">Toggle menu</button>
287+ <button [md-menu-trigger-for]="menu" #triggerEl >Toggle menu</button>
159288 <md-menu x-position="before" y-position="above" #menu="mdMenu">
160289 <button md-menu-item> Positioned Content </button>
161290 </md-menu>
162291 `
163292} )
164293class PositionedMenu {
165294 @ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
295+ @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
166296}
167297
168298
@@ -195,3 +325,14 @@ class CustomMenuPanel implements MdMenuPanel {
195325class CustomMenu {
196326 @ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
197327}
328+
329+ class FakeViewportRuler {
330+ getViewportRect ( ) {
331+ return {
332+ left : 0 , top : 0 , width : 1014 , height : 686 , bottom : 686 , right : 1014
333+ } ;
334+ }
335+ getViewportScrollPosition ( ) {
336+ return { top : 0 , left : 0 } ;
337+ }
338+ }
0 commit comments