Skip to content

Commit 73539f2

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 1f94f09 commit 73539f2

4 files changed

+110
-20
lines changed

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

+2-1
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 = 0;
182183

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

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

+44
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,50 @@ 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.withViewportMargin({top: 15, start: 10}).withPositions([
1375+
{
1376+
originX: 'start',
1377+
originY: 'bottom',
1378+
overlayX: 'start',
1379+
overlayY: 'top',
1380+
},
1381+
]);
1382+
1383+
attachOverlay({positionStrategy});
1384+
1385+
const overlayRect = overlayRef.overlayElement.getBoundingClientRect();
1386+
expect(Math.floor(overlayRect.top)).toBe(15);
1387+
expect(Math.floor(overlayRect.left)).toBe(10);
1388+
});
1389+
1390+
it('should only set the margins that were provided when pushing the overlay into the viewport from both axes', () => {
1391+
originElement.style.top = `${-OVERLAY_HEIGHT / 2}px`;
1392+
originElement.style.left = `${-OVERLAY_WIDTH / 2}px`;
1393+
1394+
positionStrategy.withViewportMargin({start: 30}).withPositions([
1395+
{
1396+
originX: 'start',
1397+
originY: 'top',
1398+
overlayX: 'start',
1399+
overlayY: 'bottom',
1400+
},
1401+
]);
1402+
1403+
attachOverlay({positionStrategy});
1404+
1405+
const overlayRect = overlayRef.overlayElement.getBoundingClientRect();
1406+
expect(Math.floor(overlayRect.left)).toBe(OVERLAY_WIDTH / 2);
1407+
expect(Math.floor(overlayRect.right)).toBe(
1408+
originElement.offsetWidth - OVERLAY_WIDTH / 2 + OVERLAY_WIDTH,
1409+
);
1410+
expect(Math.floor(overlayRect.top)).toBe(0);
1411+
expect(Math.floor(overlayRect.bottom)).toBe(OVERLAY_HEIGHT);
1412+
});
1413+
13701414
it('should not mess with the left offset when pushing from the top', () => {
13711415
originElement.style.top = `${-OVERLAY_HEIGHT * 2}px`;
13721416
originElement.style.left = '200px';

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

+63-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,17 @@ 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 =
688+
start.x < this._getViewportMarginStart()
689+
? viewport.left - scrollPosition.left - start.x
690+
: 0;
686691
}
687692

688693
if (overlay.height <= viewport.height) {
689694
pushY = overflowTop || -overflowBottom;
690695
} else {
691-
pushY = start.y < this._viewportMargin ? viewport.top - scrollPosition.top - start.y : 0;
696+
pushY =
697+
start.y < this._getViewportMarginTop() ? viewport.top - scrollPosition.top - start.y : 0;
692698
}
693699

694700
this._previousPushAmount = {x: pushX, y: pushY};
@@ -777,13 +783,14 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
777783
if (position.overlayY === 'top') {
778784
// Overlay is opening "downward" and thus is bound by the bottom viewport edge.
779785
top = origin.y;
780-
height = viewport.height - top + this._viewportMargin;
786+
height = viewport.height - top + this._getViewportMarginBottom();
781787
} else if (position.overlayY === 'bottom') {
782788
// Overlay is opening "upward" and thus is bound by the top viewport edge. We need to add
783789
// the viewport margin back in, because the viewport rect is narrowed down to remove the
784790
// 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;
791+
bottom =
792+
viewport.height - origin.y + this._getViewportMarginTop() + this._getViewportMarginBottom();
793+
height = viewport.height - bottom + this._getViewportMarginTop();
787794
} else {
788795
// If neither top nor bottom, it means that the overlay is vertically centered on the
789796
// origin point. Note that we want the position relative to the viewport, rather than
@@ -815,11 +822,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
815822
let width: number, left: number, right: number;
816823

817824
if (isBoundedByLeftViewportEdge) {
818-
right = viewport.width - origin.x + this._viewportMargin * 2;
819-
width = origin.x - this._viewportMargin;
825+
right =
826+
viewport.width - origin.x + this._getViewportMarginStart() + this._getViewportMarginEnd();
827+
width = origin.x - this._getViewportMarginStart();
820828
} else if (isBoundedByRightViewportEdge) {
821829
left = origin.x;
822-
width = viewport.right - origin.x;
830+
width = viewport.right - origin.x - this._getViewportMarginEnd();
823831
} else {
824832
// If neither start nor end, it means that the overlay is horizontally centered on the
825833
// origin point. Note that we want the position relative to the viewport, rather than
@@ -1098,12 +1106,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
10981106
const scrollPosition = this._viewportRuler.getViewportScrollPosition();
10991107

11001108
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,
1109+
top: scrollPosition.top + this._getViewportMarginTop(),
1110+
left: scrollPosition.left + this._getViewportMarginStart(),
1111+
right: scrollPosition.left + width - this._getViewportMarginEnd(),
1112+
bottom: scrollPosition.top + height - this._getViewportMarginBottom(),
1113+
width: width - this._getViewportMarginStart() - this._getViewportMarginEnd(),
1114+
height: height - this._getViewportMarginTop() - this._getViewportMarginBottom(),
11071115
};
11081116
}
11091117

@@ -1168,6 +1176,42 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
11681176
}
11691177
}
11701178

1179+
/**
1180+
* Returns either the _viewportMargin directly (if it is a number) or its 'start' value.
1181+
* @private
1182+
*/
1183+
private _getViewportMarginStart(): number {
1184+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1185+
return this._viewportMargin?.start ?? 0;
1186+
}
1187+
1188+
/**
1189+
* Returns either the _viewportMargin directly (if it is a number) or its 'end' value.
1190+
* @private
1191+
*/
1192+
private _getViewportMarginEnd(): number {
1193+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1194+
return this._viewportMargin?.end ?? 0;
1195+
}
1196+
1197+
/**
1198+
* Returns either the _viewportMargin directly (if it is a number) or its 'top' value.
1199+
* @private
1200+
*/
1201+
private _getViewportMarginTop(): number {
1202+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1203+
return this._viewportMargin?.top ?? 0;
1204+
}
1205+
1206+
/**
1207+
* Returns either the _viewportMargin directly (if it is a number) or its 'bottom' value.
1208+
* @private
1209+
*/
1210+
private _getViewportMarginBottom(): number {
1211+
if (typeof this._viewportMargin === 'number') return this._viewportMargin;
1212+
return this._viewportMargin?.bottom ?? 0;
1213+
}
1214+
11711215
/** Returns the DOMRect of the current origin. */
11721216
private _getOriginRect(): Dimensions {
11731217
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)