@@ -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,117 @@ 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 . marginLeft = '900px' ;
134+
135+ fixture . componentInstance . trigger . openMenu ( ) ;
136+ fixture . detectChanges ( ) ;
137+ const overlayPane = overlayContainerElement . children [ 0 ] as HTMLElement ;
138+ const triggerRect = trigger . getBoundingClientRect ( ) ;
139+
140+ // In "before" position, the right sides of the overlay and the origin are aligned.
141+ // To find the overlay left, subtract the menu width (112) from the origin's right side.
142+ const expectedLeft = triggerRect . right - 112 ;
143+ expect ( overlayPane . getBoundingClientRect ( ) . left . toFixed ( 2 ) )
144+ . toEqual ( expectedLeft . toFixed ( 2 ) ,
145+ `Expected menu to open in "before" position if "after" position wouldn't fit.` ) ;
146+
147+ // The y-position of the overlay should be unaffected, as it can already fit vertically
148+ expect ( overlayPane . getBoundingClientRect ( ) . top . toFixed ( 2 ) )
149+ . toEqual ( triggerRect . top . toFixed ( 2 ) ,
150+ `Expected menu top position to be unchanged if it can fit in the viewport.` ) ;
151+ } ) ;
152+
153+ it ( 'should fall back to "above" mode if "below" mode would not fit on screen' , ( ) => {
154+ const fixture = TestBed . createComponent ( SimpleMenu ) ;
155+ fixture . detectChanges ( ) ;
156+ const trigger = fixture . componentInstance . triggerEl . nativeElement ;
157+
158+ // Push trigger to the bottom part of viewport, so it doesn't have space to open
159+ // in its default "below" position below the trigger.
160+ trigger . style . marginTop = '600px' ;
161+
162+ fixture . componentInstance . trigger . openMenu ( ) ;
163+ fixture . detectChanges ( ) ;
164+ const overlayPane = overlayContainerElement . children [ 0 ] as HTMLElement ;
165+ const triggerRect = trigger . getBoundingClientRect ( ) ;
166+
167+ // In "above" position, the bottom edges of the overlay and the origin are aligned.
168+ // To find the overlay top, subtract the menu height from the origin's bottom edge.
169+ // Menu height = 48 per item * 2 + 16px padding = 112px
170+ const expectedTop = triggerRect . bottom - 112 ;
171+ expect ( overlayPane . getBoundingClientRect ( ) . top . toFixed ( 2 ) )
172+ . toEqual ( expectedTop . toFixed ( 2 ) ,
173+ `Expected menu to open in "above" position if "below" position wouldn't fit.` ) ;
174+
175+ // The x-position of the overlay should be unaffected, as it can already fit horizontally
176+ expect ( overlayPane . getBoundingClientRect ( ) . left . toFixed ( 2 ) )
177+ . toEqual ( triggerRect . left . toFixed ( 2 ) ,
178+ `Expected menu x position to be unchanged if it can fit in the viewport.` ) ;
179+ } ) ;
180+
181+ it ( 'should re-position menu on both axes if both defaults would not fit' , ( ) => {
182+ const fixture = TestBed . createComponent ( SimpleMenu ) ;
183+ fixture . detectChanges ( ) ;
184+ const trigger = fixture . componentInstance . triggerEl . nativeElement ;
185+
186+ // push trigger to the bottom, right part of viewport, so it doesn't have space to open
187+ // in its default "after below" position.
188+ trigger . style . marginLeft = '900px' ;
189+ trigger . style . marginTop = '600px' ;
190+
191+ fixture . componentInstance . trigger . openMenu ( ) ;
192+ fixture . detectChanges ( ) ;
193+ const overlayPane = overlayContainerElement . children [ 0 ] as HTMLElement ;
194+ const triggerRect = trigger . getBoundingClientRect ( ) ;
195+
196+ const expectedTop = triggerRect . bottom - 112 ;
197+ const expectedLeft = triggerRect . right - 112 ;
198+
199+ expect ( overlayPane . getBoundingClientRect ( ) . left . toFixed ( 2 ) )
200+ . toEqual ( expectedLeft . toFixed ( 2 ) ,
201+ `Expected menu to open in "before" position if "after" position wouldn't fit.` ) ;
202+
203+ expect ( overlayPane . getBoundingClientRect ( ) . top . toFixed ( 2 ) )
204+ . toEqual ( expectedTop . toFixed ( 2 ) ,
205+ `Expected menu to open in "above" position if "below" position wouldn't fit.` ) ;
206+ } ) ;
207+
208+ it ( 'should re-position a menu with custom position set' , ( ) => {
209+ const fixture = TestBed . createComponent ( PositionedMenu ) ;
210+ fixture . detectChanges ( ) ;
211+ const trigger = fixture . componentInstance . triggerEl . nativeElement ;
212+
213+ fixture . componentInstance . trigger . openMenu ( ) ;
214+ fixture . detectChanges ( ) ;
215+ const overlayPane = overlayContainerElement . children [ 0 ] as HTMLElement ;
216+ const triggerRect = trigger . getBoundingClientRect ( ) ;
217+
218+ // As designated "before" position won't fit on screen, the menu should fall back
219+ // to "after" mode, where the left sides of the overlay and trigger are aligned.
220+ expect ( overlayPane . getBoundingClientRect ( ) . left . toFixed ( 2 ) )
221+ . toEqual ( triggerRect . left . toFixed ( 2 ) ,
222+ `Expected menu to open in "after" position if "before" position wouldn't fit.` ) ;
223+
224+ // As designated "above" position won't fit on screen, the menu should fall back
225+ // to "below" mode, where the top edges of the overlay and trigger are aligned.
226+ expect ( overlayPane . getBoundingClientRect ( ) . top . toFixed ( 2 ) )
227+ . toEqual ( triggerRect . top . toFixed ( 2 ) ,
228+ `Expected menu to open in "below" position if "above" position wouldn't fit.` ) ;
229+ } ) ;
230+
231+ } ) ;
232+
233+
234+
113235 } ) ;
114236
115237 describe ( 'animations' , ( ) => {
@@ -142,27 +264,29 @@ describe('MdMenu', () => {
142264
143265@Component ( {
144266 template : `
145- <button [md-menu-trigger-for]="menu">Toggle menu</button>
267+ <button [md-menu-trigger-for]="menu" #triggerEl >Toggle menu</button>
146268 <md-menu #menu="mdMenu">
147- <button md-menu-item> Simple Content </button>
148- <button md-menu-item disabled> Disabled Content </button>
269+ <button md-menu-item> Item </button>
270+ <button md-menu-item disabled> Disabled </button>
149271 </md-menu>
150272 `
151273} )
152274class SimpleMenu {
153275 @ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
276+ @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
154277}
155278
156279@Component ( {
157280 template : `
158- <button [md-menu-trigger-for]="menu">Toggle menu</button>
281+ <button [md-menu-trigger-for]="menu" #triggerEl >Toggle menu</button>
159282 <md-menu x-position="before" y-position="above" #menu="mdMenu">
160283 <button md-menu-item> Positioned Content </button>
161284 </md-menu>
162285 `
163286} )
164287class PositionedMenu {
165288 @ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
289+ @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
166290}
167291
168292
@@ -195,3 +319,14 @@ class CustomMenuPanel implements MdMenuPanel {
195319class CustomMenu {
196320 @ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
197321}
322+
323+ class FakeViewportRuler {
324+ getViewportRect ( ) {
325+ return {
326+ left : 0 , top : 0 , width : 1014 , height : 686 , bottom : 686 , right : 1014
327+ } ;
328+ }
329+ getViewportScrollPosition ( ) {
330+ return { top : 0 , left : 0 } ;
331+ }
332+ }
0 commit comments