fix(overlays): fix memory leaks of adopt-styles and OverlayMixin

* fix(overlays): fix memory leaks of adopt-styles and OverlayMixin

* fix(overlays): support reconnecting the OverlayController to the OverlayManager

* chore: move unregister logic to OverlayController

---------

Co-authored-by: Thijs Louisse <Thijs.Louisse@ing.com>
This commit is contained in:
Krisztian Horvath 2024-11-12 17:25:37 +01:00 committed by GitHub
parent 2a989f47cf
commit 86ca2e072e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 57 additions and 10 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ui': patch
---
[overlays] Fixed memory leak caused adopted style cache and the `OverlayMixin` not releasing the `OverlayController` on teardown

View file

@ -192,7 +192,6 @@ export class OverlayController extends EventTarget {
zIndex: 9999, zIndex: 9999,
}; };
this.manager.add(this);
/** @protected */ /** @protected */
this._contentId = `overlay-content--${Math.random().toString(36).slice(2, 10)}`; this._contentId = `overlay-content--${Math.random().toString(36).slice(2, 10)}`;
/** @private */ /** @private */
@ -475,6 +474,14 @@ export class OverlayController extends EventTarget {
this._init(); this._init();
/** @private */ /** @private */
this.__elementToFocusAfterHide = undefined; this.__elementToFocusAfterHide = undefined;
if (!this.#isRegisteredOnManager()) {
this.manager.add(this);
}
}
#isRegisteredOnManager() {
return Boolean(this.manager.list.find(ctrl => this === ctrl));
} }
/** /**
@ -1354,6 +1361,10 @@ export class OverlayController extends EventTarget {
this.__handleOverlayStyles({ phase: 'teardown' }); this.__handleOverlayStyles({ phase: 'teardown' });
this._handleFeatures({ phase: 'teardown' }); this._handleFeatures({ phase: 'teardown' });
this.__wrappingDialogNode?.removeEventListener('cancel', this.__cancelHandler); this.__wrappingDialogNode?.removeEventListener('cancel', this.__cancelHandler);
if (this.#isRegisteredOnManager()) {
this.manager.remove(this);
}
} }
/** @private */ /** @private */

View file

@ -182,11 +182,13 @@ export const OverlayMixinImplementation = superclass =>
this._setupOverlayCtrl(); this._setupOverlayCtrl();
} }
disconnectedCallback() { async disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
if (this._overlayCtrl) {
this._teardownOverlayCtrl(); if (!this._overlayCtrl) return;
} if (await this.#isMovingInDom()) return;
this._teardownOverlayCtrl();
} }
/** /**
@ -253,6 +255,7 @@ export const OverlayMixinImplementation = superclass =>
_teardownOverlayCtrl() { _teardownOverlayCtrl() {
this._teardownOpenCloseListeners(); this._teardownOpenCloseListeners();
this.__teardownSyncFromOverlayController(); this.__teardownSyncFromOverlayController();
/** @type {OverlayController} */ /** @type {OverlayController} */
(this._overlayCtrl).teardown(); (this._overlayCtrl).teardown();
} }
@ -387,5 +390,15 @@ export const OverlayMixinImplementation = superclass =>
ctrl._popper.update(); ctrl._popper.update();
} }
} }
/**
* When we're moving around in dom, disconnectedCallback gets called.
* Before we decide to teardown, let's wait to see if we were not just moving nodes around.
* @return {Promise<boolean>}
*/
async #isMovingInDom() {
await this.updateComplete;
return this.isConnected;
}
}; };
export const OverlayMixin = dedupeMixin(OverlayMixinImplementation); export const OverlayMixin = dedupeMixin(OverlayMixinImplementation);

View file

@ -25,7 +25,7 @@ export const _adoptStyleUtils = {
adoptStyles: undefined, adoptStyles: undefined,
}; };
const styleCache = new Map(); const styleCache = new WeakMap();
/** /**
* @param {CSSStyleSheet} cssStyleSheet * @param {CSSStyleSheet} cssStyleSheet
@ -81,10 +81,10 @@ function adoptStyleWhenAdoptedStylesheetsNotSupported(
*/ */
function handleCache(renderRoot, style, { teardown = false } = {}) { function handleCache(renderRoot, style, { teardown = false } = {}) {
let haltFurtherExecution = false; let haltFurtherExecution = false;
if (!styleCache.has(renderRoot)) { if (renderRoot && !styleCache.has(renderRoot)) {
styleCache.set(renderRoot, []); styleCache.set(renderRoot, []);
} }
const addedStylesForRoot = styleCache.get(renderRoot); const addedStylesForRoot = styleCache.get(renderRoot) ?? [];
const foundStyle = addedStylesForRoot.find( const foundStyle = addedStylesForRoot.find(
(/** @type {import("lit").CSSResultOrNative} */ addedStyle) => style === addedStyle, (/** @type {import("lit").CSSResultOrNative} */ addedStyle) => style === addedStyle,
); );

View file

@ -36,6 +36,14 @@ async function mimicEscapePress(element) {
await aTimeout(0); await aTimeout(0);
} }
/**
* @param {OverlayController} ctrlToFind
* @returns {boolean}
*/
function isRegisteredOnManager(ctrlToFind) {
return Boolean(ctrlToFind.manager?.list?.find(ctrl => ctrlToFind === ctrl));
}
/** /**
* Make sure that all browsers serialize html in a similar way * Make sure that all browsers serialize html in a similar way
* (Firefox tends to output empty style attrs) * (Firefox tends to output empty style attrs)
@ -275,8 +283,18 @@ describe('OverlayController', () => {
}); });
}); });
// TODO: Add teardown feature tests // TODO: Add more teardown feature tests
describe('Teardown', () => {}); describe('Teardown', () => {
it('unregisters itself from overlayManager', async () => {
const ctrl = new OverlayController(withGlobalTestConfig());
expect(isRegisteredOnManager(ctrl)).to.be.true;
ctrl.teardown();
expect(isRegisteredOnManager(ctrl)).to.be.false;
});
});
describe('Node Configuration', () => { describe('Node Configuration', () => {
describe('Content', async () => { describe('Content', async () => {