Skip to content

feat(cdk/overlay): Allow passing separate X and Y values for the view… #29563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cdk/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {takeWhile} from 'rxjs/operators';
import {Overlay} from './overlay';
import {OverlayConfig} from './overlay-config';
import {OverlayRef} from './overlay-ref';
import {ConnectedOverlayPositionChange} from './position/connected-position';
import {ConnectedOverlayPositionChange, ViewportMargin} from './position/connected-position';
import {
ConnectedPosition,
FlexibleConnectedPositionStrategy,
Expand Down Expand Up @@ -178,7 +178,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
@Input('cdkConnectedOverlayPanelClass') panelClass: string | string[];

/** Margin between the overlay and the viewport edges. */
@Input('cdkConnectedOverlayViewportMargin') viewportMargin: number = 0;
@Input('cdkConnectedOverlayViewportMargin') viewportMargin: ViewportMargin = 0;

/** Strategy to be used when handling scroll events while the overlay is open. */
@Input('cdkConnectedOverlayScrollStrategy') scrollStrategy: ScrollStrategy;
Expand Down
3 changes: 3 additions & 0 deletions src/cdk/overlay/position/connected-position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export type HorizontalConnectionPos = 'start' | 'center' | 'end';
/** Vertical dimension of a connection point on the perimeter of the origin or overlay element. */
export type VerticalConnectionPos = 'top' | 'center' | 'bottom';

/** The distance between the overlay element and the viewport. */
export type ViewportMargin = number | {top?: number; bottom?: number; start?: number; end?: number};

/** A connection point on the origin element. */
export interface OriginConnectionPosition {
originX: HorizontalConnectionPos;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,50 @@ describe('FlexibleConnectedPositionStrategy', () => {
expect(Math.floor(overlayRect.top)).toBe(15);
});

it('should set separate margins when pushing the overlay into the viewport', () => {
originElement.style.top = `${-OVERLAY_HEIGHT}px`;
originElement.style.left = `${-OVERLAY_WIDTH / 2}px`;

positionStrategy.withViewportMargin({top: 15, start: 10}).withPositions([
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top',
},
]);

attachOverlay({positionStrategy});

const overlayRect = overlayRef.overlayElement.getBoundingClientRect();
expect(Math.floor(overlayRect.top)).toBe(15);
expect(Math.floor(overlayRect.left)).toBe(10);
});

it('should only set the margins that were provided when pushing the overlay into the viewport from both axes', () => {
originElement.style.top = `${-OVERLAY_HEIGHT / 2}px`;
originElement.style.left = `${-OVERLAY_WIDTH / 2}px`;

positionStrategy.withViewportMargin({start: 30}).withPositions([
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom',
},
]);

attachOverlay({positionStrategy});

const overlayRect = overlayRef.overlayElement.getBoundingClientRect();
expect(Math.floor(overlayRect.left)).toBe(OVERLAY_WIDTH / 2);
expect(Math.floor(overlayRect.right)).toBe(
originElement.offsetWidth - OVERLAY_WIDTH / 2 + OVERLAY_WIDTH,
);
expect(Math.floor(overlayRect.top)).toBe(0);
expect(Math.floor(overlayRect.bottom)).toBe(OVERLAY_HEIGHT);
});

it('should not mess with the left offset when pushing from the top', () => {
originElement.style.top = `${-OVERLAY_HEIGHT * 2}px`;
originElement.style.left = '200px';
Expand Down
82 changes: 63 additions & 19 deletions src/cdk/overlay/position/flexible-connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ScrollingVisibility,
validateHorizontalPosition,
validateVerticalPosition,
ViewportMargin,
} from './connected-position';
import {Observable, Subscription, Subject} from 'rxjs';
import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip';
Expand Down Expand Up @@ -88,8 +89,8 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
/** Cached container dimensions */
private _containerRect: Dimensions;

/** Amount of space that must be maintained between the overlay and the edge of the viewport. */
private _viewportMargin = 0;
/** Amount of space that must be maintained between the overlay and the right edge of the viewport. */
private _viewportMargin: ViewportMargin = 0;

/** The Scrollable containers used to check scrollable view properties on position change. */
private _scrollables: CdkScrollable[] = [];
Expand Down Expand Up @@ -411,10 +412,11 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
}

/**
* Sets a minimum distance the overlay may be positioned to the edge of the viewport.
* @param margin Required margin between the overlay and the viewport edge in pixels.
* Sets a minimum distance the overlay may be positioned from the bottom edge of the viewport.
* @param margin Required margin between the overlay and the viewport.
* It can be a number to be applied to all directions, or an object to supply different values for each direction.
*/
withViewportMargin(margin: number): this {
withViewportMargin(margin: ViewportMargin): this {
this._viewportMargin = margin;
return this;
}
Expand Down Expand Up @@ -682,13 +684,17 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
if (overlay.width <= viewport.width) {
pushX = overflowLeft || -overflowRight;
} else {
pushX = start.x < this._viewportMargin ? viewport.left - scrollPosition.left - start.x : 0;
pushX =
start.x < this._getViewportMarginStart()
? viewport.left - scrollPosition.left - start.x
: 0;
}

if (overlay.height <= viewport.height) {
pushY = overflowTop || -overflowBottom;
} else {
pushY = start.y < this._viewportMargin ? viewport.top - scrollPosition.top - start.y : 0;
pushY =
start.y < this._getViewportMarginTop() ? viewport.top - scrollPosition.top - start.y : 0;
}

this._previousPushAmount = {x: pushX, y: pushY};
Expand Down Expand Up @@ -777,13 +783,14 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
if (position.overlayY === 'top') {
// Overlay is opening "downward" and thus is bound by the bottom viewport edge.
top = origin.y;
height = viewport.height - top + this._viewportMargin;
height = viewport.height - top + this._getViewportMarginBottom();
} else if (position.overlayY === 'bottom') {
// Overlay is opening "upward" and thus is bound by the top viewport edge. We need to add
// the viewport margin back in, because the viewport rect is narrowed down to remove the
// margin, whereas the `origin` position is calculated based on its `DOMRect`.
bottom = viewport.height - origin.y + this._viewportMargin * 2;
height = viewport.height - bottom + this._viewportMargin;
bottom =
viewport.height - origin.y + this._getViewportMarginTop() + this._getViewportMarginBottom();
height = viewport.height - bottom + this._getViewportMarginTop();
} else {
// If neither top nor bottom, it means that the overlay is vertically centered on the
// origin point. Note that we want the position relative to the viewport, rather than
Expand Down Expand Up @@ -815,11 +822,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
let width: number, left: number, right: number;

if (isBoundedByLeftViewportEdge) {
right = viewport.width - origin.x + this._viewportMargin * 2;
width = origin.x - this._viewportMargin;
right =
viewport.width - origin.x + this._getViewportMarginStart() + this._getViewportMarginEnd();
width = origin.x - this._getViewportMarginStart();
} else if (isBoundedByRightViewportEdge) {
left = origin.x;
width = viewport.right - origin.x;
width = viewport.right - origin.x - this._getViewportMarginEnd();
} else {
// If neither start nor end, it means that the overlay is horizontally centered on the
// origin point. Note that we want the position relative to the viewport, rather than
Expand Down Expand Up @@ -1098,12 +1106,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
const scrollPosition = this._viewportRuler.getViewportScrollPosition();

return {
top: scrollPosition.top + this._viewportMargin,
left: scrollPosition.left + this._viewportMargin,
right: scrollPosition.left + width - this._viewportMargin,
bottom: scrollPosition.top + height - this._viewportMargin,
width: width - 2 * this._viewportMargin,
height: height - 2 * this._viewportMargin,
top: scrollPosition.top + this._getViewportMarginTop(),
left: scrollPosition.left + this._getViewportMarginStart(),
right: scrollPosition.left + width - this._getViewportMarginEnd(),
bottom: scrollPosition.top + height - this._getViewportMarginBottom(),
width: width - this._getViewportMarginStart() - this._getViewportMarginEnd(),
height: height - this._getViewportMarginTop() - this._getViewportMarginBottom(),
};
}

Expand Down Expand Up @@ -1168,6 +1176,42 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
}
}

/**
* Returns either the _viewportMargin directly (if it is a number) or its 'start' value.
* @private
*/
private _getViewportMarginStart(): number {
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
return this._viewportMargin?.start ?? 0;
}

/**
* Returns either the _viewportMargin directly (if it is a number) or its 'end' value.
* @private
*/
private _getViewportMarginEnd(): number {
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
return this._viewportMargin?.end ?? 0;
}

/**
* Returns either the _viewportMargin directly (if it is a number) or its 'top' value.
* @private
*/
private _getViewportMarginTop(): number {
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
return this._viewportMargin?.top ?? 0;
}

/**
* Returns either the _viewportMargin directly (if it is a number) or its 'bottom' value.
* @private
*/
private _getViewportMarginBottom(): number {
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
return this._viewportMargin?.bottom ?? 0;
}

/** Returns the DOMRect of the current origin. */
private _getOriginRect(): Dimensions {
const origin = this._origin;
Expand Down
Loading