Skip to content

Commit 3b48c0d

Browse files
crisbetommalerba
authored andcommitted
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.
1 parent 46c8097 commit 3b48c0d

File tree

4 files changed

+61
-61
lines changed

4 files changed

+61
-61
lines changed

src/cdk/overlay/position/connected-position-strategy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,9 @@ export class ConnectedPositionStrategy implements PositionStrategy {
199199
* on reposition we can evaluate if it or the overlay has been clipped or outside view. Every
200200
* Scrollable must be an ancestor element of the strategy's origin element.
201201
*/
202-
withScrollableContainers(scrollables: CdkScrollable[]) {
202+
withScrollableContainers(scrollables: CdkScrollable[]): this {
203203
this.scrollables = scrollables;
204+
return this;
204205
}
205206

206207
/**

src/demo-app/tooltip/tooltip-demo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ export class TooltipDemo {
2424
tooltips: string[] = [];
2525
disabled = false;
2626
showDelay = 0;
27-
hideDelay = 1000;
27+
hideDelay = 0;
2828
showExtraClass = false;
2929
}

src/lib/tooltip/tooltip.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ describe('MatTooltip', () => {
201201

202202
const overlayRef = tooltipDirective._overlayRef;
203203

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

315315
tooltipDirective.position = initialPosition;
316316
tooltipDirective.show();
317-
expect(tooltipDirective._tooltipInstance).toBeDefined();
317+
expect(tooltipDirective._tooltipInstance).toBeTruthy();
318318

319319
// Same position value should not remove the tooltip
320320
tooltipDirective.position = initialPosition;
321-
expect(tooltipDirective._tooltipInstance).toBeDefined();
321+
expect(tooltipDirective._tooltipInstance).toBeTruthy();
322322

323323
// Different position value should destroy the tooltip
324324
tooltipDirective.position = changedPosition;
325325
assertTooltipInstance(tooltipDirective, false);
326-
expect(tooltipDirective._overlayRef).toBeNull();
327326
});
328327

329328
it('should be able to modify the tooltip message', fakeAsync(() => {
@@ -541,6 +540,7 @@ describe('MatTooltip', () => {
541540
tick(0);
542541
fixture.detectChanges();
543542
tick(500);
543+
fixture.detectChanges();
544544

545545
expect(tooltipDirective._isTooltipVisible()).toBe(false);
546546
expect(overlayContainerElement.textContent).toBe('');

src/lib/tooltip/tooltip.ts

Lines changed: 54 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@ import {
1515
HorizontalConnectionPos,
1616
OriginConnectionPosition,
1717
Overlay,
18-
OverlayConfig,
18+
ScrollDispatcher,
1919
OverlayConnectionPosition,
2020
OverlayRef,
2121
RepositionScrollStrategy,
2222
ScrollStrategy,
2323
VerticalConnectionPos,
24+
ConnectedPositionStrategy,
2425
} from '@angular/cdk/overlay';
2526
import {Platform} from '@angular/cdk/platform';
2627
import {ComponentPortal} from '@angular/cdk/portal';
2728
import {take} from 'rxjs/operators/take';
28-
import {merge} from 'rxjs/observable/merge';
29-
import {ScrollDispatcher} from '@angular/cdk/scrolling';
29+
import {filter} from 'rxjs/operators/filter';
3030
import {
3131
ChangeDetectionStrategy,
3232
ChangeDetectorRef,
@@ -108,6 +108,7 @@ export class MatTooltip implements OnDestroy {
108108
_overlayRef: OverlayRef | null;
109109
_tooltipInstance: TooltipComponent | null;
110110

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

122-
// TODO(andrewjs): When the overlay's position can be dynamically changed, do not destroy
123-
// the tooltip.
124-
if (this._tooltipInstance) {
125-
this._disposeTooltip();
123+
if (this._overlayRef) {
124+
// TODO(andrewjs): When the overlay's position can be
125+
// dynamically changed, do not destroy the tooltip.
126+
this._detach();
127+
this._updatePosition();
126128
}
127129
}
128130
}
@@ -236,15 +238,15 @@ export class MatTooltip implements OnDestroy {
236238
* Dispose the tooltip when destroyed.
237239
*/
238240
ngOnDestroy() {
239-
if (this._tooltipInstance) {
240-
this._disposeTooltip();
241+
if (this._overlayRef) {
242+
this._overlayRef.dispose();
243+
this._tooltipInstance = null;
241244
}
242245

243246
// Clean up the event listeners set in the constructor
244247
if (!this._platform.IOS) {
245-
this._manualListeners.forEach((listener, event) => {
246-
this._elementRef.nativeElement.removeEventListener(event, listener);
247-
});
248+
this._manualListeners.forEach((listener, event) =>
249+
this._elementRef.nativeElement.removeEventListener(event, listener));
248250

249251
this._manualListeners.clear();
250252
}
@@ -257,10 +259,12 @@ export class MatTooltip implements OnDestroy {
257259
show(delay: number = this.showDelay): void {
258260
if (this.disabled || !this.message) { return; }
259261

260-
if (!this._tooltipInstance) {
261-
this._createTooltip();
262-
}
262+
const overlayRef = this._createOverlay();
263263

264+
this._detach();
265+
this._portal = this._portal || new ComponentPortal(TooltipComponent, this._viewContainerRef);
266+
this._tooltipInstance = overlayRef.attach(this._portal).instance;
267+
this._tooltipInstance.afterHidden().subscribe(() => this._detach());
264268
this._setTooltipClass(this._tooltipClass);
265269
this._updateTooltipMessage();
266270
this._tooltipInstance!.show(this._position, delay);
@@ -296,73 +300,68 @@ export class MatTooltip implements OnDestroy {
296300
this.hide(this._defaultOptions ? this._defaultOptions.touchendHideDelay : 1500);
297301
}
298302

299-
/** Create the tooltip to display */
300-
private _createTooltip(): void {
301-
const overlayRef = this._createOverlay();
302-
const portal = new ComponentPortal(TooltipComponent, this._viewContainerRef);
303-
304-
this._tooltipInstance = overlayRef.attach(portal).instance;
305-
306-
// Dispose of the tooltip when the overlay is detached.
307-
merge(this._tooltipInstance!.afterHidden(), overlayRef.detachments()).subscribe(() => {
308-
// Check first if the tooltip has already been removed through this components destroy.
309-
if (this._tooltipInstance) {
310-
this._disposeTooltip();
311-
}
312-
});
313-
}
314-
315303
/** Create the overlay config and position strategy */
316304
private _createOverlay(): OverlayRef {
305+
if (this._overlayRef) {
306+
return this._overlayRef;
307+
}
308+
317309
const origin = this._getOrigin();
318310
const overlay = this._getOverlayPosition();
319311

320312
// Create connected position strategy that listens for scroll events to reposition.
321313
const strategy = this._overlay
322314
.position()
323315
.connectedTo(this._elementRef, origin.main, overlay.main)
324-
.withFallbackPosition(origin.fallback, overlay.fallback);
325-
326-
const scrollableAncestors = this._scrollDispatcher
327-
.getAncestorScrollContainers(this._elementRef);
328-
329-
strategy.withScrollableContainers(scrollableAncestors);
330-
331-
strategy.onPositionChange.subscribe(change => {
332-
if (this._tooltipInstance) {
333-
if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance.isVisible()) {
334-
// After position changes occur and the overlay is clipped by
335-
// a parent scrollable then close the tooltip.
336-
this._ngZone.run(() => this.hide(0));
337-
} else {
338-
// Otherwise recalculate the origin based on the new position.
339-
this._tooltipInstance._setTransformOrigin(change.connectionPair);
340-
}
316+
.withFallbackPosition(origin.fallback, overlay.fallback)
317+
.withScrollableContainers(
318+
this._scrollDispatcher.getAncestorScrollContainers(this._elementRef)
319+
);
320+
321+
strategy.onPositionChange.pipe(filter(() => !!this._tooltipInstance)).subscribe(change => {
322+
if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance!.isVisible()) {
323+
// After position changes occur and the overlay is clipped by
324+
// a parent scrollable then close the tooltip.
325+
this._ngZone.run(() => this.hide(0));
326+
} else {
327+
// Otherwise recalculate the origin based on the new position.
328+
this._tooltipInstance!._setTransformOrigin(change.connectionPair);
341329
}
342330
});
343331

344-
const config = new OverlayConfig({
332+
this._overlayRef = this._overlay.create({
345333
direction: this._dir ? this._dir.value : 'ltr',
346334
positionStrategy: strategy,
347335
panelClass: TOOLTIP_PANEL_CLASS,
348336
scrollStrategy: this._scrollStrategy()
349337
});
350338

351-
this._overlayRef = this._overlay.create(config);
339+
this._overlayRef.detachments().subscribe(() => this._detach());
352340

353341
return this._overlayRef;
354342
}
355343

356-
/** Disposes the current tooltip and the overlay it is attached to */
357-
private _disposeTooltip(): void {
358-
if (this._overlayRef) {
359-
this._overlayRef.dispose();
360-
this._overlayRef = null;
344+
/** Detaches the currently-attached tooltip. */
345+
private _detach() {
346+
if (this._overlayRef && this._overlayRef.hasAttached()) {
347+
this._overlayRef.detach();
361348
}
362349

363350
this._tooltipInstance = null;
364351
}
365352

353+
/** Updates the position of the current tooltip. */
354+
private _updatePosition() {
355+
const position = this._overlayRef!.getConfig().positionStrategy as ConnectedPositionStrategy;
356+
const origin = this._getOrigin();
357+
const overlay = this._getOverlayPosition();
358+
359+
position
360+
.withPositions([])
361+
.withFallbackPosition(origin.main, overlay.main)
362+
.withFallbackPosition(origin.fallback, overlay.fallback);
363+
}
364+
366365
/**
367366
* Returns the origin position and a fallback position based on the user's position preference.
368367
* The fallback position is the inverse of the origin (e.g. `'below' -> 'above'`).

0 commit comments

Comments
 (0)