Skip to content

Commit 82cd076

Browse files
feat(cdk/overlay): Allow passing separate X and Y values for the viewportMargin (#29563)
The overlay directive now accepts two additional (optional parameters) [viewportMarginX] and [viewportMarginY]. You can use these to pass separate margin values for the viewport.
1 parent faf1ef0 commit 82cd076

File tree

4 files changed

+112
-21
lines changed

4 files changed

+112
-21
lines changed

src/cdk/overlay/overlay-directives.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {takeWhile} from 'rxjs/operators';
3232
import {createOverlayRef} from './overlay';
3333
import {OverlayConfig} from './overlay-config';
3434
import {OverlayRef} from './overlay-ref';
35-
import {ConnectedOverlayPositionChange} from './position/connected-position';
35+
import {ConnectedOverlayPositionChange, ViewportMargin} from './position/connected-position';
3636
import {
3737
ConnectedPosition,
3838
createFlexibleConnectedPositionStrategy,
@@ -179,7 +179,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
179179
@Input('cdkConnectedOverlayPanelClass') panelClass: string | string[];
180180

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

184184
/** Strategy to be used when handling scroll events while the overlay is open. */
185185
@Input('cdkConnectedOverlayScrollStrategy') scrollStrategy: ScrollStrategy;

src/cdk/overlay/position/connected-position.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export type HorizontalConnectionPos = 'start' | 'center' | 'end';
1212
/** Vertical dimension of a connection point on the perimeter of the origin or overlay element. */
1313
export type VerticalConnectionPos = 'top' | 'center' | 'bottom';
1414

15+
/** The distance between the overlay element and the viewport. */
16+
export type ViewportMargin = number | {top?: number; bottom?: number; start?: number; end?: number};
17+
1518
/** A connection point on the origin element. */
1619
export interface OriginConnectionPosition {
1720
originX: HorizontalConnectionPos;

src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,50 @@ describe('FlexibleConnectedPositionStrategy', () => {
13641364
expect(Math.floor(overlayRect.top)).toBe(15);
13651365
});
13661366

1367+
it('should set separate margins when pushing the overlay into the viewport', () => {
1368+
originElement.style.top = `${-OVERLAY_HEIGHT}px`;
1369+
originElement.style.left = `${-OVERLAY_WIDTH / 2}px`;
1370+
1371+
positionStrategy.withViewportMargin({top: 15, start: 10}).withPositions([
1372+
{
1373+
originX: 'start',
1374+
originY: 'bottom',
1375+
overlayX: 'start',
1376+
overlayY: 'top',
1377+
},
1378+
]);
1379+
1380+
attachOverlay({positionStrategy});
1381+
1382+
const overlayRect = overlayRef.overlayElement.getBoundingClientRect();
1383+
expect(Math.floor(overlayRect.top)).toBe(15);
1384+
expect(Math.floor(overlayRect.left)).toBe(10);
1385+
});
1386+
1387+
it('should only set the margins that were provided when pushing the overlay into the viewport from both axes', () => {
1388+
originElement.style.top = `${-OVERLAY_HEIGHT / 2}px`;
1389+
originElement.style.left = `${-OVERLAY_WIDTH / 2}px`;
1390+
1391+
positionStrategy.withViewportMargin({start: 30}).withPositions([
1392+
{
1393+
originX: 'start',
1394+
originY: 'top',
1395+
overlayX: 'start',
1396+
overlayY: 'bottom',
1397+
},
1398+
]);
1399+
1400+
attachOverlay({positionStrategy});
1401+
1402+
const overlayRect = overlayRef.overlayElement.getBoundingClientRect();
1403+
expect(Math.floor(overlayRect.left)).toBe(OVERLAY_WIDTH / 2);
1404+
expect(Math.floor(overlayRect.right)).toBe(
1405+
originElement.offsetWidth - OVERLAY_WIDTH / 2 + OVERLAY_WIDTH,
1406+
);
1407+
expect(Math.floor(overlayRect.top)).toBe(0);
1408+
expect(Math.floor(overlayRect.bottom)).toBe(OVERLAY_HEIGHT);
1409+
});
1410+
13671411
it('should not mess with the left offset when pushing from the top', () => {
13681412
originElement.style.top = `${-OVERLAY_HEIGHT * 2}px`;
13691413
originElement.style.left = '200px';

src/cdk/overlay/position/flexible-connected-position-strategy.ts

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ScrollingVisibility,
1616
validateHorizontalPosition,
1717
validateVerticalPosition,
18+
ViewportMargin,
1819
} from './connected-position';
1920
import {Observable, Subscription, Subject} from 'rxjs';
2021
import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip';
@@ -106,8 +107,8 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
106107
/** Cached container dimensions */
107108
private _containerRect: Dimensions;
108109

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

112113
/** The Scrollable containers used to check scrollable view properties on position change. */
113114
private _scrollables: CdkScrollable[] = [];
@@ -429,10 +430,11 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
429430
}
430431

431432
/**
432-
* Sets a minimum distance the overlay may be positioned to the edge of the viewport.
433-
* @param margin Required margin between the overlay and the viewport edge in pixels.
433+
* Sets a minimum distance the overlay may be positioned from the bottom edge of the viewport.
434+
* @param margin Required margin between the overlay and the viewport.
435+
* It can be a number to be applied to all directions, or an object to supply different values for each direction.
434436
*/
435-
withViewportMargin(margin: number): this {
437+
withViewportMargin(margin: ViewportMargin): this {
436438
this._viewportMargin = margin;
437439
return this;
438440
}
@@ -700,13 +702,17 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
700702
if (overlay.width <= viewport.width) {
701703
pushX = overflowLeft || -overflowRight;
702704
} else {
703-
pushX = start.x < this._viewportMargin ? viewport.left - scrollPosition.left - start.x : 0;
705+
pushX =
706+
start.x < this._getViewportMarginStart()
707+
? viewport.left - scrollPosition.left - start.x
708+
: 0;
704709
}
705710

706711
if (overlay.height <= viewport.height) {
707712
pushY = overflowTop || -overflowBottom;
708713
} else {
709-
pushY = start.y < this._viewportMargin ? viewport.top - scrollPosition.top - start.y : 0;
714+
pushY =
715+
start.y < this._getViewportMarginTop() ? viewport.top - scrollPosition.top - start.y : 0;
710716
}
711717

712718
this._previousPushAmount = {x: pushX, y: pushY};
@@ -795,13 +801,14 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
795801
if (position.overlayY === 'top') {
796802
// Overlay is opening "downward" and thus is bound by the bottom viewport edge.
797803
top = origin.y;
798-
height = viewport.height - top + this._viewportMargin;
804+
height = viewport.height - top + this._getViewportMarginBottom();
799805
} else if (position.overlayY === 'bottom') {
800806
// Overlay is opening "upward" and thus is bound by the top viewport edge. We need to add
801807
// the viewport margin back in, because the viewport rect is narrowed down to remove the
802808
// margin, whereas the `origin` position is calculated based on its `DOMRect`.
803-
bottom = viewport.height - origin.y + this._viewportMargin * 2;
804-
height = viewport.height - bottom + this._viewportMargin;
809+
bottom =
810+
viewport.height - origin.y + this._getViewportMarginTop() + this._getViewportMarginBottom();
811+
height = viewport.height - bottom + this._getViewportMarginTop();
805812
} else {
806813
// If neither top nor bottom, it means that the overlay is vertically centered on the
807814
// origin point. Note that we want the position relative to the viewport, rather than
@@ -833,11 +840,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
833840
let width: number, left: number, right: number;
834841

835842
if (isBoundedByLeftViewportEdge) {
836-
right = viewport.width - origin.x + this._viewportMargin * 2;
837-
width = origin.x - this._viewportMargin;
843+
right =
844+
viewport.width - origin.x + this._getViewportMarginStart() + this._getViewportMarginEnd();
845+
width = origin.x - this._getViewportMarginStart();
838846
} else if (isBoundedByRightViewportEdge) {
839847
left = origin.x;
840-
width = viewport.right - origin.x;
848+
width = viewport.right - origin.x - this._getViewportMarginEnd();
841849
} else {
842850
// If neither start nor end, it means that the overlay is horizontally centered on the
843851
// origin point. Note that we want the position relative to the viewport, rather than
@@ -1116,12 +1124,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
11161124
const scrollPosition = this._viewportRuler.getViewportScrollPosition();
11171125

11181126
return {
1119-
top: scrollPosition.top + this._viewportMargin,
1120-
left: scrollPosition.left + this._viewportMargin,
1121-
right: scrollPosition.left + width - this._viewportMargin,
1122-
bottom: scrollPosition.top + height - this._viewportMargin,
1123-
width: width - 2 * this._viewportMargin,
1124-
height: height - 2 * this._viewportMargin,
1127+
top: scrollPosition.top + this._getViewportMarginTop(),
1128+
left: scrollPosition.left + this._getViewportMarginStart(),
1129+
right: scrollPosition.left + width - this._getViewportMarginEnd(),
1130+
bottom: scrollPosition.top + height - this._getViewportMarginBottom(),
1131+
width: width - this._getViewportMarginStart() - this._getViewportMarginEnd(),
1132+
height: height - this._getViewportMarginTop() - this._getViewportMarginBottom(),
11251133
};
11261134
}
11271135

@@ -1186,6 +1194,42 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
11861194
}
11871195
}
11881196

1197+
/**
1198+
* Returns either the _viewportMargin directly (if it is a number) or its 'start' value.
1199+
* @private
1200+
*/
1201+
private _getViewportMarginStart(): number {
1202+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1203+
return this._viewportMargin?.start ?? 0;
1204+
}
1205+
1206+
/**
1207+
* Returns either the _viewportMargin directly (if it is a number) or its 'end' value.
1208+
* @private
1209+
*/
1210+
private _getViewportMarginEnd(): number {
1211+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1212+
return this._viewportMargin?.end ?? 0;
1213+
}
1214+
1215+
/**
1216+
* Returns either the _viewportMargin directly (if it is a number) or its 'top' value.
1217+
* @private
1218+
*/
1219+
private _getViewportMarginTop(): number {
1220+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1221+
return this._viewportMargin?.top ?? 0;
1222+
}
1223+
1224+
/**
1225+
* Returns either the _viewportMargin directly (if it is a number) or its 'bottom' value.
1226+
* @private
1227+
*/
1228+
private _getViewportMarginBottom(): number {
1229+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1230+
return this._viewportMargin?.bottom ?? 0;
1231+
}
1232+
11891233
/** Returns the DOMRect of the current origin. */
11901234
private _getOriginRect(): Dimensions {
11911235
const origin = this._origin;

0 commit comments

Comments
 (0)