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