feat(core): add scrollable view properties to connected pos strategy (#2259)

* feat(scroll): provide directive and service to listen to scrolling

* review response

* fix lint

* move scroll to overlay

* feat(scroll): hide tooltip when clipped by scrollable container

* use overlay for clipping instead of trigger

* add tooltip test

* add test for scroll dispatcher

* add comment about the ScrollableViewProperties

* revert tooltip demo

* add extra lines to tooltip demo files

* add connected position strategy tests

* make lint happy

* add scroll demo to tooltip

* make FF and IE happy in tests

* change test back to Chrome

* remove fdescribe

* resolve comments

* remove usage of scrollables until throttling is set up

* remove sha

* remove combineLatest

* remove unused OnInit import
This commit is contained in:
Andrew Seguin
2016-12-21 16:37:24 -08:00
committed by Jeremy Elbourn
parent 634bd0f0b2
commit b60d33fa44
12 changed files with 731 additions and 423 deletions
+6 -6
View File
@@ -3,12 +3,12 @@
<p class="centered">
<button #tooltip="mdTooltip"
md-raised-button
color="primary"
[mdTooltip]="message"
[mdTooltipPosition]="position"
[mdTooltipShowDelay]="showDelay"
[mdTooltipHideDelay]="hideDelay">
md-raised-button
color="primary"
[mdTooltip]="message"
[mdTooltipPosition]="position"
[mdTooltipShowDelay]="showDelay"
[mdTooltipHideDelay]="hideDelay">
Mouse over to see the tooltip
</button>
</p>
+1 -1
View File
@@ -12,5 +12,5 @@ export class TooltipDemo {
position: TooltipPosition = 'below';
message: string = 'Here is the tooltip';
showDelay = 0;
hideDelay = 0;
hideDelay = 1000;
}
@@ -3,6 +3,9 @@ import {ConnectedPositionStrategy} from './connected-position-strategy';
import {ViewportRuler} from './viewport-ruler';
import {OverlayPositionBuilder} from './overlay-position-builder';
import {ConnectedOverlayPositionChange} from './connected-position';
import {Scrollable} from '../scroll/scrollable';
import {Subscription} from 'rxjs';
import Spy = jasmine.Spy;
// Default width and height of the overlay and origin panels throughout these tests.
@@ -14,415 +17,533 @@ const DEFAULT_WIDTH = 60;
// for tests on CI (both SauceLabs and Browserstack).
describe('ConnectedPositionStrategy', () => {
const ORIGIN_HEIGHT = DEFAULT_HEIGHT;
const ORIGIN_WIDTH = DEFAULT_WIDTH;
const OVERLAY_HEIGHT = DEFAULT_HEIGHT;
const OVERLAY_WIDTH = DEFAULT_WIDTH;
let originElement: HTMLElement;
let overlayElement: HTMLElement;
let overlayContainerElement: HTMLElement;
let strategy: ConnectedPositionStrategy;
let fakeElementRef: ElementRef;
let fakeViewportRuler: FakeViewportRuler;
let positionBuilder: OverlayPositionBuilder;
describe('with origin on document body', () => {
const ORIGIN_HEIGHT = DEFAULT_HEIGHT;
const ORIGIN_WIDTH = DEFAULT_WIDTH;
const OVERLAY_HEIGHT = DEFAULT_HEIGHT;
const OVERLAY_WIDTH = DEFAULT_WIDTH;
let originRect: ClientRect;
let originCenterX: number;
let originCenterY: number;
let originElement: HTMLElement;
let overlayElement: HTMLElement;
let overlayContainerElement: HTMLElement;
let strategy: ConnectedPositionStrategy;
let fakeElementRef: ElementRef;
let fakeViewportRuler: FakeViewportRuler;
let positionBuilder: OverlayPositionBuilder;
beforeEach(() => {
fakeViewportRuler = new FakeViewportRuler();
// The origin and overlay elements need to be in the document body in order to have geometry.
originElement = createPositionedBlockElement();
overlayContainerElement = createFixedElement();
overlayElement = createPositionedBlockElement();
document.body.appendChild(originElement);
document.body.appendChild(overlayContainerElement);
overlayContainerElement.appendChild(overlayElement);
fakeElementRef = new FakeElementRef(originElement);
positionBuilder = new OverlayPositionBuilder(new ViewportRuler());
});
afterEach(() => {
document.body.removeChild(originElement);
document.body.removeChild(overlayContainerElement);
// Reset the origin geometry after each test so we don't accidently keep state between tests.
originRect = null;
originCenterX = null;
originCenterY = null;
});
describe('when not near viewport edge, not scrolled', () => {
// Place the original element close to the center of the window.
// (1024 / 2, 768 / 2). It's not exact, since outerWidth/Height includes browser
// chrome, but it doesn't really matter for these tests.
const ORIGIN_LEFT = 500;
const ORIGIN_TOP = 350;
let originRect: ClientRect;
let originCenterX: number;
let originCenterY: number;
beforeEach(() => {
originElement.style.left = `${ORIGIN_LEFT}px`;
originElement.style.top = `${ORIGIN_TOP}px`;
fakeViewportRuler = new FakeViewportRuler();
originRect = originElement.getBoundingClientRect();
originCenterX = originRect.left + (ORIGIN_WIDTH / 2);
originCenterY = originRect.top + (ORIGIN_HEIGHT / 2);
});
// The origin and overlay elements need to be in the document body in order to have geometry.
originElement = createPositionedBlockElement();
overlayContainerElement = createFixedElement();
overlayElement = createPositionedBlockElement();
document.body.appendChild(originElement);
document.body.appendChild(overlayContainerElement);
overlayContainerElement.appendChild(overlayElement);
// Preconditions are set, now just run the full set of simple position tests.
runSimplePositionTests();
});
describe('when scrolled', () => {
// Place the original element decently far outside the unscrolled document (1024x768).
const ORIGIN_LEFT = 2500;
const ORIGIN_TOP = 2500;
// Create a very large element that will make the page scrollable.
let veryLargeElement: HTMLElement = document.createElement('div');
veryLargeElement.style.width = '4000px';
veryLargeElement.style.height = '4000px';
beforeEach(() => {
// Scroll the page such that the origin element is roughly in the
// center of the visible viewport (2500 - 1024/2, 2500 - 768/2).
document.body.appendChild(veryLargeElement);
document.body.scrollTop = 2100;
document.body.scrollLeft = 2100;
originElement.style.top = `${ORIGIN_TOP}px`;
originElement.style.left = `${ORIGIN_LEFT}px`;
originRect = originElement.getBoundingClientRect();
originCenterX = originRect.left + (ORIGIN_WIDTH / 2);
originCenterY = originRect.top + (ORIGIN_HEIGHT / 2);
fakeElementRef = new FakeElementRef(originElement);
positionBuilder = new OverlayPositionBuilder(new ViewportRuler());
});
afterEach(() => {
document.body.removeChild(veryLargeElement);
document.body.scrollTop = 0;
document.body.scrollLeft = 0;
document.body.removeChild(originElement);
document.body.removeChild(overlayContainerElement);
// Reset the origin geometry after each test so we don't accidently keep state between tests.
originRect = null;
originCenterX = null;
originCenterY = null;
});
// Preconditions are set, now just run the full set of simple position tests.
runSimplePositionTests();
});
describe('when not near viewport edge, not scrolled', () => {
// Place the original element close to the center of the window.
// (1024 / 2, 768 / 2). It's not exact, since outerWidth/Height includes browser
// chrome, but it doesn't really matter for these tests.
const ORIGIN_LEFT = 500;
const ORIGIN_TOP = 350;
describe('when near viewport edge', () => {
it('should reposition the overlay if it would go off the top of the screen', () => {
// We can use the real ViewportRuler in this test since we know that zero is
// always the top of the viewport.
beforeEach(() => {
originElement.style.left = `${ORIGIN_LEFT}px`;
originElement.style.top = `${ORIGIN_TOP}px`;
originElement.style.top = '5px';
originElement.style.left = '200px';
originRect = originElement.getBoundingClientRect();
originRect = originElement.getBoundingClientRect();
originCenterX = originRect.left + (ORIGIN_WIDTH / 2);
originCenterY = originRect.top + (ORIGIN_HEIGHT / 2);
});
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'bottom'})
.withFallbackPosition(
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.left).toBe(originRect.left);
// Preconditions are set, now just run the full set of simple position tests.
runSimplePositionTests();
});
it('should reposition the overlay if it would go off the left of the screen', () => {
// We can use the real ViewportRuler in this test since we know that zero is
// always the left edge of the viewport.
describe('when scrolled', () => {
// Place the original element decently far outside the unscrolled document (1024x768).
const ORIGIN_LEFT = 2500;
const ORIGIN_TOP = 2500;
// Create a very large element that will make the page scrollable.
let veryLargeElement: HTMLElement = document.createElement('div');
veryLargeElement.style.width = '4000px';
veryLargeElement.style.height = '4000px';
beforeEach(() => {
// Scroll the page such that the origin element is roughly in the
// center of the visible viewport (2500 - 1024/2, 2500 - 768/2).
document.body.appendChild(veryLargeElement);
document.body.scrollTop = 2100;
document.body.scrollLeft = 2100;
originElement.style.top = `${ORIGIN_TOP}px`;
originElement.style.left = `${ORIGIN_LEFT}px`;
originRect = originElement.getBoundingClientRect();
originCenterX = originRect.left + (ORIGIN_WIDTH / 2);
originCenterY = originRect.top + (ORIGIN_HEIGHT / 2);
});
afterEach(() => {
document.body.removeChild(veryLargeElement);
document.body.scrollTop = 0;
document.body.scrollLeft = 0;
});
// Preconditions are set, now just run the full set of simple position tests.
runSimplePositionTests();
});
describe('when near viewport edge', () => {
it('should reposition the overlay if it would go off the top of the screen', () => {
// We can use the real ViewportRuler in this test since we know that zero is
// always the top of the viewport.
originElement.style.top = '5px';
originElement.style.left = '200px';
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'bottom'})
.withFallbackPosition(
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.left).toBe(originRect.left);
});
it('should reposition the overlay if it would go off the left of the screen', () => {
// We can use the real ViewportRuler in this test since we know that zero is
// always the left edge of the viewport.
originElement.style.top = '200px';
originElement.style.left = '5px';
originRect = originElement.getBoundingClientRect();
originCenterY = originRect.top + (ORIGIN_HEIGHT / 2);
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'})
.withFallbackPosition(
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originCenterY - (OVERLAY_HEIGHT / 2));
expect(overlayRect.left).toBe(originRect.right);
});
it('should reposition the overlay if it would go off the bottom of the screen', () => {
// Use the fake viewport ruler because we don't know *exactly* how big the viewport is.
fakeViewportRuler.fakeRect = {
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
};
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
originElement.style.top = '475px';
originElement.style.left = '200px';
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'})
.withFallbackPosition(
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'bottom'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.bottom).toBe(originRect.top);
expect(overlayRect.right).toBe(originRect.right);
});
it('should reposition the overlay if it would go off the right of the screen', () => {
// Use the fake viewport ruler because we don't know *exactly* how big the viewport is.
fakeViewportRuler.fakeRect = {
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
};
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
originElement.style.top = '200px';
originElement.style.left = '475px';
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'})
.withFallbackPosition(
{originX: 'start', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.right).toBe(originRect.left);
});
it('should position a panel properly when rtl', () => {
// must make the overlay longer than the origin to properly test attachment
overlayElement.style.width = `500px`;
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'})
.withDirection('rtl');
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.right).toBe(originRect.right);
});
it('should position a panel with the x offset provided', () => {
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'top'});
strategy.withOffsetX(10);
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.top);
expect(overlayRect.left).toBe(originRect.left + 10);
});
it('should position a panel with the y offset provided', () => {
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'top'});
strategy.withOffsetY(50);
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.top + 50);
expect(overlayRect.left).toBe(originRect.left);
});
});
it('should emit onPositionChange event when position changes', () => {
// force the overlay to open in a fallback position
fakeViewportRuler.fakeRect = {
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
};
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
originElement.style.top = '200px';
originElement.style.left = '5px';
originRect = originElement.getBoundingClientRect();
originCenterY = originRect.top + (ORIGIN_HEIGHT / 2);
originElement.style.left = '475px';
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'})
.withFallbackPosition(
fakeElementRef,
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'});
{overlayX: 'start', overlayY: 'center'})
.withFallbackPosition(
{originX: 'start', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'});
const positionChangeHandler = jasmine.createSpy('positionChangeHandler');
const subscription = strategy.onPositionChange.subscribe(positionChangeHandler);
strategy.apply(overlayElement);
expect(positionChangeHandler).toHaveBeenCalled();
expect(positionChangeHandler.calls.mostRecent().args[0])
.toEqual(jasmine.any(ConnectedOverlayPositionChange),
`Expected strategy to emit an instance of ConnectedOverlayPositionChange.`);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originCenterY - (OVERLAY_HEIGHT / 2));
expect(overlayRect.left).toBe(originRect.right);
it('should pick the fallback position that shows the largest area of the element', () => {
// Use the fake viewport ruler because we don't know *exactly* how big the viewport is.
fakeViewportRuler.fakeRect = {
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
};
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
originElement.style.top = '200px';
originElement.style.left = '475px';
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'})
.withFallbackPosition(
{originX: 'end', originY: 'top'},
{overlayX: 'start', overlayY: 'bottom'})
.withFallbackPosition(
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'top'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.top);
expect(overlayRect.left).toBe(originRect.left);
});
it('should position a panel properly when rtl', () => {
// must make the overlay longer than the origin to properly test attachment
overlayElement.style.width = `500px`;
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'})
.withDirection('rtl');
originElement.style.top = '0';
originElement.style.left = '0';
// If the strategy is re-applied and the initial position would now fit,
// the position change event should be emitted again.
strategy.apply(overlayElement);
expect(positionChangeHandler).toHaveBeenCalledTimes(2);
subscription.unsubscribe();
});
});
it('should reposition the overlay if it would go off the bottom of the screen', () => {
// Use the fake viewport ruler because we don't know *exactly* how big the viewport is.
fakeViewportRuler.fakeRect = {
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
};
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
originElement.style.top = '475px';
originElement.style.left = '200px';
originRect = originElement.getBoundingClientRect();
/**
* Run all tests for connecting the overlay to the origin such that first preferred
* position does not go off-screen. We do this because there are several cases where we
* want to run the exact same tests with different preconditions (e.g., not scroll, scrolled,
* different element sized, etc.).
*/
function runSimplePositionTests() {
it('should position a panel below, left-aligned', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'});
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'})
.withFallbackPosition(
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'bottom'});
strategy.apply(overlayElement);
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.left).toBe(originRect.left);
});
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.bottom).toBe(originRect.top);
expect(overlayRect.right).toBe(originRect.right);
});
it('should position to the right, center aligned vertically', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'});
it('should reposition the overlay if it would go off the right of the screen', () => {
// Use the fake viewport ruler because we don't know *exactly* how big the viewport is.
fakeViewportRuler.fakeRect = {
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
};
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
strategy.apply(overlayElement);
originElement.style.top = '200px';
originElement.style.left = '475px';
originRect = originElement.getBoundingClientRect();
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originCenterY - (OVERLAY_HEIGHT / 2));
expect(overlayRect.left).toBe(originRect.right);
});
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'})
.withFallbackPosition(
{originX: 'start', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.right).toBe(originRect.left);
});
it('should pick the fallback position that shows the largest area of the element', () => {
// Use the fake viewport ruler because we don't know *exactly* how big the viewport is.
fakeViewportRuler.fakeRect = {
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
};
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
originElement.style.top = '200px';
originElement.style.left = '475px';
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'})
.withFallbackPosition(
{originX: 'end', originY: 'top'},
{overlayX: 'start', overlayY: 'bottom'})
.withFallbackPosition(
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'top'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.top);
expect(overlayRect.left).toBe(originRect.left);
});
it('should position a panel properly when rtl', () => {
// must make the overlay longer than the origin to properly test attachment
overlayElement.style.width = `500px`;
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'})
.withDirection('rtl');
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.right).toBe(originRect.right);
});
it('should position a panel with the x offset provided', () => {
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'top'});
strategy.withOffsetX(10);
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.top);
expect(overlayRect.left).toBe(originRect.left + 10);
});
it('should position a panel with the y offset provided', () => {
originRect = originElement.getBoundingClientRect();
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'top'});
strategy.withOffsetY(50);
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.top + 50);
expect(overlayRect.left).toBe(originRect.left);
});
});
it('should emit onPositionChange event when position changes', () => {
// force the overlay to open in a fallback position
fakeViewportRuler.fakeRect = {
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
};
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
originElement.style.top = '200px';
originElement.style.left = '475px';
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'})
.withFallbackPosition(
it('should position to the left, below', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'});
const positionChangeHandler = jasmine.createSpy('positionChangeHandler');
const subscription = strategy.onPositionChange.subscribe(positionChangeHandler);
strategy.apply(overlayElement);
strategy.apply(overlayElement);
expect(positionChangeHandler).toHaveBeenCalled();
expect(positionChangeHandler.calls.mostRecent().args[0])
.toEqual(jasmine.any(ConnectedOverlayPositionChange),
`Expected strategy to emit an instance of ConnectedOverlayPositionChange.`);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.right).toBe(originRect.left);
});
originElement.style.top = '0';
originElement.style.left = '0';
it('should position above, right aligned', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'bottom'});
// If the strategy is re-applied and the initial position would now fit,
// the position change event should be emitted again.
strategy.apply(overlayElement);
expect(positionChangeHandler).toHaveBeenCalledTimes(2);
strategy.apply(overlayElement);
subscription.unsubscribe();
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.bottom).toBe(originRect.top);
expect(overlayRect.right).toBe(originRect.right);
});
it('should position below, centered', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'center', originY: 'bottom'},
{overlayX: 'center', overlayY: 'top'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.left).toBe(originCenterX - (OVERLAY_WIDTH / 2));
});
it('should center the overlay on the origin', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'center', originY: 'center'},
{overlayX: 'center', overlayY: 'center'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.top);
expect(overlayRect.left).toBe(originRect.left);
});
}
});
describe('onPositionChange with scrollable view properties', () => {
let overlayElement: HTMLElement;
let overlayContainerElement: HTMLElement;
let strategy: ConnectedPositionStrategy;
/**
* Run all tests for connecting the overlay to the origin such that first preferred
* position does not go off-screen. We do this because there are several cases where we
* want to run the exact same tests with different preconditions (e.g., not scroll, scrolled,
* different element sized, etc.).
*/
function runSimplePositionTests() {
it('should position a panel below, left-aligned', () => {
let scrollable: HTMLDivElement;
let positionChangeHandler: Spy;
let onPositionChangeSubscription: Subscription;
let positionChange: ConnectedOverlayPositionChange;
beforeEach(() => {
// Set up the overlay
overlayContainerElement = createFixedElement();
overlayElement = createPositionedBlockElement();
document.body.appendChild(overlayContainerElement);
overlayContainerElement.appendChild(overlayElement);
// Set up the origin
let originElement = createBlockElement();
originElement.style.margin = '0 1000px 1000px 0'; // Added so that the container scrolls
// Create a scrollable container and put the origin inside
scrollable = createOverflowContainerElement();
document.body.appendChild(scrollable);
scrollable.appendChild(originElement);
// Create a strategy with knowledge of the scrollable container
let positionBuilder = new OverlayPositionBuilder(new ViewportRuler());
let fakeElementRef = new FakeElementRef(originElement);
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'});
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'});
strategy.withScrollableContainers([new Scrollable(new FakeElementRef(scrollable), null)]);
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.left).toBe(originRect.left);
positionChangeHandler = jasmine.createSpy('positionChangeHandler');
onPositionChangeSubscription = strategy.onPositionChange.subscribe(positionChangeHandler);
});
it('should position to the right, center aligned vertically', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'});
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originCenterY - (OVERLAY_HEIGHT / 2));
expect(overlayRect.left).toBe(originRect.right);
afterEach(() => {
onPositionChangeSubscription.unsubscribe();
document.body.removeChild(scrollable);
document.body.removeChild(overlayContainerElement);
});
it('should position to the left, below', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'});
it('should not have origin or overlay clipped or out of view without scroll', () => {
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.right).toBe(originRect.left);
expect(positionChangeHandler).toHaveBeenCalled();
positionChange = positionChangeHandler.calls.mostRecent().args[0];
expect(positionChange.scrollableViewProperties).toEqual({
isOriginClipped: false,
isOriginOutsideView: false,
isOverlayClipped: false,
isOverlayOutsideView: false
});
});
it('should position above, right aligned', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'bottom'});
it('should evaluate if origin is clipped if scrolled slightly down', () => {
scrollable.scrollTop = 10; // Clip the origin by 10 pixels
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.bottom).toBe(originRect.top);
expect(overlayRect.right).toBe(originRect.right);
expect(positionChangeHandler).toHaveBeenCalled();
positionChange = positionChangeHandler.calls.mostRecent().args[0];
expect(positionChange.scrollableViewProperties).toEqual({
isOriginClipped: true,
isOriginOutsideView: false,
isOverlayClipped: false,
isOverlayOutsideView: false
});
});
it('should position below, centered', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'center', originY: 'bottom'},
{overlayX: 'center', overlayY: 'top'});
it('should evaluate if origin is out of view and overlay is clipped if scrolled enough', () => {
scrollable.scrollTop = 31; // Origin is 30 pixels, move out of view and clip the overlay 1px
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.left).toBe(originCenterX - (OVERLAY_WIDTH / 2));
expect(positionChangeHandler).toHaveBeenCalled();
positionChange = positionChangeHandler.calls.mostRecent().args[0];
expect(positionChange.scrollableViewProperties).toEqual({
isOriginClipped: true,
isOriginOutsideView: true,
isOverlayClipped: true,
isOverlayOutsideView: false
});
});
it('should center the overlay on the origin', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'center', originY: 'center'},
{overlayX: 'center', overlayY: 'center'});
it('should evaluate the overlay and origin are both out of the view', () => {
scrollable.scrollTop = 61; // Scroll by overlay height + origin height + 1px buffer
strategy.apply(overlayElement);
let overlayRect = overlayElement.getBoundingClientRect();
expect(overlayRect.top).toBe(originRect.top);
expect(overlayRect.left).toBe(originRect.left);
expect(positionChangeHandler).toHaveBeenCalled();
positionChange = positionChangeHandler.calls.mostRecent().args[0];
expect(positionChange.scrollableViewProperties).toEqual({
isOriginClipped: true,
isOriginOutsideView: true,
isOverlayClipped: true,
isOverlayOutsideView: true
});
});
}
});
});
/** Creates an absolutely positioned, display: block element with a default size. */
function createPositionedBlockElement() {
let element = document.createElement('div');
let element = createBlockElement();
element.style.position = 'absolute';
element.style.top = '0';
element.style.left = '0';
return element;
}
/** Creates a block element with a default size. */
function createBlockElement() {
let element = document.createElement('div');
element.style.width = `${DEFAULT_WIDTH}px`;
element.style.height = `${DEFAULT_HEIGHT}px`;
element.style.backgroundColor = 'rebeccapurple';
@@ -442,6 +563,17 @@ function createFixedElement() {
return element;
}
/** Creates an overflow container with a set height and width with margin. */
function createOverflowContainerElement() {
let element = document.createElement('div');
element.style.position = 'relative';
element.style.overflow = 'auto';
element.style.height = '300px';
element.style.width = '300px';
element.style.margin = '100px';
return element;
}
/** Fake implementation of ViewportRuler that just returns the previously given ClientRect. */
class FakeViewportRuler implements ViewportRuler {
@@ -2,13 +2,26 @@ import {PositionStrategy} from './position-strategy';
import {ElementRef} from '@angular/core';
import {ViewportRuler} from './viewport-ruler';
import {
ConnectionPositionPair,
OriginConnectionPosition,
OverlayConnectionPosition,
ConnectedOverlayPositionChange
ConnectionPositionPair,
OriginConnectionPosition,
OverlayConnectionPosition,
ConnectedOverlayPositionChange, ScrollableViewProperties
} from './connected-position';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
import {Scrollable} from '../scroll/scrollable';
/**
* 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.
*/
export type ElementBoundingPositions = {
top: number;
right: number;
bottom: number;
left: number;
}
/**
* A strategy for positioning overlays. Using this strategy, an overlay is given an
@@ -26,6 +39,9 @@ export class ConnectedPositionStrategy implements PositionStrategy {
/** The offset in pixels for the overlay connection point on the y-axis */
private _offsetY: number = 0;
/** The Scrollable containers used to check scrollable view properties on position change. */
private scrollables: Scrollable[] = [];
/** Whether the we're dealing with an RTL context */
get _isRtl() {
return this._dir === 'rtl';
@@ -37,7 +53,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
/** The origin element against which the overlay will be positioned. */
private _origin: HTMLElement;
private _onPositionChange:
_onPositionChange:
Subject<ConnectedOverlayPositionChange> = new Subject<ConnectedOverlayPositionChange>();
/** Emits an event when the connection point changes. */
@@ -95,7 +111,12 @@ export class ConnectedPositionStrategy implements PositionStrategy {
// If the overlay in the calculated position fits on-screen, put it there and we're done.
if (overlayPoint.fitsInViewport) {
this._setElementPosition(element, overlayPoint);
this._onPositionChange.next(new ConnectedOverlayPositionChange(pos));
// Notify that the position has been changed along with its change properties.
const scrollableViewProperties = this.getScrollableViewProperties(element);
const positionChange = new ConnectedOverlayPositionChange(pos, scrollableViewProperties);
this._onPositionChange.next(positionChange);
return Promise.resolve(null);
} else if (!fallbackPoint || fallbackPoint.visibleArea < overlayPoint.visibleArea) {
fallbackPoint = overlayPoint;
@@ -109,6 +130,15 @@ export class ConnectedPositionStrategy implements PositionStrategy {
return Promise.resolve(null);
}
/**
* Sets the list of Scrollable containers that host the origin element so that
* on reposition we can evaluate if it or the overlay has been clipped or outside view. Every
* Scrollable must be an ancestor element of the strategy's origin element.
*/
withScrollableContainers(scrollables: Scrollable[]) {
this.scrollables = scrollables;
}
/**
* Adds a new preferred fallback position.
* @param originPos
@@ -241,6 +271,53 @@ export class ConnectedPositionStrategy implements PositionStrategy {
return {x, y, fitsInViewport, visibleArea};
}
/**
* 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);
});
return {
isOriginClipped: this.isElementClipped(originBounds, scrollContainerBounds),
isOriginOutsideView: this.isElementOutsideView(originBounds, scrollContainerBounds),
isOverlayClipped: this.isElementClipped(overlayBounds, scrollContainerBounds),
isOverlayOutsideView: this.isElementOutsideView(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.
* @param element
@@ -251,6 +328,17 @@ export class ConnectedPositionStrategy implements PositionStrategy {
element.style.top = overlayPoint.y + 'px';
}
/** 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.
*/
@@ -265,7 +353,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
interface Point {
x: number;
y: number;
};
}
/**
* Expands the simple (x, y) coordinate by adding info about whether the
@@ -1,4 +1,5 @@
/** Horizontal dimension of a connection point on the perimeter of the origin or overlay element. */
import {Optional} from '@angular/core';
export type HorizontalConnectionPos = 'start' | 'center' | 'end';
/** Vertical dimension of a connection point on the perimeter of the origin or overlay element. */
@@ -32,7 +33,38 @@ export class ConnectionPositionPair {
}
}
/**
* Set of properties regarding the position of the origin and overlay relative to the viewport
* with respect to the containing Scrollable elements.
*
* The overlay and origin are clipped if any part of their bounding client rectangle exceeds the
* bounds of any one of the strategy's Scrollable's bounding client rectangle.
*
* The overlay and origin are outside view if there is no overlap between their bounding client
* rectangle and any one of the strategy's Scrollable's bounding client rectangle.
*
* ----------- -----------
* | outside | | clipped |
* | view | --------------------------
* | | | | | |
* ---------- | ----------- |
* -------------------------- | |
* | | | Scrollable |
* | | | |
* | | --------------------------
* | Scrollable |
* | |
* --------------------------
*/
export class ScrollableViewProperties {
isOriginClipped: boolean;
isOriginOutsideView: boolean;
isOverlayClipped: boolean;
isOverlayOutsideView: boolean;
}
/** The change event emitted by the strategy when a fallback position is used. */
export class ConnectedOverlayPositionChange {
constructor(public connectionPair: ConnectionPositionPair) {}
constructor(public connectionPair: ConnectionPositionPair,
@Optional() public scrollableViewProperties: ScrollableViewProperties) {}
}
@@ -1,12 +1,10 @@
import {inject, TestBed, async, ComponentFixture} from '@angular/core/testing';
import {NgModule, Component, ViewChild, ElementRef} from '@angular/core';
import {NgModule, Component, ViewChild, ElementRef, QueryList, ViewChildren} from '@angular/core';
import {ScrollDispatcher} from './scroll-dispatcher';
import {OverlayModule} from '../overlay-directives';
import {Scrollable} from './scrollable';
describe('Scroll Dispatcher', () => {
let scroll: ScrollDispatcher;
let fixture: ComponentFixture<ScrollingComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -16,45 +14,71 @@ describe('Scroll Dispatcher', () => {
TestBed.compileComponents();
}));
beforeEach(inject([ScrollDispatcher], (s: ScrollDispatcher) => {
scroll = s;
describe('Basic usage', () => {
let scroll: ScrollDispatcher;
let fixture: ComponentFixture<ScrollingComponent>;
fixture = TestBed.createComponent(ScrollingComponent);
fixture.detectChanges();
}));
beforeEach(inject([ScrollDispatcher], (s: ScrollDispatcher) => {
scroll = s;
it('should be registered with the scrollable directive with the scroll service', () => {
const componentScrollable = fixture.componentInstance.scrollable;
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true);
fixture = TestBed.createComponent(ScrollingComponent);
fixture.detectChanges();
}));
it('should be registered with the scrollable directive with the scroll service', () => {
const componentScrollable = fixture.componentInstance.scrollable;
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true);
});
it('should have the scrollable directive deregistered when the component is destroyed', () => {
const componentScrollable = fixture.componentInstance.scrollable;
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true);
fixture.destroy();
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(false);
});
it('should notify through the directive and service that a scroll event occurred', () => {
let hasDirectiveScrollNotified = false;
// Listen for notifications from scroll directive
let scrollable = fixture.componentInstance.scrollable;
scrollable.elementScrolled().subscribe(() => { hasDirectiveScrollNotified = true; });
// Listen for notifications from scroll service
let hasServiceScrollNotified = false;
scroll.scrolled().subscribe(() => { hasServiceScrollNotified = true; });
// Emit a scroll event from the scrolling element in our component.
// This event should be picked up by the scrollable directive and notify.
// The notification should be picked up by the service.
const scrollEvent = document.createEvent('UIEvents');
scrollEvent.initUIEvent('scroll', true, true, window, 0);
fixture.componentInstance.scrollingElement.nativeElement.dispatchEvent(scrollEvent);
expect(hasDirectiveScrollNotified).toBe(true);
expect(hasServiceScrollNotified).toBe(true);
});
});
it('should have the scrollable directive deregistered when the component is destroyed', () => {
const componentScrollable = fixture.componentInstance.scrollable;
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true);
describe('Nested scrollables', () => {
let scroll: ScrollDispatcher;
let fixture: ComponentFixture<NestedScrollingComponent>;
fixture.destroy();
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(false);
});
beforeEach(inject([ScrollDispatcher], (s: ScrollDispatcher) => {
scroll = s;
it('should notify through the directive and service that a scroll event occurred', () => {
let hasDirectiveScrollNotified = false;
// Listen for notifications from scroll directive
let scrollable = fixture.componentInstance.scrollable;
scrollable.elementScrolled().subscribe(() => { hasDirectiveScrollNotified = true; });
fixture = TestBed.createComponent(NestedScrollingComponent);
fixture.detectChanges();
}));
// Listen for notifications from scroll service
let hasServiceScrollNotified = false;
scroll.scrolled().subscribe(() => { hasServiceScrollNotified = true; });
it('should be able to identify the containing scrollables of an element', () => {
const interestingElement = fixture.componentInstance.interestingElement;
const scrollContainers = scroll.getScrollContainers(interestingElement);
const scrollableElementIds =
scrollContainers.map(scrollable => scrollable.getElementRef().nativeElement.id);
// Emit a scroll event from the scrolling element in our component.
// This event should be picked up by the scrollable directive and notify.
// The notification should be picked up by the service.
const scrollEvent = document.createEvent('UIEvents');
scrollEvent.initUIEvent('scroll', true, true, window, 0);
fixture.componentInstance.scrollingElement.nativeElement.dispatchEvent(scrollEvent);
expect(hasDirectiveScrollNotified).toBe(true);
expect(hasServiceScrollNotified).toBe(true);
expect(scrollableElementIds).toEqual(['scrollable-1', 'scrollable-1a']);
});
});
});
@@ -68,7 +92,25 @@ class ScrollingComponent {
@ViewChild('scrollingElement') scrollingElement: ElementRef;
}
const TEST_COMPONENTS = [ScrollingComponent];
/** Component containing nested scrollables. */
@Component({
template: `
<div id="scrollable-1" cdk-scrollable>
<div id="scrollable-1a" cdk-scrollable>
<div #interestingElement></div>
</div>
<div id="scrollable-1b" cdk-scrollable></div>
</div>
<div id="scrollable-2" cdk-scrollable></div>
`
})
class NestedScrollingComponent {
@ViewChild('interestingElement') interestingElement: ElementRef;
@ViewChildren(Scrollable) scrollables: QueryList<Scrollable>;
}
const TEST_COMPONENTS = [ScrollingComponent, NestedScrollingComponent];
@NgModule({
imports: [OverlayModule],
providers: [ScrollDispatcher],
@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, ElementRef} from '@angular/core';
import {Scrollable} from './scrollable';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
@@ -19,7 +19,7 @@ export class ScrollDispatcher {
* Map of all the scrollable references that are registered with the service and their
* scroll event subscriptions.
*/
scrollableReferences: WeakMap<Scrollable, Subscription> = new WeakMap();
scrollableReferences: Map<Scrollable, Subscription> = new Map();
constructor() {
// By default, notify a scroll event when the document is scrolled or the window is resized.
@@ -57,8 +57,34 @@ export class ScrollDispatcher {
return this._scrolled.asObservable();
}
/** Returns all registered Scrollables that contain the provided element. */
getScrollContainers(elementRef: ElementRef): Scrollable[] {
const scrollingContainers: Scrollable[] = [];
this.scrollableReferences.forEach((subscription: Subscription, scrollable: Scrollable) => {
if (this.scrollableContainsElement(scrollable, elementRef)) {
scrollingContainers.push(scrollable);
}
});
return scrollingContainers;
}
/** Returns true if the element is contained within the provided Scrollable. */
scrollableContainsElement(scrollable: Scrollable, elementRef: ElementRef): boolean {
let element = elementRef.nativeElement;
let scrollableElement = scrollable.getElementRef().nativeElement;
// Traverse through the element parents until we reach null, checking if any of the elements
// are the scrollable's element.
do {
if (element == scrollableElement) { return true; }
} while (element = element.parentElement);
}
/** Sends a notification that a scroll event has been fired. */
_notify() {
this._scrolled.next();
}
}
+7 -4
View File
@@ -1,6 +1,4 @@
import {
Directive, ElementRef, OnInit, OnDestroy
} from '@angular/core';
import {Directive, ElementRef, OnInit, OnDestroy} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {ScrollDispatcher} from './scroll-dispatcher';
import 'rxjs/add/observable/fromEvent';
@@ -15,7 +13,8 @@ import 'rxjs/add/observable/fromEvent';
selector: '[cdk-scrollable]'
})
export class Scrollable implements OnInit, OnDestroy {
constructor(private _elementRef: ElementRef, private _scroll: ScrollDispatcher) {}
constructor(private _elementRef: ElementRef,
private _scroll: ScrollDispatcher) {}
ngOnInit() {
this._scroll.register(this);
@@ -31,4 +30,8 @@ export class Scrollable implements OnInit, OnDestroy {
elementScrolled(): Observable<any> {
return Observable.fromEvent(this._elementRef.nativeElement, 'scroll');
}
getElementRef(): ElementRef {
return this._elementRef;
}
}
+1 -1
View File
@@ -3,6 +3,6 @@
<ng-content select="md-sidenav, mat-sidenav"></ng-content>
<div class="md-sidenav-content" [ngStyle]="_getStyles()" cdk-scrollable>
<div class="md-sidenav-content" [ngStyle]="_getStyles()">
<ng-content></ng-content>
</div>
+1 -2
View File
@@ -22,7 +22,6 @@ import {FocusTrap} from '../core/a11y/focus-trap';
import {ESCAPE} from '../core/keyboard/keycodes';
import {OverlayModule} from '../core/overlay/overlay-directives';
import {InteractivityChecker} from '../core/a11y/interactivity-checker';
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
/** Exception thrown when two MdSidenav are matching the same side. */
@@ -516,7 +515,7 @@ export class MdSidenavModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: MdSidenavModule,
providers: [InteractivityChecker, ScrollDispatcher]
providers: [InteractivityChecker]
};
}
}
+9 -7
View File
@@ -1,16 +1,17 @@
import {
async,
ComponentFixture,
TestBed,
tick,
fakeAsync,
flushMicrotasks
async,
ComponentFixture,
TestBed,
tick,
fakeAsync,
flushMicrotasks
} from '@angular/core/testing';
import {Component, DebugElement, AnimationTransitionEvent} from '@angular/core';
import {By} from '@angular/platform-browser';
import {TooltipPosition, MdTooltip, MdTooltipModule} from './tooltip';
import {OverlayContainer} from '../core';
import {Dir, LayoutDirection} from '../core/rtl/dir';
import {OverlayModule} from '../core/overlay/overlay-directives';
const initialTooltipMessage = 'initial tooltip message';
@@ -20,7 +21,7 @@ describe('MdTooltip', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdTooltipModule.forRoot()],
imports: [MdTooltipModule.forRoot(), OverlayModule],
declarations: [BasicTooltipDemo],
providers: [
{provide: OverlayContainer, useFactory: () => {
@@ -312,3 +313,4 @@ class BasicTooltipDemo {
message: string = initialTooltipMessage;
showButton: boolean = true;
}
+4 -20
View File
@@ -14,8 +14,7 @@ import {
AnimationTransitionEvent,
NgZone,
Optional,
OnDestroy,
OnInit
OnDestroy
} from '@angular/core';
import {
Overlay,
@@ -31,7 +30,6 @@ import {MdTooltipInvalidPositionError} from './tooltip-errors';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {Dir} from '../core/rtl/dir';
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
import {OVERLAY_PROVIDERS} from '../core/overlay/overlay';
import 'rxjs/add/operator/first';
@@ -56,7 +54,7 @@ export const TOUCHEND_HIDE_DELAY = 1500;
},
exportAs: 'mdTooltip',
})
export class MdTooltip implements OnInit, OnDestroy {
export class MdTooltip implements OnDestroy {
_overlayRef: OverlayRef;
_tooltipInstance: TooltipComponent;
@@ -105,21 +103,10 @@ export class MdTooltip implements OnInit, OnDestroy {
set _deprecatedMessage(v: string) { this.message = v; }
constructor(private _overlay: Overlay,
private _scrollDispatcher: ScrollDispatcher,
private _elementRef: ElementRef,
private _viewContainerRef: ViewContainerRef,
private _ngZone: NgZone,
@Optional() private _dir: Dir) {}
ngOnInit() {
// When a scroll on the page occurs, update the position in case this tooltip needs
// to be repositioned.
this._scrollDispatcher.scrolled().subscribe(() => {
if (this._overlayRef) {
this._overlayRef.updatePosition();
}
});
}
@Optional() private _dir: Dir) { }
/**
* Dispose the tooltip when destroyed.
@@ -400,10 +387,7 @@ export class MdTooltipModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: MdTooltipModule,
providers: [
OVERLAY_PROVIDERS,
ScrollDispatcher
]
providers: [OVERLAY_PROVIDERS]
};
}
}