Skip to content

Commit

Permalink
feat(cdk/overlay): Allow passing separate X and Y values for the view…
Browse files Browse the repository at this point in the history
…portMargin

The overlay directive now accepts two additional (optional parameters) [viewportMarginX] and [viewportMarginY]. You can use these to pass separate margin values for the viewport.
  • Loading branch information
LukeyBeachBoy committed Oct 9, 2024
1 parent 1f94f09 commit 73539f2
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 20 deletions.
3 changes: 2 additions & 1 deletion src/cdk/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
FlexibleConnectedPositionStrategyOrigin,
} from './position/flexible-connected-position-strategy';
import {RepositionScrollStrategy, ScrollStrategy} from './scroll/index';
import {ViewportMargin} from './position/viewport-margin';

/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
const defaultPositionList: ConnectedPosition[] = [
Expand Down Expand Up @@ -178,7 +179,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
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 @@ -22,6 +22,7 @@ import {coerceCssPixelValue, coerceArray} from '@angular/cdk/coercion';
import {Platform} from '@angular/cdk/platform';
import {OverlayContainer} from '../overlay-container';
import {OverlayRef} from '../overlay-ref';
import {ViewportMargin} from './viewport-margin';

// TODO: refactor clipping detection into a separate thing (part of scrolling module)
// TODO: doesn't handle both flexible width and height when it has to scroll along both axis.
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
1 change: 1 addition & 0 deletions src/cdk/overlay/position/viewport-margin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ViewportMargin = number | {top?: number; bottom?: number; start?: number; end?: number};

0 comments on commit 73539f2

Please sign in to comment.