Skip to content

Commit 742668a

Browse files
committed
feat(cdk/overlay): Allow passing separate X and Y values for the viewportMargin
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 df7104f commit 742668a

4 files changed

+106
-21
lines changed

Diff for: src/cdk/overlay/overlay-directives.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
FlexibleConnectedPositionStrategyOrigin,
4141
} from './position/flexible-connected-position-strategy';
4242
import {RepositionScrollStrategy, ScrollStrategy} from './scroll/index';
43+
import {ViewportMargin} from './position/viewport-margin';
4344

4445
/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
4546
const defaultPositionList: ConnectedPosition[] = [
@@ -178,7 +179,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
178179
@Input('cdkConnectedOverlayPanelClass') panelClass: string | string[];
179180

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

183184
/** Strategy to be used when handling scroll events while the overlay is open. */
184185
@Input('cdkConnectedOverlayScrollStrategy') scrollStrategy: ScrollStrategy;
@@ -378,7 +379,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
378379
.withFlexibleDimensions(this.flexibleDimensions)
379380
.withPush(this.push)
380381
.withGrowAfterOpen(this.growAfterOpen)
381-
.withViewportMargin(this.viewportMargin)
382+
.withViewportMargin(this.viewportMargin ?? 0)
382383
.withLockedPosition(this.lockPosition)
383384
.withTransformOriginOn(this.transformOriginSelector);
384385
}

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

+45
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,51 @@ describe('FlexibleConnectedPositionStrategy', () => {
13671367
expect(Math.floor(overlayRect.top)).toBe(15);
13681368
});
13691369

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

Diff for: src/cdk/overlay/position/flexible-connected-position-strategy.ts

+57-19
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {coerceCssPixelValue, coerceArray} from '@angular/cdk/coercion';
2222
import {Platform} from '@angular/cdk/platform';
2323
import {OverlayContainer} from '../overlay-container';
2424
import {OverlayRef} from '../overlay-ref';
25+
import {ViewportMargin} from './viewport-margin';
2526

2627
// TODO: refactor clipping detection into a separate thing (part of scrolling module)
2728
// TODO: doesn't handle both flexible width and height when it has to scroll along both axis.
@@ -88,8 +89,8 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
8889
/** Cached container dimensions */
8990
private _containerRect: Dimensions;
9091

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

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

413414
/**
414-
* Sets a minimum distance the overlay may be positioned to the edge of the viewport.
415-
* @param margin Required margin between the overlay and the viewport edge in pixels.
415+
* Sets a minimum distance the overlay may be positioned from the bottom edge of the viewport.
416+
* @param margin Required margin between the overlay and the viewport.
417+
* It can be a number to be applied to all directions, or an object to supply different values for each direction.
416418
*/
417-
withViewportMargin(margin: number): this {
419+
withViewportMargin(margin: ViewportMargin): this {
418420
this._viewportMargin = margin;
419421
return this;
420422
}
@@ -682,13 +684,13 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
682684
if (overlay.width <= viewport.width) {
683685
pushX = overflowLeft || -overflowRight;
684686
} else {
685-
pushX = start.x < this._viewportMargin ? viewport.left - scrollPosition.left - start.x : 0;
687+
pushX = start.x < this._getViewportMarginStart() ? viewport.left - scrollPosition.left - start.x : 0;
686688
}
687689

688690
if (overlay.height <= viewport.height) {
689691
pushY = overflowTop || -overflowBottom;
690692
} else {
691-
pushY = start.y < this._viewportMargin ? viewport.top - scrollPosition.top - start.y : 0;
693+
pushY = start.y < this._getViewportMarginTop() ? viewport.top - scrollPosition.top - start.y : 0;
692694
}
693695

694696
this._previousPushAmount = {x: pushX, y: pushY};
@@ -777,13 +779,13 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
777779
if (position.overlayY === 'top') {
778780
// Overlay is opening "downward" and thus is bound by the bottom viewport edge.
779781
top = origin.y;
780-
height = viewport.height - top + this._viewportMargin;
782+
height = viewport.height - top + this._getViewportMarginBottom();
781783
} else if (position.overlayY === 'bottom') {
782784
// Overlay is opening "upward" and thus is bound by the top viewport edge. We need to add
783785
// the viewport margin back in, because the viewport rect is narrowed down to remove the
784786
// margin, whereas the `origin` position is calculated based on its `DOMRect`.
785-
bottom = viewport.height - origin.y + this._viewportMargin * 2;
786-
height = viewport.height - bottom + this._viewportMargin;
787+
bottom = viewport.height - origin.y + this._getViewportMarginTop() + this._getViewportMarginBottom();
788+
height = viewport.height - bottom + this._getViewportMarginTop();
787789
} else {
788790
// If neither top nor bottom, it means that the overlay is vertically centered on the
789791
// origin point. Note that we want the position relative to the viewport, rather than
@@ -815,11 +817,11 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
815817
let width: number, left: number, right: number;
816818

817819
if (isBoundedByLeftViewportEdge) {
818-
right = viewport.width - origin.x + this._viewportMargin * 2;
819-
width = origin.x - this._viewportMargin;
820+
right = viewport.width - origin.x + this._getViewportMarginStart() + this._getViewportMarginEnd();
821+
width = origin.x - this._getViewportMarginStart();
820822
} else if (isBoundedByRightViewportEdge) {
821823
left = origin.x;
822-
width = viewport.right - origin.x;
824+
width = viewport.right - origin.x - this._getViewportMarginEnd();
823825
} else {
824826
// If neither start nor end, it means that the overlay is horizontally centered on the
825827
// origin point. Note that we want the position relative to the viewport, rather than
@@ -1098,12 +1100,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
10981100
const scrollPosition = this._viewportRuler.getViewportScrollPosition();
10991101

11001102
return {
1101-
top: scrollPosition.top + this._viewportMargin,
1102-
left: scrollPosition.left + this._viewportMargin,
1103-
right: scrollPosition.left + width - this._viewportMargin,
1104-
bottom: scrollPosition.top + height - this._viewportMargin,
1105-
width: width - 2 * this._viewportMargin,
1106-
height: height - 2 * this._viewportMargin,
1103+
top: scrollPosition.top + this._getViewportMarginTop(),
1104+
left: scrollPosition.left + this._getViewportMarginStart(),
1105+
right: scrollPosition.left + width - this._getViewportMarginEnd(),
1106+
bottom: scrollPosition.top + height - this._getViewportMarginBottom(),
1107+
width: width - this._getViewportMarginStart() - this._getViewportMarginEnd(),
1108+
height: height - this._getViewportMarginTop() - this._getViewportMarginBottom(),
11071109
};
11081110
}
11091111

@@ -1168,6 +1170,42 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
11681170
}
11691171
}
11701172

1173+
/**
1174+
* Returns either the _viewportMargin directly (if it is a number) or its 'start' value.
1175+
* @private
1176+
*/
1177+
private _getViewportMarginStart(): number {
1178+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1179+
return this._viewportMargin.start ?? 0
1180+
}
1181+
1182+
/**
1183+
* Returns either the _viewportMargin directly (if it is a number) or its 'end' value.
1184+
* @private
1185+
*/
1186+
private _getViewportMarginEnd(): number {
1187+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1188+
return this._viewportMargin.end ?? 0;
1189+
}
1190+
1191+
/**
1192+
* Returns either the _viewportMargin directly (if it is a number) or its 'top' value.
1193+
* @private
1194+
*/
1195+
private _getViewportMarginTop(): number {
1196+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1197+
return this._viewportMargin.top ?? 0;
1198+
}
1199+
1200+
/**
1201+
* Returns either the _viewportMargin directly (if it is a number) or its 'bottom' value.
1202+
* @private
1203+
*/
1204+
private _getViewportMarginBottom(): number {
1205+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1206+
return this._viewportMargin.bottom ?? 0;
1207+
}
1208+
11711209
/** Returns the DOMRect of the current origin. */
11721210
private _getOriginRect(): Dimensions {
11731211
const origin = this._origin;

Diff for: src/cdk/overlay/position/viewport-margin.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type ViewportMargin = number | {top?: number, bottom?: number, start?: number, end?: number};

0 commit comments

Comments
 (0)