@@ -2,6 +2,7 @@ import {TestBed, async} from '@angular/core/testing';
2
2
import { By } from '@angular/platform-browser' ;
3
3
import {
4
4
Component ,
5
+ ElementRef ,
5
6
EventEmitter ,
6
7
Output ,
7
8
TemplateRef ,
@@ -15,6 +16,7 @@ import {
15
16
MenuPositionY
16
17
} from './menu' ;
17
18
import { OverlayContainer } from '../core/overlay/overlay-container' ;
19
+ import { ViewportRuler } from '../core/overlay/position/viewport-ruler' ;
18
20
19
21
describe ( 'MdMenu' , ( ) => {
20
22
let overlayContainerElement : HTMLElement ;
@@ -26,14 +28,23 @@ describe('MdMenu', () => {
26
28
providers : [
27
29
{ provide : OverlayContainer , useFactory : ( ) => {
28
30
overlayContainerElement = document . createElement ( 'div' ) ;
31
+ overlayContainerElement . style . position = 'fixed' ;
32
+ overlayContainerElement . style . top = '0' ;
33
+ overlayContainerElement . style . left = '0' ;
34
+ document . body . appendChild ( overlayContainerElement ) ;
29
35
return { getContainerElement : ( ) => overlayContainerElement } ;
30
- } }
36
+ } } ,
37
+ { provide : ViewportRuler , useClass : FakeViewportRuler }
31
38
]
32
39
} ) ;
33
40
34
41
TestBed . compileComponents ( ) ;
35
42
} ) ) ;
36
43
44
+ afterEach ( ( ) => {
45
+ document . body . removeChild ( overlayContainerElement ) ;
46
+ } ) ;
47
+
37
48
it ( 'should open the menu as an idempotent operation' , ( ) => {
38
49
const fixture = TestBed . createComponent ( SimpleMenu ) ;
39
50
fixture . detectChanges ( ) ;
@@ -42,8 +53,8 @@ describe('MdMenu', () => {
42
53
fixture . componentInstance . trigger . openMenu ( ) ;
43
54
fixture . componentInstance . trigger . openMenu ( ) ;
44
55
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' ) ;
47
58
} ) . not . toThrowError ( ) ;
48
59
} ) ;
49
60
@@ -110,6 +121,123 @@ describe('MdMenu', () => {
110
121
expect ( panel . classList ) . not . toContain ( 'md-menu-below' ) ;
111
122
} ) ;
112
123
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
+
113
241
} ) ;
114
242
115
243
describe ( 'animations' , ( ) => {
@@ -142,27 +270,29 @@ describe('MdMenu', () => {
142
270
143
271
@Component ( {
144
272
template : `
145
- <button [md-menu-trigger-for]="menu">Toggle menu</button>
273
+ <button [md-menu-trigger-for]="menu" #triggerEl >Toggle menu</button>
146
274
<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>
149
277
</md-menu>
150
278
`
151
279
} )
152
280
class SimpleMenu {
153
281
@ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
282
+ @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
154
283
}
155
284
156
285
@Component ( {
157
286
template : `
158
- <button [md-menu-trigger-for]="menu">Toggle menu</button>
287
+ <button [md-menu-trigger-for]="menu" #triggerEl >Toggle menu</button>
159
288
<md-menu x-position="before" y-position="above" #menu="mdMenu">
160
289
<button md-menu-item> Positioned Content </button>
161
290
</md-menu>
162
291
`
163
292
} )
164
293
class PositionedMenu {
165
294
@ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
295
+ @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
166
296
}
167
297
168
298
@@ -195,3 +325,14 @@ class CustomMenuPanel implements MdMenuPanel {
195
325
class CustomMenu {
196
326
@ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
197
327
}
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