@@ -76,28 +76,33 @@ export class ConnectedPositionStrategy implements PositionStrategy {
7676
7777 // We use the viewport rect to determine whether a position would go off-screen.
7878 const viewportRect = this . _viewportRuler . getViewportRect ( ) ;
79- let firstOverlayPoint : Point = null ;
79+ let attemptedPositions : OverlayPoint [ ] = [ ] ;
8080
8181 // We want to place the overlay in the first of the preferred positions such that the
8282 // overlay fits on-screen.
8383 for ( let pos of this . _preferredPositions ) {
8484 // Get the (x, y) point of connection on the origin, and then use that to get the
8585 // (top, left) coordinate for the overlay at `pos`.
8686 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 ) ;
8988
9089 // 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 ) {
9291 this . _setElementPosition ( element , overlayPoint ) ;
9392 this . _onPositionChange . next ( new ConnectedOverlayPositionChange ( pos ) ) ;
9493 return Promise . resolve ( null ) ;
94+ } else {
95+ attemptedPositions . push ( overlayPoint ) ;
9596 }
9697 }
9798
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+
101106 return Promise . resolve ( null ) ;
102107 }
103108
@@ -172,15 +177,14 @@ export class ConnectedPositionStrategy implements PositionStrategy {
172177
173178 /**
174179 * 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.
179182 */
180183 private _getOverlayPoint (
181184 originPoint : Point ,
182185 overlayRect : ClientRect ,
183- pos : ConnectionPositionPair ) : Point {
186+ viewportRect : ClientRect ,
187+ pos : ConnectionPositionPair ) : OverlayPoint {
184188 // Calculate the (overlayStartX, overlayStartY), the start of the potential overlay position
185189 // relative to the origin point.
186190 let overlayStartX : number ;
@@ -199,31 +203,26 @@ export class ConnectedPositionStrategy implements PositionStrategy {
199203 overlayStartY = pos . overlayY == 'top' ? 0 : - overlayRect . height ;
200204 }
201205
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 ;
207209
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 ;
208215
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 ) ;
219219
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 ;
226223
224+ return { x, y, fitsInViewport, visibleArea} ;
225+ }
227226
228227 /**
229228 * Physically positions the overlay element to the given coordinate.
@@ -234,8 +233,29 @@ export class ConnectedPositionStrategy implements PositionStrategy {
234233 element . style . left = overlayPoint . x + 'px' ;
235234 element . style . top = overlayPoint . y + 'px' ;
236235 }
237- }
238236
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+ }
239246
240247/** 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