Skip to content

Commit e061211

Browse files
committed
add tooltip test
1 parent e86755d commit e061211

File tree

4 files changed

+136
-25
lines changed

4 files changed

+136
-25
lines changed

src/lib/core/overlay/position/connected-position-strategy.ts

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import {PositionStrategy} from './position-strategy';
22
import {ElementRef} from '@angular/core';
33
import {ViewportRuler} from './viewport-ruler';
44
import {
5-
ConnectionPositionPair,
6-
OriginConnectionPosition,
7-
OverlayConnectionPosition,
8-
ConnectedOverlayPositionChange
5+
ConnectionPositionPair,
6+
OriginConnectionPosition,
7+
OverlayConnectionPosition,
8+
ConnectedOverlayPositionChange, ScrollableViewProperties
99
} from './connected-position';
1010
import {Subject} from 'rxjs/Subject';
1111
import {Observable} from 'rxjs/Observable';
@@ -35,7 +35,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
3535
private _offsetY: number = 0;
3636

3737
/** The Scrollable containers that may cause the overlay's connectedTo element to be clipped */
38-
private scrollables: Scrollable[] = [];
38+
private scrollables: Scrollable[];
3939

4040
/** Whether the we're dealing with an RTL context */
4141
get _isRtl() {
@@ -48,7 +48,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
4848
/** The origin element against which the overlay will be positioned. */
4949
private _origin: HTMLElement;
5050

51-
private _onPositionChange:
51+
_onPositionChange:
5252
Subject<ConnectedOverlayPositionChange> = new Subject<ConnectedOverlayPositionChange>();
5353

5454
/** Emits an event when the connection point changes. */
@@ -106,8 +106,12 @@ export class ConnectedPositionStrategy implements PositionStrategy {
106106
// If the overlay in the calculated position fits on-screen, put it there and we're done.
107107
if (overlayPoint.fitsInViewport) {
108108
this._setElementPosition(element, overlayPoint);
109-
const isClipped = this.isOverlayElementClipped(element);
110-
this._onPositionChange.next(new ConnectedOverlayPositionChange(pos, isClipped));
109+
110+
// Notify that the position has been changed along with its change properties.
111+
const scrollableViewProperties = this.getScrollableViewProperties(element);
112+
const positionChange = new ConnectedOverlayPositionChange(pos, scrollableViewProperties);
113+
this._onPositionChange.next(positionChange);
114+
111115
return Promise.resolve(null);
112116
} else if (!fallbackPoint || fallbackPoint.visibleArea < overlayPoint.visibleArea) {
113117
fallbackPoint = overlayPoint;
@@ -262,16 +266,48 @@ export class ConnectedPositionStrategy implements PositionStrategy {
262266
return {x, y, fitsInViewport, visibleArea};
263267
}
264268

265-
/** Whether the overlay element is clipped out of view of one of the scrollable containers. */
266-
private isOverlayElementClipped(element: HTMLElement): boolean {
267-
const elementBounds = this._getElementBounds(element);
268-
return this.scrollables.some((scrollable: Scrollable) => {
269-
const scrollingContainerBounds = this._getElementBounds(scrollable.getElementRef().nativeElement);
269+
/**
270+
* Gets the view properties of the trigger and overlay, including whether they are clipped
271+
* or completely outside the view of any of the strategy's scrollables.
272+
*/
273+
private getScrollableViewProperties(overlay: HTMLElement): ScrollableViewProperties {
274+
const triggerBounds = this._getElementBounds(this._connectedTo.nativeElement);
275+
const overlayBounds = this._getElementBounds(overlay);
276+
const scrollContainerBounds = this.scrollables.map((scrollable: Scrollable) => {
277+
return this._getElementBounds(scrollable.getElementRef().nativeElement);
278+
});
270279

271-
const clippedAbove = elementBounds.top < scrollingContainerBounds.top;
272-
const clippedBelow = elementBounds.bottom > scrollingContainerBounds.bottom;
273-
const clippedLeft = elementBounds.left < scrollingContainerBounds.left;
274-
const clippedRight = elementBounds.right > scrollingContainerBounds.right;
280+
return {
281+
isTriggerClipped: this.isElementClipped(triggerBounds, scrollContainerBounds),
282+
isTriggerOutsideView: this.isElementOutsideView(triggerBounds, scrollContainerBounds),
283+
isOverlayClipped: this.isElementClipped(overlayBounds, scrollContainerBounds),
284+
isOverlayOutsideView: this.isElementOutsideView(overlayBounds, scrollContainerBounds),
285+
};
286+
}
287+
288+
/** Whether the element is completely out of the view of any of the containers. */
289+
private isElementOutsideView(
290+
elementBounds: ElementBoundingPositions,
291+
containersBounds: ElementBoundingPositions[]): boolean {
292+
return containersBounds.some((containerBounds: ElementBoundingPositions) => {
293+
const outsideAbove = elementBounds.bottom < containerBounds.top;
294+
const outsideBelow = elementBounds.top > containerBounds.bottom;
295+
const outsideLeft = elementBounds.right < containerBounds.left;
296+
const outsideRight = elementBounds.left > containerBounds.right;
297+
298+
return outsideAbove || outsideBelow || outsideLeft || outsideRight;
299+
});
300+
}
301+
302+
/** Whether the element is clipped by any of the containers. */
303+
private isElementClipped(
304+
elementBounds: ElementBoundingPositions,
305+
containersBounds: ElementBoundingPositions[]): boolean {
306+
return containersBounds.some((containerBounds: ElementBoundingPositions) => {
307+
const clippedAbove = elementBounds.top < containerBounds.top;
308+
const clippedBelow = elementBounds.bottom > containerBounds.bottom;
309+
const clippedLeft = elementBounds.left < containerBounds.left;
310+
const clippedRight = elementBounds.right > containerBounds.right;
275311

276312
return clippedAbove || clippedBelow || clippedLeft || clippedRight;
277313
});
@@ -311,7 +347,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
311347
interface Point {
312348
x: number;
313349
y: number;
314-
};
350+
}
315351

316352
/**
317353
* Expands the simple (x, y) coordinate by adding info about whether the

src/lib/core/overlay/position/connected-position.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@ export class ConnectionPositionPair {
3232
}
3333
}
3434

35+
export class ScrollableViewProperties {
36+
isTriggerClipped: boolean;
37+
isTriggerOutsideView: boolean;
38+
isOverlayClipped: boolean;
39+
isOverlayOutsideView: boolean;
40+
}
41+
3542
/** The change event emitted by the strategy when a fallback position is used. */
3643
export class ConnectedOverlayPositionChange {
37-
constructor(public connectionPair: ConnectionPositionPair, public isClipped: boolean) {}
44+
constructor(public connectionPair: ConnectionPositionPair,
45+
public scrollableViewProperties: ScrollableViewProperties) {}
3846
}

src/lib/tooltip/tooltip.spec.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import {
66
fakeAsync,
77
flushMicrotasks
88
} from '@angular/core/testing';
9-
import {Component, DebugElement, AnimationTransitionEvent} from '@angular/core';
9+
import {Component, DebugElement, AnimationTransitionEvent, ViewChild} from '@angular/core';
1010
import {By} from '@angular/platform-browser';
1111
import {TooltipPosition, MdTooltip, MdTooltipModule} from './tooltip';
1212
import {OverlayContainer} from '../core';
1313
import {Dir, LayoutDirection} from '../core/rtl/dir';
14+
import {OverlayModule} from '../core/overlay/overlay-directives';
15+
import {Scrollable} from '../core/overlay/scroll/scrollable';
1416

1517
const initialTooltipMessage = 'initial tooltip message';
1618

@@ -20,8 +22,8 @@ describe('MdTooltip', () => {
2022

2123
beforeEach(async(() => {
2224
TestBed.configureTestingModule({
23-
imports: [MdTooltipModule.forRoot()],
24-
declarations: [BasicTooltipDemo],
25+
imports: [MdTooltipModule.forRoot(), OverlayModule],
26+
declarations: [BasicTooltipDemo, ScrollableTooltipDemo],
2527
providers: [
2628
{provide: OverlayContainer, useFactory: () => {
2729
overlayContainerElement = document.createElement('div');
@@ -296,6 +298,34 @@ describe('MdTooltip', () => {
296298
}).toThrowError('Tooltip position "everywhere" is invalid.');
297299
});
298300
});
301+
302+
303+
fdescribe('scrollable usage', () => {
304+
let fixture: ComponentFixture<ScrollableTooltipDemo>;
305+
let buttonDebugElement: DebugElement;
306+
let buttonElement: HTMLButtonElement;
307+
let tooltipDirective: MdTooltip;
308+
309+
beforeEach(() => {
310+
fixture = TestBed.createComponent(ScrollableTooltipDemo);
311+
fixture.detectChanges();
312+
buttonDebugElement = fixture.debugElement.query(By.css('button'));
313+
buttonElement = <HTMLButtonElement> buttonDebugElement.nativeElement;
314+
tooltipDirective = buttonDebugElement.injector.get(MdTooltip);
315+
});
316+
317+
it('should hide tooltip if clipped after changing positions', fakeAsync(() => {
318+
expect(tooltipDirective._tooltipInstance).toBeUndefined();
319+
320+
tooltipDirective.show();
321+
tick(0); // Tick for the show delay (default is 0)
322+
expect(tooltipDirective._isTooltipVisible()).toBe(true);
323+
324+
fixture.componentInstance.scrollDown();
325+
tick();
326+
expect(tooltipDirective._isTooltipVisible()).toBe(false);
327+
}));
328+
});
299329
});
300330

301331
@Component({
@@ -312,3 +342,34 @@ class BasicTooltipDemo {
312342
message: string = initialTooltipMessage;
313343
showButton: boolean = true;
314344
}
345+
346+
@Component({
347+
selector: 'app',
348+
template: `
349+
<div cdk-scrollable style="margin-top: 300px; height: 200px; width: 200px; overflow: auto;">
350+
<button *ngIf="showButton" style="margin-bottom: 350px"
351+
[md-tooltip]="message"
352+
[tooltip-position]="position">
353+
Button
354+
</button>
355+
</div>`
356+
})
357+
class ScrollableTooltipDemo {
358+
position: string = 'below';
359+
message: string = initialTooltipMessage;
360+
showButton: boolean = true;
361+
362+
@ViewChild(Scrollable) scrollingContainer: Scrollable;
363+
364+
scrollDown() {
365+
const scrollingContainerEl = this.scrollingContainer.getElementRef().nativeElement;
366+
scrollingContainerEl.scrollTop = 50;
367+
368+
// Emit a scroll event from the scrolling element in our component.
369+
// This event should be picked up by the scrollable directive and notify.
370+
// The notification should be picked up by the service.
371+
const scrollEvent = document.createEvent('UIEvents');
372+
scrollEvent.initUIEvent('scroll', true, true, window, 0);
373+
scrollingContainerEl.dispatchEvent(scrollEvent);
374+
}
375+
}

src/lib/tooltip/tooltip.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {Subject} from 'rxjs/Subject';
3333
import {Dir} from '../core/rtl/dir';
3434
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
3535
import {OVERLAY_PROVIDERS} from '../core/overlay/overlay';
36+
import {ConnectedOverlayPositionChange} from '../core/overlay/position/connected-position';
3637
import 'rxjs/add/operator/first';
3738

3839
export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after';
@@ -179,11 +180,16 @@ export class MdTooltip implements OnInit, OnDestroy {
179180
let origin = this._getOrigin();
180181
let position = this._getOverlayPosition();
181182

182-
// Create connected strategy that listens for scroll events to reposition. After position
183-
// changes occur and the overlay is clipped then close the tooltip.
183+
// Create connected position strategy that listens for scroll events to reposition.
184+
// After position changes occur and the overlay is clipped by a parent scrollable then
185+
// close the tooltip.
184186
let strategy = this._overlay.position().connectedTo(this._elementRef, origin, position);
185187
strategy.withScrollableContainers(this._scrollDispatcher.getScrollContainers(this._elementRef));
186-
strategy.onPositionChange.subscribe(change => { if (change.isClipped) { this.hide(0); } });
188+
strategy.onPositionChange.subscribe((change: ConnectedOverlayPositionChange) => {
189+
if (change.scrollableViewProperties.isOverlayClipped) {
190+
this.hide(0);
191+
}
192+
});
187193

188194
let config = new OverlayState();
189195
config.positionStrategy = strategy;

0 commit comments

Comments
 (0)