@@ -76,28 +76,32 @@ export class ConnectedPositionStrategy implements PositionStrategy {
76
76
77
77
// We use the viewport rect to determine whether a position would go off-screen.
78
78
const viewportRect = this . _viewportRuler . getViewportRect ( ) ;
79
- let firstOverlayPoint : Point = null ;
79
+
80
+ // Fallback point if none of the fallbacks fit into the viewport.
81
+ let fallbackPoint : OverlayPoint = null ;
80
82
81
83
// We want to place the overlay in the first of the preferred positions such that the
82
84
// overlay fits on-screen.
83
85
for ( let pos of this . _preferredPositions ) {
84
86
// Get the (x, y) point of connection on the origin, and then use that to get the
85
87
// (top, left) coordinate for the overlay at `pos`.
86
88
let originPoint = this . _getOriginConnectionPoint ( originRect , pos ) ;
87
- let overlayPoint = this . _getOverlayPoint ( originPoint , overlayRect , pos ) ;
88
- firstOverlayPoint = firstOverlayPoint || overlayPoint ;
89
+ let overlayPoint = this . _getOverlayPoint ( originPoint , overlayRect , viewportRect , pos ) ;
89
90
90
91
// If the overlay in the calculated position fits on-screen, put it there and we're done.
91
- if ( this . _willOverlayFitWithinViewport ( overlayPoint , overlayRect , viewportRect ) ) {
92
+ if ( overlayPoint . fitsInViewport ) {
92
93
this . _setElementPosition ( element , overlayPoint ) ;
93
94
this . _onPositionChange . next ( new ConnectedOverlayPositionChange ( pos ) ) ;
94
95
return Promise . resolve ( null ) ;
96
+ } else if ( ! fallbackPoint || fallbackPoint . visibleArea < overlayPoint . visibleArea ) {
97
+ fallbackPoint = overlayPoint ;
95
98
}
96
99
}
97
100
98
- // TODO(jelbourn): fallback behavior for when none of the preferred positions fit on-screen.
99
- // For now, just stick it in the first position and let it go off-screen.
100
- this . _setElementPosition ( element , firstOverlayPoint ) ;
101
+ // If none of the preferred positions were in the viewport, take the one
102
+ // with the largest visible area.
103
+ this . _setElementPosition ( element , fallbackPoint ) ;
104
+
101
105
return Promise . resolve ( null ) ;
102
106
}
103
107
@@ -172,15 +176,14 @@ export class ConnectedPositionStrategy implements PositionStrategy {
172
176
173
177
/**
174
178
* Gets the (x, y) coordinate of the top-left corner of the overlay given a given position and
175
- * origin point to which the overlay should be connected.
176
- * @param originPoint
177
- * @param overlayRect
178
- * @param pos
179
+ * origin point to which the overlay should be connected, as well as how much of the element
180
+ * would be inside the viewport at that position.
179
181
*/
180
182
private _getOverlayPoint (
181
183
originPoint : Point ,
182
184
overlayRect : ClientRect ,
183
- pos : ConnectionPositionPair ) : Point {
185
+ viewportRect : ClientRect ,
186
+ pos : ConnectionPositionPair ) : OverlayPoint {
184
187
// Calculate the (overlayStartX, overlayStartY), the start of the potential overlay position
185
188
// relative to the origin point.
186
189
let overlayStartX : number ;
@@ -199,31 +202,26 @@ export class ConnectedPositionStrategy implements PositionStrategy {
199
202
overlayStartY = pos . overlayY == 'top' ? 0 : - overlayRect . height ;
200
203
}
201
204
202
- return {
203
- x : originPoint . x + overlayStartX + this . _offsetX ,
204
- y : originPoint . y + overlayStartY + this . _offsetY
205
- } ;
206
- }
205
+ // The (x, y) coordinates of the overlay.
206
+ let x = originPoint . x + overlayStartX + this . _offsetX ;
207
+ let y = originPoint . y + overlayStartY + this . _offsetY ;
207
208
209
+ // How much the overlay would overflow at this position, on each side.
210
+ let leftOverflow = viewportRect . left - x ;
211
+ let rightOverflow = ( x + overlayRect . width ) - viewportRect . right ;
212
+ let topOverflow = viewportRect . top - y ;
213
+ let bottomOverflow = ( y + overlayRect . height ) - viewportRect . bottom ;
208
214
209
- /**
210
- * Gets whether the overlay positioned at the given point will fit on-screen.
211
- * @param overlayPoint The top-left coordinate of the overlay.
212
- * @param overlayRect Bounding rect of the overlay, used to get its size.
213
- * @param viewportRect The bounding viewport.
214
- */
215
- private _willOverlayFitWithinViewport (
216
- overlayPoint : Point ,
217
- overlayRect : ClientRect ,
218
- viewportRect : ClientRect ) : boolean {
215
+ // Visible parts of the element on each axis.
216
+ let visibleWidth = this . _subtractOverflows ( overlayRect . width , leftOverflow , rightOverflow ) ;
217
+ let visibleHeight = this . _subtractOverflows ( overlayRect . height , topOverflow , bottomOverflow ) ;
219
218
220
- // TODO(jelbourn): probably also want some space between overlay edge and viewport edge.
221
- return overlayPoint . x >= 0 &&
222
- overlayPoint . x + overlayRect . width <= viewportRect . width &&
223
- overlayPoint . y >= 0 &&
224
- overlayPoint . y + overlayRect . height <= viewportRect . height ;
225
- }
219
+ // The area of the element that's within the viewport.
220
+ let visibleArea = visibleWidth * visibleHeight ;
221
+ let fitsInViewport = ( overlayRect . width * overlayRect . height ) === visibleArea ;
226
222
223
+ return { x, y, fitsInViewport, visibleArea} ;
224
+ }
227
225
228
226
/**
229
227
* Physically positions the overlay element to the given coordinate.
@@ -234,8 +232,29 @@ export class ConnectedPositionStrategy implements PositionStrategy {
234
232
element . style . left = overlayPoint . x + 'px' ;
235
233
element . style . top = overlayPoint . y + 'px' ;
236
234
}
237
- }
238
235
236
+ /**
237
+ * Subtracts the amount that an element is overflowing on an axis from it's length.
238
+ */
239
+ private _subtractOverflows ( length : number , ...overflows : number [ ] ) : number {
240
+ return overflows . reduce ( ( currentValue : number , currentOverflow : number ) => {
241
+ return currentValue - Math . max ( currentOverflow , 0 ) ;
242
+ } , length ) ;
243
+ }
244
+ }
239
245
240
246
/** A simple (x, y) coordinate. */
241
- type Point = { x : number , y : number } ;
247
+ interface Point {
248
+ x : number ;
249
+ y : number ;
250
+ } ;
251
+
252
+ /**
253
+ * Expands the simple (x, y) coordinate by adding info about whether the
254
+ * element would fit inside the viewport at that position, as well as
255
+ * how much of the element would be visible.
256
+ */
257
+ interface OverlayPoint extends Point {
258
+ visibleArea ?: number ;
259
+ fitsInViewport ?: boolean ;
260
+ }
0 commit comments