@@ -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,117 @@ 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 . 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 )
144
+ . toEqual ( expectedLeft ,
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 )
149
+ . toEqual ( triggerRect . top ,
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 )
172
+ . toEqual ( expectedTop ,
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 )
177
+ . toEqual ( triggerRect . left ,
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 )
200
+ . toEqual ( expectedLeft ,
201
+ `Expected menu to open in "before" position if "after" position wouldn't fit.` ) ;
202
+
203
+ expect ( overlayPane . getBoundingClientRect ( ) . top )
204
+ . toEqual ( expectedTop ,
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 )
221
+ . toEqual ( triggerRect . left ,
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 )
227
+ . toEqual ( triggerRect . top ,
228
+ `Expected menu to open in "below" position if "above" position wouldn't fit.` ) ;
229
+ } ) ;
230
+
231
+ } ) ;
232
+
233
+
234
+
113
235
} ) ;
114
236
115
237
describe ( 'animations' , ( ) => {
@@ -142,27 +264,29 @@ describe('MdMenu', () => {
142
264
143
265
@Component ( {
144
266
template : `
145
- <button [md-menu-trigger-for]="menu">Toggle menu</button>
267
+ <button [md-menu-trigger-for]="menu" #triggerEl >Toggle menu</button>
146
268
<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>
149
271
</md-menu>
150
272
`
151
273
} )
152
274
class SimpleMenu {
153
275
@ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
276
+ @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
154
277
}
155
278
156
279
@Component ( {
157
280
template : `
158
- <button [md-menu-trigger-for]="menu">Toggle menu</button>
281
+ <button [md-menu-trigger-for]="menu" #triggerEl >Toggle menu</button>
159
282
<md-menu x-position="before" y-position="above" #menu="mdMenu">
160
283
<button md-menu-item> Positioned Content </button>
161
284
</md-menu>
162
285
`
163
286
} )
164
287
class PositionedMenu {
165
288
@ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
289
+ @ViewChild ( 'triggerEl' ) triggerEl : ElementRef ;
166
290
}
167
291
168
292
@@ -195,3 +319,14 @@ class CustomMenuPanel implements MdMenuPanel {
195
319
class CustomMenu {
196
320
@ViewChild ( MdMenuTrigger ) trigger : MdMenuTrigger ;
197
321
}
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