Skip to content

Commit a81faea

Browse files
committed
add tooltip test
1 parent 46f5ec1 commit a81faea

File tree

4 files changed

+135
-25
lines changed

4 files changed

+135
-25
lines changed

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

Lines changed: 53 additions & 17 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. */
@@ -102,8 +102,12 @@ export class ConnectedPositionStrategy implements PositionStrategy {
102102
// If the overlay in the calculated position fits on-screen, put it there and we're done.
103103
if (overlayPoint.fitsInViewport) {
104104
this._setElementPosition(element, overlayPoint);
105-
const isClipped = this.isOverlayElementClipped(element);
106-
this._onPositionChange.next(new ConnectedOverlayPositionChange(pos, isClipped));
105+
106+
// Notify that the position has been changed along with its change properties.
107+
const scrollableViewProperties = this.getScrollableViewProperties(element);
108+
const positionChange = new ConnectedOverlayPositionChange(pos, scrollableViewProperties);
109+
this._onPositionChange.next(positionChange);
110+
107111
return Promise.resolve(null);
108112
} else if (!fallbackPoint || fallbackPoint.visibleArea < overlayPoint.visibleArea) {
109113
fallbackPoint = overlayPoint;
@@ -243,16 +247,48 @@ export class ConnectedPositionStrategy implements PositionStrategy {
243247
return {x, y, fitsInViewport, visibleArea};
244248
}
245249

246-
/** Whether the overlay element is clipped out of view of one of the scrollable containers. */
247-
private isOverlayElementClipped(element: HTMLElement): boolean {
248-
const elementBounds = this._getElementBounds(element);
249-
return this.scrollables.some((scrollable: Scrollable) => {
250-
const scrollingContainerBounds = this._getElementBounds(scrollable.getElementRef().nativeElement);
250+
/**
251+
* Gets the view properties of the trigger and overlay, including whether they are clipped
252+
* or completely outside the view of any of the strategy's scrollables.
253+
*/
254+
private getScrollableViewProperties(overlay: HTMLElement): ScrollableViewProperties {
255+
const triggerBounds = this._getElementBounds(this._connectedTo.nativeElement);
256+
const overlayBounds = this._getElementBounds(overlay);
257+
const scrollContainerBounds = this.scrollables.map((scrollable: Scrollable) => {
258+
return this._getElementBounds(scrollable.getElementRef().nativeElement);
259+
});
260+
261+
return {
262+
isTriggerClipped: this.isElementClipped(triggerBounds, scrollContainerBounds),
263+
isTriggerOutsideView: this.isElementOutsideView(triggerBounds, scrollContainerBounds),
264+
isOverlayClipped: this.isElementClipped(overlayBounds, scrollContainerBounds),
265+
isOverlayOutsideView: this.isElementOutsideView(overlayBounds, scrollContainerBounds),
266+
};
267+
}
268+
269+
/** Whether the element is completely out of the view of any of the containers. */
270+
private isElementOutsideView(
271+
elementBounds: ElementBoundingPositions,
272+
containersBounds: ElementBoundingPositions[]): boolean {
273+
return containersBounds.some((containerBounds: ElementBoundingPositions) => {
274+
const outsideAbove = elementBounds.bottom < containerBounds.top;
275+
const outsideBelow = elementBounds.top > containerBounds.bottom;
276+
const outsideLeft = elementBounds.right < containerBounds.left;
277+
const outsideRight = elementBounds.left > containerBounds.right;
278+
279+
return outsideAbove || outsideBelow || outsideLeft || outsideRight;
280+
});
281+
}
251282

252-
const clippedAbove = elementBounds.top < scrollingContainerBounds.top;
253-
const clippedBelow = elementBounds.bottom > scrollingContainerBounds.bottom;
254-
const clippedLeft = elementBounds.left < scrollingContainerBounds.left;
255-
const clippedRight = elementBounds.right > scrollingContainerBounds.right;
283+
/** Whether the element is clipped by any of the containers. */
284+
private isElementClipped(
285+
elementBounds: ElementBoundingPositions,
286+
containersBounds: ElementBoundingPositions[]): boolean {
287+
return containersBounds.some((containerBounds: ElementBoundingPositions) => {
288+
const clippedAbove = elementBounds.top < containerBounds.top;
289+
const clippedBelow = elementBounds.bottom > containerBounds.bottom;
290+
const clippedLeft = elementBounds.left < containerBounds.left;
291+
const clippedRight = elementBounds.right > containerBounds.right;
256292

257293
return clippedAbove || clippedBelow || clippedLeft || clippedRight;
258294
});

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 & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {Dir} from '../core/rtl/dir';
3434
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
3535
import {OverlayPositionBuilder} from '../core/overlay/position/overlay-position-builder';
3636
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
37-
import {Scrollable} from '../core/overlay/scroll/scrollable';
37+
import {ConnectedOverlayPositionChange} from '../core/overlay/position/connected-position';
3838
import 'rxjs/add/operator/first';
3939

4040
export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after';
@@ -181,11 +181,16 @@ export class MdTooltip implements OnInit, OnDestroy {
181181
let origin = this._getOrigin();
182182
let position = this._getOverlayPosition();
183183

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

190195
let config = new OverlayState();
191196
config.positionStrategy = strategy;

0 commit comments

Comments
 (0)