@@ -76,28 +76,32 @@ 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+
80+ // Fallback point if none of the fallbacks fit into the viewport.
81+ let fallbackPoint : OverlayPoint = null ;
8082
8183 // We want to place the overlay in the first of the preferred positions such that the
8284 // overlay fits on-screen.
8385 for ( let pos of this . _preferredPositions ) {
8486 // Get the (x, y) point of connection on the origin, and then use that to get the
8587 // (top, left) coordinate for the overlay at `pos`.
8688 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 ) ;
8990
9091 // 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 ) {
9293 this . _setElementPosition ( element , overlayPoint ) ;
9394 this . _onPositionChange . next ( new ConnectedOverlayPositionChange ( pos ) ) ;
9495 return Promise . resolve ( null ) ;
96+ } else if ( ! fallbackPoint || fallbackPoint . visibleArea < overlayPoint . visibleArea ) {
97+ fallbackPoint = overlayPoint ;
9598 }
9699 }
97100
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+
101105 return Promise . resolve ( null ) ;
102106 }
103107
@@ -172,15 +176,14 @@ export class ConnectedPositionStrategy implements PositionStrategy {
172176
173177 /**
174178 * 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.
179181 */
180182 private _getOverlayPoint (
181183 originPoint : Point ,
182184 overlayRect : ClientRect ,
183- pos : ConnectionPositionPair ) : Point {
185+ viewportRect : ClientRect ,
186+ pos : ConnectionPositionPair ) : OverlayPoint {
184187 // Calculate the (overlayStartX, overlayStartY), the start of the potential overlay position
185188 // relative to the origin point.
186189 let overlayStartX : number ;
@@ -199,31 +202,26 @@ export class ConnectedPositionStrategy implements PositionStrategy {
199202 overlayStartY = pos . overlayY == 'top' ? 0 : - overlayRect . height ;
200203 }
201204
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 ;
207208
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 ;
208214
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 ) ;
219218
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 ;
226222
223+ return { x, y, fitsInViewport, visibleArea} ;
224+ }
227225
228226 /**
229227 * Physically positions the overlay element to the given coordinate.
@@ -234,8 +232,29 @@ export class ConnectedPositionStrategy implements PositionStrategy {
234232 element . style . left = overlayPoint . x + 'px' ;
235233 element . style . top = overlayPoint . y + 'px' ;
236234 }
237- }
238235
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+ }
239245
240246/** 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