mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(connected-position): apply the fallback position that shows the largest area of the element (#2102)
* feat(connected-position): apply the fallback position that shows the largest area of the element Switches the `connected-position` to pick the fallback position with the largest visible area, if all of the fallbacks didn't fit into the viewport. Fixes #2049. * Sort the fallbacks within the same loop.
This commit is contained in:
committed by
Jeremy Elbourn
parent
9b68e68168
commit
4f5b9c546e
@@ -211,6 +211,36 @@ describe('ConnectedPositionStrategy', () => {
|
||||
expect(overlayRect.right).toBe(originRect.left);
|
||||
});
|
||||
|
||||
it('should pick the fallback position that shows the largest area of the element', () => {
|
||||
// Use the fake viewport ruler because we don't know *exactly* how big the viewport is.
|
||||
fakeViewportRuler.fakeRect = {
|
||||
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
|
||||
};
|
||||
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
|
||||
|
||||
originElement.style.top = '200px';
|
||||
originElement.style.left = '475px';
|
||||
originRect = originElement.getBoundingClientRect();
|
||||
|
||||
strategy = positionBuilder.connectedTo(
|
||||
fakeElementRef,
|
||||
{originX: 'end', originY: 'center'},
|
||||
{overlayX: 'start', overlayY: 'center'})
|
||||
.withFallbackPosition(
|
||||
{originX: 'end', originY: 'top'},
|
||||
{overlayX: 'start', overlayY: 'bottom'})
|
||||
.withFallbackPosition(
|
||||
{originX: 'end', originY: 'top'},
|
||||
{overlayX: 'end', overlayY: 'top'});
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
|
||||
expect(overlayRect.top).toBe(originRect.top);
|
||||
expect(overlayRect.left).toBe(originRect.left);
|
||||
});
|
||||
|
||||
it('should position a panel properly when rtl', () => {
|
||||
// must make the overlay longer than the origin to properly test attachment
|
||||
overlayElement.style.width = `500px`;
|
||||
|
||||
@@ -76,7 +76,9 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
|
||||
// We use the viewport rect to determine whether a position would go off-screen.
|
||||
const viewportRect = this._viewportRuler.getViewportRect();
|
||||
let firstOverlayPoint: Point = null;
|
||||
|
||||
// Fallback point if none of the fallbacks fit into the viewport.
|
||||
let fallbackPoint: OverlayPoint = null;
|
||||
|
||||
// We want to place the overlay in the first of the preferred positions such that the
|
||||
// overlay fits on-screen.
|
||||
@@ -84,20 +86,22 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
// Get the (x, y) point of connection on the origin, and then use that to get the
|
||||
// (top, left) coordinate for the overlay at `pos`.
|
||||
let originPoint = this._getOriginConnectionPoint(originRect, pos);
|
||||
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, pos);
|
||||
firstOverlayPoint = firstOverlayPoint || overlayPoint;
|
||||
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, viewportRect, pos);
|
||||
|
||||
// If the overlay in the calculated position fits on-screen, put it there and we're done.
|
||||
if (this._willOverlayFitWithinViewport(overlayPoint, overlayRect, viewportRect)) {
|
||||
if (overlayPoint.fitsInViewport) {
|
||||
this._setElementPosition(element, overlayPoint);
|
||||
this._onPositionChange.next(new ConnectedOverlayPositionChange(pos));
|
||||
return Promise.resolve(null);
|
||||
} else if (!fallbackPoint || fallbackPoint.visibleArea < overlayPoint.visibleArea) {
|
||||
fallbackPoint = overlayPoint;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jelbourn): fallback behavior for when none of the preferred positions fit on-screen.
|
||||
// For now, just stick it in the first position and let it go off-screen.
|
||||
this._setElementPosition(element, firstOverlayPoint);
|
||||
// If none of the preferred positions were in the viewport, take the one
|
||||
// with the largest visible area.
|
||||
this._setElementPosition(element, fallbackPoint);
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
@@ -172,15 +176,14 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
|
||||
/**
|
||||
* Gets the (x, y) coordinate of the top-left corner of the overlay given a given position and
|
||||
* origin point to which the overlay should be connected.
|
||||
* @param originPoint
|
||||
* @param overlayRect
|
||||
* @param pos
|
||||
* origin point to which the overlay should be connected, as well as how much of the element
|
||||
* would be inside the viewport at that position.
|
||||
*/
|
||||
private _getOverlayPoint(
|
||||
originPoint: Point,
|
||||
overlayRect: ClientRect,
|
||||
pos: ConnectionPositionPair): Point {
|
||||
viewportRect: ClientRect,
|
||||
pos: ConnectionPositionPair): OverlayPoint {
|
||||
// Calculate the (overlayStartX, overlayStartY), the start of the potential overlay position
|
||||
// relative to the origin point.
|
||||
let overlayStartX: number;
|
||||
@@ -199,32 +202,27 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
overlayStartY = pos.overlayY == 'top' ? 0 : -overlayRect.height;
|
||||
}
|
||||
|
||||
return {
|
||||
x: originPoint.x + overlayStartX + this._offsetX,
|
||||
y: originPoint.y + overlayStartY + this._offsetY
|
||||
};
|
||||
// The (x, y) coordinates of the overlay.
|
||||
let x = originPoint.x + overlayStartX + this._offsetX;
|
||||
let y = originPoint.y + overlayStartY + this._offsetY;
|
||||
|
||||
// How much the overlay would overflow at this position, on each side.
|
||||
let leftOverflow = viewportRect.left - x;
|
||||
let rightOverflow = (x + overlayRect.width) - viewportRect.right;
|
||||
let topOverflow = viewportRect.top - y;
|
||||
let bottomOverflow = (y + overlayRect.height) - viewportRect.bottom;
|
||||
|
||||
// Visible parts of the element on each axis.
|
||||
let visibleWidth = this._subtractOverflows(overlayRect.width, leftOverflow, rightOverflow);
|
||||
let visibleHeight = this._subtractOverflows(overlayRect.height, topOverflow, bottomOverflow);
|
||||
|
||||
// The area of the element that's within the viewport.
|
||||
let visibleArea = visibleWidth * visibleHeight;
|
||||
let fitsInViewport = (overlayRect.width * overlayRect.height) === visibleArea;
|
||||
|
||||
return {x, y, fitsInViewport, visibleArea};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets whether the overlay positioned at the given point will fit on-screen.
|
||||
* @param overlayPoint The top-left coordinate of the overlay.
|
||||
* @param overlayRect Bounding rect of the overlay, used to get its size.
|
||||
* @param viewportRect The bounding viewport.
|
||||
*/
|
||||
private _willOverlayFitWithinViewport(
|
||||
overlayPoint: Point,
|
||||
overlayRect: ClientRect,
|
||||
viewportRect: ClientRect): boolean {
|
||||
|
||||
// TODO(jelbourn): probably also want some space between overlay edge and viewport edge.
|
||||
return overlayPoint.x >= 0 &&
|
||||
overlayPoint.x + overlayRect.width <= viewportRect.width &&
|
||||
overlayPoint.y >= 0 &&
|
||||
overlayPoint.y + overlayRect.height <= viewportRect.height;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Physically positions the overlay element to the given coordinate.
|
||||
* @param element
|
||||
@@ -234,8 +232,29 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
element.style.left = overlayPoint.x + 'px';
|
||||
element.style.top = overlayPoint.y + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts the amount that an element is overflowing on an axis from it's length.
|
||||
*/
|
||||
private _subtractOverflows(length: number, ...overflows: number[]): number {
|
||||
return overflows.reduce((currentValue: number, currentOverflow: number) => {
|
||||
return currentValue - Math.max(currentOverflow, 0);
|
||||
}, length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** A simple (x, y) coordinate. */
|
||||
type Point = {x: number, y: number};
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Expands the simple (x, y) coordinate by adding info about whether the
|
||||
* element would fit inside the viewport at that position, as well as
|
||||
* how much of the element would be visible.
|
||||
*/
|
||||
interface OverlayPoint extends Point {
|
||||
visibleArea?: number;
|
||||
fitsInViewport?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user