refactor(tooltip): reuse OverlayRef and portal (#9637)

* Reworks the tooltip to reuse the `OverlayRef` after the first time it is opened, rather than destroying it and creating a new one for every subsequent open. This reduces the overhead from having the create the new overlays all the time.
* Changes the signature of the `ConnectedPositionStrategy.withScrollableContainers` method to return the position strategy. This is primarily for consistency with the other methods.
This commit is contained in:
Kristiyan Kostadinov
2018-02-08 22:42:35 +01:00
committed by mmalerba
parent 46c809728d
commit 3b48c0d1c3
4 changed files with 60 additions and 60 deletions
@@ -199,8 +199,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;
}
/**
+1 -1
View File
@@ -24,6 +24,6 @@ export class TooltipDemo {
tooltips: string[] = [];
disabled = false;
showDelay = 0;
hideDelay = 1000;
hideDelay = 0;
showExtraClass = false;
}
+4 -4
View File
@@ -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.');
}));
@@ -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(() => {
@@ -541,6 +540,7 @@ describe('MatTooltip', () => {
tick(0);
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(tooltipDirective._isTooltipVisible()).toBe(false);
expect(overlayContainerElement.textContent).toBe('');
+53 -54
View File
@@ -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,
@@ -108,6 +108,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};
@@ -119,10 +120,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();
}
}
}
@@ -236,15 +238,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();
}
@@ -257,10 +259,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);
@@ -296,24 +300,12 @@ 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();
@@ -321,48 +313,55 @@ export class MatTooltip implements OnDestroy {
const strategy = this._overlay
.position()
.connectedTo(this._elementRef, origin.main, overlay.main)
.withFallbackPosition(origin.fallback, overlay.fallback);
.withFallbackPosition(origin.fallback, overlay.fallback)
.withScrollableContainers(
this._scrollDispatcher.getAncestorScrollContainers(this._elementRef)
);
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);
}
strategy.onPositionChange.pipe(filter(() => !!this._tooltipInstance)).subscribe(change => {
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'`).