diff --git a/src/cdk/overlay/position/connected-position-strategy.ts b/src/cdk/overlay/position/connected-position-strategy.ts index 0c1c3a532f9..3472f6eac59 100644 --- a/src/cdk/overlay/position/connected-position-strategy.ts +++ b/src/cdk/overlay/position/connected-position-strategy.ts @@ -13,23 +13,14 @@ import { ConnectionPositionPair, OriginConnectionPosition, OverlayConnectionPosition, - ConnectedOverlayPositionChange, ScrollableViewProperties + ConnectedOverlayPositionChange, + ScrollingVisibility, } from './connected-position'; import {Subject} from 'rxjs/Subject'; import {Observable} from 'rxjs/Observable'; import {Scrollable} from '../scroll/scrollable'; +import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip'; -/** - * Container to hold the bounding positions of a particular element with respect to the viewport, - * where top and bottom are the y-axis coordinates of the bounding rectangle and left and right are - * the x-axis coordinates. - */ -type ElementBoundingPositions = { - top: number; - right: number; - bottom: number; - left: number; -}; /** * A strategy for positioning overlays. Using this strategy, an overlay is given an @@ -306,49 +297,20 @@ export class ConnectedPositionStrategy implements PositionStrategy { * Gets the view properties of the trigger and overlay, including whether they are clipped * or completely outside the view of any of the strategy's scrollables. */ - private getScrollableViewProperties(overlay: HTMLElement): ScrollableViewProperties { - const originBounds = this._getElementBounds(this._origin); - const overlayBounds = this._getElementBounds(overlay); - const scrollContainerBounds = this.scrollables.map((scrollable: Scrollable) => { - return this._getElementBounds(scrollable.getElementRef().nativeElement); - }); + private _getScrollVisibility(overlay: HTMLElement): ScrollingVisibility { + const originBounds = this._origin.getBoundingClientRect(); + const overlayBounds = overlay.getBoundingClientRect(); + const scrollContainerBounds = + this.scrollables.map(s => s.getElementRef().nativeElement.getBoundingClientRect()); return { - isOriginClipped: this.isElementClipped(originBounds, scrollContainerBounds), - isOriginOutsideView: this.isElementOutsideView(originBounds, scrollContainerBounds), - isOverlayClipped: this.isElementClipped(overlayBounds, scrollContainerBounds), - isOverlayOutsideView: this.isElementOutsideView(overlayBounds, scrollContainerBounds), + isOriginClipped: isElementClippedByScrolling(originBounds, scrollContainerBounds), + isOriginOutsideView: isElementScrolledOutsideView(originBounds, scrollContainerBounds), + isOverlayClipped: isElementClippedByScrolling(overlayBounds, scrollContainerBounds), + isOverlayOutsideView: isElementScrolledOutsideView(overlayBounds, scrollContainerBounds), }; } - /** Whether the element is completely out of the view of any of the containers. */ - private isElementOutsideView( - elementBounds: ElementBoundingPositions, - containersBounds: ElementBoundingPositions[]): boolean { - return containersBounds.some((containerBounds: ElementBoundingPositions) => { - const outsideAbove = elementBounds.bottom < containerBounds.top; - const outsideBelow = elementBounds.top > containerBounds.bottom; - const outsideLeft = elementBounds.right < containerBounds.left; - const outsideRight = elementBounds.left > containerBounds.right; - - return outsideAbove || outsideBelow || outsideLeft || outsideRight; - }); - } - - /** Whether the element is clipped by any of the containers. */ - private isElementClipped( - elementBounds: ElementBoundingPositions, - containersBounds: ElementBoundingPositions[]): boolean { - return containersBounds.some((containerBounds: ElementBoundingPositions) => { - const clippedAbove = elementBounds.top < containerBounds.top; - const clippedBelow = elementBounds.bottom > containerBounds.bottom; - const clippedLeft = elementBounds.left < containerBounds.left; - const clippedRight = elementBounds.right > containerBounds.right; - - return clippedAbove || clippedBelow || clippedLeft || clippedRight; - }); - } - /** Physically positions the overlay element to the given coordinate. */ private _setElementPosition( element: HTMLElement, @@ -392,22 +354,11 @@ export class ConnectedPositionStrategy implements PositionStrategy { element.style[horizontalStyleProperty] = `${x}px`; // Notify that the position has been changed along with its change properties. - const scrollableViewProperties = this.getScrollableViewProperties(element); + const scrollableViewProperties = this._getScrollVisibility(element); const positionChange = new ConnectedOverlayPositionChange(pos, scrollableViewProperties); this._onPositionChange.next(positionChange); } - /** Returns the bounding positions of the provided element with respect to the viewport. */ - private _getElementBounds(element: HTMLElement): ElementBoundingPositions { - const boundingClientRect = element.getBoundingClientRect(); - return { - top: boundingClientRect.top, - right: boundingClientRect.left + boundingClientRect.width, - bottom: boundingClientRect.top + boundingClientRect.height, - left: boundingClientRect.left - }; - } - /** * Subtracts the amount that an element is overflowing on an axis from it's length. */ diff --git a/src/cdk/overlay/position/connected-position.ts b/src/cdk/overlay/position/connected-position.ts index 02021649d23..e9affad91db 100644 --- a/src/cdk/overlay/position/connected-position.ts +++ b/src/cdk/overlay/position/connected-position.ts @@ -64,7 +64,7 @@ export class ConnectionPositionPair { * | | * -------------------------- */ -export class ScrollableViewProperties { +export class ScrollingVisibility { isOriginClipped: boolean; isOriginOutsideView: boolean; isOverlayClipped: boolean; @@ -74,5 +74,5 @@ export class ScrollableViewProperties { /** The change event emitted by the strategy when a fallback position is used. */ export class ConnectedOverlayPositionChange { constructor(public connectionPair: ConnectionPositionPair, - @Optional() public scrollableViewProperties: ScrollableViewProperties) {} + @Optional() public scrollableViewProperties: ScrollingVisibility) {} } diff --git a/src/cdk/overlay/position/scroll-clip.ts b/src/cdk/overlay/position/scroll-clip.ts new file mode 100644 index 00000000000..07b2a20d9dc --- /dev/null +++ b/src/cdk/overlay/position/scroll-clip.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// TODO(jelbourn): move this to live with the rest of the scrolling code +// TODO(jelbourn): someday replace this with IntersectionObservers + +/** + * Gets whether an element is scrolled outside of view by any of its parent scrolling containers. + * @param element Dimensions of the element (from getBoundingClientRect) + * @param scrollContainers Dimensions of element's scrolling containers (from getBoundingClientRect) + * @returns Whether the element is scrolled out of view + * @docs-private + */ +export function isElementScrolledOutsideView(element: ClientRect, scrollContainers: ClientRect[]) { + return scrollContainers.some(containerBounds => { + const outsideAbove = element.bottom < containerBounds.top; + const outsideBelow = element.top > containerBounds.bottom; + const outsideLeft = element.right < containerBounds.left; + const outsideRight = element.left > containerBounds.right; + + return outsideAbove || outsideBelow || outsideLeft || outsideRight; + }); +} + + +/** + * Gets whether an element is clipped by any of its scrolling containers. + * @param element Dimensions of the element (from getBoundingClientRect) + * @param scrollContainers Dimensions of element's scrolling containers (from getBoundingClientRect) + * @returns Whether the element is clipped + * @docs-private + */ +export function isElementClippedByScrolling(element: ClientRect, scrollContainers: ClientRect[]) { + return scrollContainers.some(scrollContainerRect => { + const clippedAbove = element.top < scrollContainerRect.top; + const clippedBelow = element.bottom > scrollContainerRect.bottom; + const clippedLeft = element.left < scrollContainerRect.left; + const clippedRight = element.right > scrollContainerRect.right; + + return clippedAbove || clippedBelow || clippedLeft || clippedRight; + }); +}