Skip to content

Commit 7572e34

Browse files
authored
fix(menu): reposition menu if it would open off screen (#1761)
1 parent 522324c commit 7572e34

File tree

4 files changed

+214
-42
lines changed

4 files changed

+214
-42
lines changed

src/lib/menu/menu-directive.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
5454
@Attribute('y-position') posY: MenuPositionY) {
5555
if (posX) { this._setPositionX(posX); }
5656
if (posY) { this._setPositionY(posY); }
57-
this._setPositionClasses();
57+
this.setPositionClasses(this.positionX, this.positionY);
5858
}
5959

6060
// TODO: internal
@@ -83,7 +83,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
8383
obj[className] = true;
8484
return obj;
8585
}, {});
86-
this._setPositionClasses();
86+
this.setPositionClasses(this.positionX, this.positionY);
8787
}
8888

8989
@Output() close = new EventEmitter<void>();
@@ -123,11 +123,11 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
123123
* It's necessary to set position-based classes to ensure the menu panel animation
124124
* folds out from the correct direction.
125125
*/
126-
private _setPositionClasses() {
127-
this._classList['md-menu-before'] = this.positionX == 'before';
128-
this._classList['md-menu-after'] = this.positionX == 'after';
129-
this._classList['md-menu-above'] = this.positionY == 'above';
130-
this._classList['md-menu-below'] = this.positionY == 'below';
126+
setPositionClasses(posX: MenuPositionX, posY: MenuPositionY): void {
127+
this._classList['md-menu-before'] = posX == 'before';
128+
this._classList['md-menu-after'] = posX == 'after';
129+
this._classList['md-menu-above'] = posY == 'above';
130+
this._classList['md-menu-below'] = posY == 'below';
131131
}
132132

133133
}

src/lib/menu/menu-panel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export interface MdMenuPanel {
77
templateRef: TemplateRef<any>;
88
close: EventEmitter<void>;
99
focusFirstItem: () => void;
10+
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
1011
}

src/lib/menu/menu-trigger.ts

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import {
2222
TemplatePortal,
2323
ConnectedPositionStrategy,
2424
HorizontalConnectionPos,
25-
VerticalConnectionPos
25+
VerticalConnectionPos,
2626
} from '../core';
2727
import { Subscription } from 'rxjs/Subscription';
28+
import {MenuPositionX, MenuPositionY} from './menu-positions';
2829

2930
/**
3031
* This directive is intended to be used in conjunction with an md-menu tag. It is
@@ -44,6 +45,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
4445
private _overlayRef: OverlayRef;
4546
private _menuOpen: boolean = false;
4647
private _backdropSubscription: Subscription;
48+
private _positionSubscription: Subscription;
4749

4850
// tracking input type is necessary so it's possible to only auto-focus
4951
// the first item of the list when the menu is opened via the keyboard
@@ -92,9 +94,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
9294
this._overlayRef.dispose();
9395
this._overlayRef = null;
9496

95-
if (this._backdropSubscription) {
96-
this._backdropSubscription.unsubscribe();
97-
}
97+
this._cleanUpSubscriptions();
9898
}
9999
}
100100

@@ -172,7 +172,9 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
172172
private _createOverlay(): void {
173173
if (!this._overlayRef) {
174174
this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef);
175-
this._overlayRef = this._overlay.create(this._getOverlayConfig());
175+
const config = this._getOverlayConfig();
176+
this._subscribeToPositions(config.positionStrategy as ConnectedPositionStrategy);
177+
this._overlayRef = this._overlay.create(config);
176178
}
177179
}
178180

@@ -190,20 +192,52 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
190192
return overlayState;
191193
}
192194

195+
/**
196+
* Listens to changes in the position of the overlay and sets the correct classes
197+
* on the menu based on the new position. This ensures the animation origin is always
198+
* correct, even if a fallback position is used for the overlay.
199+
*/
200+
private _subscribeToPositions(position: ConnectedPositionStrategy): void {
201+
this._positionSubscription = position.onPositionChange.subscribe((change) => {
202+
const posX: MenuPositionX = change.connectionPair.originX === 'start' ? 'after' : 'before';
203+
const posY: MenuPositionY = change.connectionPair.originY === 'top' ? 'below' : 'above';
204+
this.menu.setPositionClasses(posX, posY);
205+
});
206+
}
207+
193208
/**
194209
* This method builds the position strategy for the overlay, so the menu is properly connected
195210
* to the trigger.
196211
* @returns ConnectedPositionStrategy
197212
*/
198213
private _getPosition(): ConnectedPositionStrategy {
199-
const positionX: HorizontalConnectionPos = this.menu.positionX === 'before' ? 'end' : 'start';
200-
const positionY: VerticalConnectionPos = this.menu.positionY === 'above' ? 'bottom' : 'top';
201-
202-
return this._overlay.position().connectedTo(
203-
this._element,
204-
{originX: positionX, originY: positionY},
205-
{overlayX: positionX, overlayY: positionY}
206-
);
214+
const [posX, fallbackX]: HorizontalConnectionPos[] =
215+
this.menu.positionX === 'before' ? ['end', 'start'] : ['start', 'end'];
216+
217+
const [posY, fallbackY]: VerticalConnectionPos[] =
218+
this.menu.positionY === 'above' ? ['bottom', 'top'] : ['top', 'bottom'];
219+
220+
return this._overlay.position()
221+
.connectedTo(this._element,
222+
{originX: posX, originY: posY}, {overlayX: posX, overlayY: posY})
223+
.withFallbackPosition(
224+
{originX: fallbackX, originY: posY},
225+
{overlayX: fallbackX, overlayY: posY})
226+
.withFallbackPosition(
227+
{originX: posX, originY: fallbackY},
228+
{overlayX: posX, overlayY: fallbackY})
229+
.withFallbackPosition(
230+
{originX: fallbackX, originY: fallbackY},
231+
{overlayX: fallbackX, overlayY: fallbackY});
232+
}
233+
234+
private _cleanUpSubscriptions(): void {
235+
if (this._backdropSubscription) {
236+
this._backdropSubscription.unsubscribe();
237+
}
238+
if (this._positionSubscription) {
239+
this._positionSubscription.unsubscribe();
240+
}
207241
}
208242

209243
_handleMousedown(event: MouseEvent): void {

0 commit comments

Comments
 (0)