Skip to content

refactor(tooltip): reuse OverlayRef and portal #9637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/cdk/overlay/position/connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,9 @@ export class ConnectedPositionStrategy implements PositionStrategy {
* 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: CdkScrollable[]) {
withScrollableContainers(scrollables: CdkScrollable[]): this {
this.scrollables = scrollables;
return this;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/demo-app/tooltip/tooltip-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ export class TooltipDemo {
tooltips: string[] = [];
disabled = false;
showDelay = 0;
hideDelay = 1000;
hideDelay = 0;
showExtraClass = false;
}
8 changes: 4 additions & 4 deletions src/lib/tooltip/tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ describe('MatTooltip', () => {

const overlayRef = tooltipDirective._overlayRef;

expect(overlayRef).not.toBeNull();
expect(!!overlayRef).toBeTruthy();
expect(overlayRef!.overlayElement.classList).toContain(TOOLTIP_PANEL_CLASS,
'Expected the overlay panel element to have the tooltip panel class set.');
}));
Expand Down Expand Up @@ -314,16 +314,15 @@ describe('MatTooltip', () => {

tooltipDirective.position = initialPosition;
tooltipDirective.show();
expect(tooltipDirective._tooltipInstance).toBeDefined();
expect(tooltipDirective._tooltipInstance).toBeTruthy();

// Same position value should not remove the tooltip
tooltipDirective.position = initialPosition;
expect(tooltipDirective._tooltipInstance).toBeDefined();
expect(tooltipDirective._tooltipInstance).toBeTruthy();

// Different position value should destroy the tooltip
tooltipDirective.position = changedPosition;
assertTooltipInstance(tooltipDirective, false);
expect(tooltipDirective._overlayRef).toBeNull();
});

it('should be able to modify the tooltip message', fakeAsync(() => {
Expand Down Expand Up @@ -541,6 +540,7 @@ describe('MatTooltip', () => {
tick(0);
fixture.detectChanges();
tick(500);
fixture.detectChanges();

expect(tooltipDirective._isTooltipVisible()).toBe(false);
expect(overlayContainerElement.textContent).toBe('');
Expand Down
109 changes: 54 additions & 55 deletions src/lib/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ import {
HorizontalConnectionPos,
OriginConnectionPosition,
Overlay,
OverlayConfig,
ScrollDispatcher,
OverlayConnectionPosition,
OverlayRef,
RepositionScrollStrategy,
ScrollStrategy,
VerticalConnectionPos,
ConnectedPositionStrategy,
} from '@angular/cdk/overlay';
import {Platform} from '@angular/cdk/platform';
import {ComponentPortal} from '@angular/cdk/portal';
import {take} from 'rxjs/operators/take';
import {merge} from 'rxjs/observable/merge';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {filter} from 'rxjs/operators/filter';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Expand Down Expand Up @@ -107,6 +107,7 @@ export class MatTooltip implements OnDestroy {
_overlayRef: OverlayRef | null;
_tooltipInstance: TooltipComponent | null;

private _portal: ComponentPortal<TooltipComponent>;
private _position: TooltipPosition = 'below';
private _disabled: boolean = false;
private _tooltipClass: string|string[]|Set<string>|{[key: string]: any};
Expand All @@ -118,10 +119,11 @@ export class MatTooltip implements OnDestroy {
if (value !== this._position) {
this._position = value;

// TODO(andrewjs): When the overlay's position can be dynamically changed, do not destroy
// the tooltip.
if (this._tooltipInstance) {
this._disposeTooltip();
if (this._overlayRef) {
// TODO(andrewjs): When the overlay's position can be
// dynamically changed, do not destroy the tooltip.
this._detach();
this._updatePosition();
}
}
}
Expand Down Expand Up @@ -235,15 +237,15 @@ export class MatTooltip implements OnDestroy {
* Dispose the tooltip when destroyed.
*/
ngOnDestroy() {
if (this._tooltipInstance) {
this._disposeTooltip();
if (this._overlayRef) {
this._overlayRef.dispose();
this._tooltipInstance = null;
}

// Clean up the event listeners set in the constructor
if (!this._platform.IOS) {
this._manualListeners.forEach((listener, event) => {
this._elementRef.nativeElement.removeEventListener(event, listener);
});
this._manualListeners.forEach((listener, event) =>
this._elementRef.nativeElement.removeEventListener(event, listener));

this._manualListeners.clear();
}
Expand All @@ -256,10 +258,12 @@ export class MatTooltip implements OnDestroy {
show(delay: number = this.showDelay): void {
if (this.disabled || !this.message) { return; }

if (!this._tooltipInstance) {
this._createTooltip();
}
const overlayRef = this._createOverlay();

this._detach();
this._portal = this._portal || new ComponentPortal(TooltipComponent, this._viewContainerRef);
this._tooltipInstance = overlayRef.attach(this._portal).instance;
this._tooltipInstance.afterHidden().subscribe(() => this._detach());
this._setTooltipClass(this._tooltipClass);
this._updateTooltipMessage();
this._tooltipInstance!.show(this._position, delay);
Expand Down Expand Up @@ -295,73 +299,68 @@ export class MatTooltip implements OnDestroy {
this.hide(this._defaultOptions ? this._defaultOptions.touchendHideDelay : 1500);
}

/** Create the tooltip to display */
private _createTooltip(): void {
const overlayRef = this._createOverlay();
const portal = new ComponentPortal(TooltipComponent, this._viewContainerRef);

this._tooltipInstance = overlayRef.attach(portal).instance;

// Dispose of the tooltip when the overlay is detached.
merge(this._tooltipInstance!.afterHidden(), overlayRef.detachments()).subscribe(() => {
// Check first if the tooltip has already been removed through this components destroy.
if (this._tooltipInstance) {
this._disposeTooltip();
}
});
}

/** Create the overlay config and position strategy */
private _createOverlay(): OverlayRef {
if (this._overlayRef) {
return this._overlayRef;
}

const origin = this._getOrigin();
const overlay = this._getOverlayPosition();

// Create connected position strategy that listens for scroll events to reposition.
const strategy = this._overlay
.position()
.connectedTo(this._elementRef, origin.main, overlay.main)
.withFallbackPosition(origin.fallback, overlay.fallback);

const scrollableAncestors = this._scrollDispatcher
.getAncestorScrollContainers(this._elementRef);

strategy.withScrollableContainers(scrollableAncestors);

strategy.onPositionChange.subscribe(change => {
if (this._tooltipInstance) {
if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance.isVisible()) {
// After position changes occur and the overlay is clipped by
// a parent scrollable then close the tooltip.
this._ngZone.run(() => this.hide(0));
} else {
// Otherwise recalculate the origin based on the new position.
this._tooltipInstance._setTransformOrigin(change.connectionPair);
}
.withFallbackPosition(origin.fallback, overlay.fallback)
.withScrollableContainers(
this._scrollDispatcher.getAncestorScrollContainers(this._elementRef)
);

strategy.onPositionChange.pipe(filter(() => !!this._tooltipInstance)).subscribe(change => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!! shouldn't be necessary here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is because filter's predicate function is a (value: T, index: number) => boolean.

if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance!.isVisible()) {
// After position changes occur and the overlay is clipped by
// a parent scrollable then close the tooltip.
this._ngZone.run(() => this.hide(0));
} else {
// Otherwise recalculate the origin based on the new position.
this._tooltipInstance!._setTransformOrigin(change.connectionPair);
}
});

const config = new OverlayConfig({
this._overlayRef = this._overlay.create({
direction: this._dir ? this._dir.value : 'ltr',
positionStrategy: strategy,
panelClass: TOOLTIP_PANEL_CLASS,
scrollStrategy: this._scrollStrategy()
});

this._overlayRef = this._overlay.create(config);
this._overlayRef.detachments().subscribe(() => this._detach());

return this._overlayRef;
}

/** Disposes the current tooltip and the overlay it is attached to */
private _disposeTooltip(): void {
if (this._overlayRef) {
this._overlayRef.dispose();
this._overlayRef = null;
/** Detaches the currently-attached tooltip. */
private _detach() {
if (this._overlayRef && this._overlayRef.hasAttached()) {
this._overlayRef.detach();
}

this._tooltipInstance = null;
}

/** Updates the position of the current tooltip. */
private _updatePosition() {
const position = this._overlayRef!.getConfig().positionStrategy as ConnectedPositionStrategy;
const origin = this._getOrigin();
const overlay = this._getOverlayPosition();

position
.withPositions([])
.withFallbackPosition(origin.main, overlay.main)
.withFallbackPosition(origin.fallback, overlay.fallback);
}

/**
* Returns the origin position and a fallback position based on the user's position preference.
* The fallback position is the inverse of the origin (e.g. `'below' -> 'above'`).
Expand Down