fix(overlays): rework setup and teardown logic of OverlayMixin
Add a failing test chore: broken test fix chore: harden overlay teardown tests and cleanup select-rich
This commit is contained in:
parent
fef94cd016
commit
8377e8d17d
11 changed files with 371 additions and 36 deletions
|
|
@ -2,4 +2,4 @@
|
||||||
'@lion/ui': patch
|
'@lion/ui': patch
|
||||||
---
|
---
|
||||||
|
|
||||||
overlayController teardown cleanup (removes logs in test files)
|
[overlays]: overlayController teardown cleanup (removes logs in test files)
|
||||||
|
|
|
||||||
5
.changeset/tricky-suns-change.md
Normal file
5
.changeset/tricky-suns-change.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
[overlays]: allow Subclassers to teardown at the right moment
|
||||||
5
.changeset/tricky-suns-changerz.md
Normal file
5
.changeset/tricky-suns-changerz.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
[overlays]: rework setup and teardown logic of OverlayMixin (allow to reconnect offline dom nodes; allow moving dom nodes in performant way)
|
||||||
|
|
@ -229,6 +229,8 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`)
|
`)
|
||||||
);
|
);
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
const { _inputNode } = getComboboxMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
_inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
|
_inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
|
||||||
|
|
|
||||||
43
packages/ui/components/dialog/test-helpers/test-router.js
Normal file
43
packages/ui/components/dialog/test-helpers/test-router.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { html, LitElement } from 'lit';
|
||||||
|
|
||||||
|
class TestRouter extends LitElement {
|
||||||
|
static properties = {
|
||||||
|
routingMap: {
|
||||||
|
type: Object,
|
||||||
|
attribute: false,
|
||||||
|
},
|
||||||
|
path: { type: String },
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/** @type {{ [path: string]: HTMLElement }} */
|
||||||
|
this.routingMap = {};
|
||||||
|
/** @type {string} */
|
||||||
|
this.path = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<div id="selector">
|
||||||
|
${Object.keys(this.routingMap).map(
|
||||||
|
path =>
|
||||||
|
html`<button
|
||||||
|
id="path-${path}"
|
||||||
|
@click="${() => {
|
||||||
|
this.path = path;
|
||||||
|
}}"
|
||||||
|
>
|
||||||
|
${path}
|
||||||
|
</button>`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div id="view">${this.routingMap[this.path]}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('test-router', TestRouter);
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { expect, fixture as _fixture, html, unsafeStatic, aTimeout } from '@open-wc/testing';
|
import { expect, fixture as _fixture, html, unsafeStatic, aTimeout } from '@open-wc/testing';
|
||||||
import { runOverlayMixinSuite } from '../../overlays/test-suites/OverlayMixin.suite.js';
|
import { runOverlayMixinSuite } from '../../overlays/test-suites/OverlayMixin.suite.js';
|
||||||
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
||||||
|
import '../test-helpers/test-router.js';
|
||||||
import '@lion/ui/define/lion-dialog.js';
|
import '@lion/ui/define/lion-dialog.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -187,4 +188,57 @@ describe('lion-dialog', () => {
|
||||||
expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
|
expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
it('does not lose click event handler when was detached and reattached', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<test-router
|
||||||
|
.routingMap="${{
|
||||||
|
a: html`
|
||||||
|
<lion-dialog>
|
||||||
|
<div slot="content" class="dialog">Hey there</div>
|
||||||
|
<button slot="invoker">Popup button</button>
|
||||||
|
</lion-dialog>
|
||||||
|
`,
|
||||||
|
b: html` <div>B</div> `,
|
||||||
|
}}"
|
||||||
|
path="a"
|
||||||
|
></test-router>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const getDialog = () =>
|
||||||
|
/** @type {LionDialog} */ (el.shadowRoot?.querySelector('lion-dialog'));
|
||||||
|
const getInvoker = () =>
|
||||||
|
/** @type {HTMLElement} */ (getDialog().querySelector('[slot="invoker"]'));
|
||||||
|
|
||||||
|
getInvoker().click();
|
||||||
|
const dialog = getDialog();
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
await dialog._overlayCtrl._showComplete;
|
||||||
|
expect(dialog.opened).to.be.true;
|
||||||
|
|
||||||
|
dialog.close();
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
await dialog._overlayCtrl._hideComplete;
|
||||||
|
expect(dialog.opened).to.be.false;
|
||||||
|
|
||||||
|
const buttonA = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('#path-a'));
|
||||||
|
const buttonB = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('#path-b'));
|
||||||
|
|
||||||
|
buttonB.click();
|
||||||
|
await el.updateComplete;
|
||||||
|
buttonA.click();
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
getInvoker().click();
|
||||||
|
|
||||||
|
const dialogAfterRouteChange = getDialog();
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
expect(dialogAfterRouteChange._overlayCtrl).not.to.be.undefined;
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
await dialogAfterRouteChange._overlayCtrl._showComplete;
|
||||||
|
expect(dialogAfterRouteChange.opened).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1003,16 +1003,16 @@ export class OverlayController extends EventTarget {
|
||||||
* @param {{ phase: OverlayPhase }} config
|
* @param {{ phase: OverlayPhase }} config
|
||||||
*/
|
*/
|
||||||
_handleVisibilityTriggers({ phase }) {
|
_handleVisibilityTriggers({ phase }) {
|
||||||
if (typeof this.visibilityTriggerFunction !== 'function') return;
|
if (typeof this.visibilityTriggerFunction === 'function') {
|
||||||
|
if (phase === 'init') {
|
||||||
if (phase === 'init') {
|
this.__visibilityTriggerHandler = this.visibilityTriggerFunction({
|
||||||
this.__visibilityTriggerHandler = this.visibilityTriggerFunction({
|
phase,
|
||||||
phase,
|
controller: this,
|
||||||
controller: this,
|
});
|
||||||
});
|
}
|
||||||
}
|
if (this.__visibilityTriggerHandler[phase]) {
|
||||||
if (this.__visibilityTriggerHandler[phase]) {
|
this.__visibilityTriggerHandler[phase]();
|
||||||
this.__visibilityTriggerHandler[phase]();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1192,7 +1192,6 @@ export class OverlayController extends EventTarget {
|
||||||
event.composedPath().includes(this.contentNode) ||
|
event.composedPath().includes(this.contentNode) ||
|
||||||
deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
|
deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
|
||||||
if (hasPressedInside) return;
|
if (hasPressedInside) return;
|
||||||
|
|
||||||
this.hide();
|
this.hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ export const OverlayMixinImplementation = superclass =>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#hasSetup = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
/**
|
/**
|
||||||
|
|
@ -173,22 +175,23 @@ export const OverlayMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
connectedCallback() {
|
||||||
* @param {import('lit-element').PropertyValues } changedProperties
|
super.connectedCallback();
|
||||||
*/
|
|
||||||
firstUpdated(changedProperties) {
|
|
||||||
super.firstUpdated(changedProperties);
|
|
||||||
|
|
||||||
this._setupOverlayCtrl();
|
this.updateComplete.then(() => {
|
||||||
|
if (this.#hasSetup) return;
|
||||||
|
this._setupOverlayCtrl();
|
||||||
|
this.#hasSetup = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnectedCallback() {
|
async disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
|
||||||
if (!this._overlayCtrl) return;
|
if (await this._isPermanentlyDisconnected()) {
|
||||||
if (await this.#isMovingInDom()) return;
|
this._teardownOverlayCtrl();
|
||||||
|
this.#hasSetup = false;
|
||||||
this._teardownOverlayCtrl();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -238,6 +241,8 @@ export const OverlayMixinImplementation = superclass =>
|
||||||
|
|
||||||
/** @protected */
|
/** @protected */
|
||||||
_setupOverlayCtrl() {
|
_setupOverlayCtrl() {
|
||||||
|
if (this._overlayCtrl) return;
|
||||||
|
|
||||||
/** @type {OverlayController} */
|
/** @type {OverlayController} */
|
||||||
this._overlayCtrl = this._defineOverlay({
|
this._overlayCtrl = this._defineOverlay({
|
||||||
contentNode: this._overlayContentNode,
|
contentNode: this._overlayContentNode,
|
||||||
|
|
@ -253,11 +258,12 @@ export const OverlayMixinImplementation = superclass =>
|
||||||
|
|
||||||
/** @protected */
|
/** @protected */
|
||||||
_teardownOverlayCtrl() {
|
_teardownOverlayCtrl() {
|
||||||
|
if (!this._overlayCtrl) return;
|
||||||
|
|
||||||
this._teardownOpenCloseListeners();
|
this._teardownOpenCloseListeners();
|
||||||
this.__teardownSyncFromOverlayController();
|
this.__teardownSyncFromOverlayController();
|
||||||
|
|
||||||
/** @type {OverlayController} */
|
/** @type {OverlayController} */ (this._overlayCtrl).teardown();
|
||||||
(this._overlayCtrl).teardown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -396,9 +402,9 @@ export const OverlayMixinImplementation = superclass =>
|
||||||
* Before we decide to teardown, let's wait to see if we were not just moving nodes around.
|
* Before we decide to teardown, let's wait to see if we were not just moving nodes around.
|
||||||
* @return {Promise<boolean>}
|
* @return {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async #isMovingInDom() {
|
async _isPermanentlyDisconnected() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
return this.isConnected;
|
return !this.isConnected;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const OverlayMixin = dedupeMixin(OverlayMixinImplementation);
|
export const OverlayMixin = dedupeMixin(OverlayMixinImplementation);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
import { expect, fixture, html, nextFrame, aTimeout } from '@open-wc/testing';
|
import {
|
||||||
|
unsafeStatic,
|
||||||
import sinon from 'sinon';
|
nextFrame,
|
||||||
|
aTimeout,
|
||||||
|
defineCE,
|
||||||
|
fixture,
|
||||||
|
expect,
|
||||||
|
html,
|
||||||
|
} from '@open-wc/testing';
|
||||||
import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js';
|
import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js';
|
||||||
|
|
||||||
import '@lion/ui/define/lion-dialog.js';
|
|
||||||
import { browserDetection } from '@lion/ui/core.js';
|
import { browserDetection } from '@lion/ui/core.js';
|
||||||
|
import { cache } from 'lit/directives/cache.js';
|
||||||
|
import '@lion/ui/define/lion-dialog.js';
|
||||||
|
import { LitElement } from 'lit';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
|
|
||||||
* @typedef {import('../types/OverlayMixinTypes.js').DefineOverlayConfig} DefineOverlayConfig
|
* @typedef {import('../types/OverlayMixinTypes.js').DefineOverlayConfig} DefineOverlayConfig
|
||||||
* @typedef {import('../types/OverlayMixinTypes.js').OverlayHost} OverlayHost
|
|
||||||
* @typedef {import('../types/OverlayMixinTypes.js').OverlayMixin} OverlayMixin
|
|
||||||
* @typedef {import('lit').LitElement} LitElement
|
|
||||||
* @typedef {LitElement & OverlayHost & {_overlayCtrl:OverlayController}} OverlayEl
|
* @typedef {LitElement & OverlayHost & {_overlayCtrl:OverlayController}} OverlayEl
|
||||||
|
* @typedef {import('../types/OverlayMixinTypes.js').OverlayMixin} OverlayMixin
|
||||||
|
* @typedef {import('../types/OverlayMixinTypes.js').OverlayHost} OverlayHost
|
||||||
|
* @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function getGlobalOverlayCtrls() {
|
function getGlobalOverlayCtrls() {
|
||||||
|
|
@ -331,6 +338,173 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
||||||
}).to.not.throw;
|
}).to.not.throw;
|
||||||
stub.restore();
|
stub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Teardown', () => {
|
||||||
|
it('does not teardown when moving dom nodes', async () => {
|
||||||
|
const el = /** @type {OverlayEl} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${tag}>
|
||||||
|
<div slot="content">content</div>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
</${tag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
// @ts-expect-error [allow-protected] in tests
|
||||||
|
const teardownSpy = sinon.spy(el, '_teardownOverlayCtrl');
|
||||||
|
// @ts-expect-error [allow-protected] in tests
|
||||||
|
const spyDisconnected = sinon.spy(el, '_isPermanentlyDisconnected');
|
||||||
|
|
||||||
|
// move around in dom
|
||||||
|
const newParent = document.createElement('div');
|
||||||
|
document.body.appendChild(newParent);
|
||||||
|
newParent.appendChild(el);
|
||||||
|
expect(spyDisconnected.callCount).to.equal(1);
|
||||||
|
|
||||||
|
const otherParent = document.createElement('div');
|
||||||
|
document.body.appendChild(otherParent);
|
||||||
|
otherParent.appendChild(el);
|
||||||
|
await aTimeout(0);
|
||||||
|
|
||||||
|
expect(spyDisconnected.callCount).to.equal(2);
|
||||||
|
expect(teardownSpy.callCount).to.equal(0);
|
||||||
|
|
||||||
|
// simulate a permanent disconnect
|
||||||
|
otherParent.removeChild(el);
|
||||||
|
await aTimeout(0);
|
||||||
|
|
||||||
|
expect(teardownSpy.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows to disconnect and reconnect later', async () => {
|
||||||
|
const el = /** @type {OverlayEl} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${tag}>
|
||||||
|
<div slot="content">content</div>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
</${tag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
// @ts-expect-error [allow-protected] in tests
|
||||||
|
const teardownSpy = sinon.spy(el, '_teardownOverlayCtrl');
|
||||||
|
// @ts-expect-error [allow-protected] in tests
|
||||||
|
const spyDisconnected = sinon.spy(el, '_isPermanentlyDisconnected');
|
||||||
|
// @ts-expect-error [allow-protected] in tests
|
||||||
|
const setupSpy = sinon.spy(el, '_setupOverlayCtrl');
|
||||||
|
|
||||||
|
// 'permanently' disconnect (although we will reconnect later)
|
||||||
|
const offlineParent = document.createElement('div');
|
||||||
|
offlineParent.appendChild(el);
|
||||||
|
await aTimeout(0);
|
||||||
|
|
||||||
|
expect(spyDisconnected.callCount).to.equal(1);
|
||||||
|
expect(teardownSpy.callCount).to.equal(1);
|
||||||
|
|
||||||
|
// reconnect
|
||||||
|
document.body.appendChild(offlineParent);
|
||||||
|
await aTimeout(0);
|
||||||
|
|
||||||
|
expect(setupSpy.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with cache directive when reconnected', async () => {
|
||||||
|
class CachingContext extends LitElement {
|
||||||
|
static properties = {
|
||||||
|
switched: { type: Boolean },
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.switched = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get overlayEl() {
|
||||||
|
return this.shadowRoot?.querySelector('#myOverlay');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`${cache(
|
||||||
|
this.switched
|
||||||
|
? html`something else`
|
||||||
|
: html`<${tag} id="myOverlay">
|
||||||
|
<div slot="content">content b</div>
|
||||||
|
<button slot="invoker">invoker button b</button>
|
||||||
|
</${tag}>`,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachingTagName = defineCE(CachingContext);
|
||||||
|
const cachingTag = unsafeStatic(cachingTagName);
|
||||||
|
|
||||||
|
const el = /** @type {CachingContext} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${cachingTag}></${cachingTag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-expect-error [allow-protected] in tests
|
||||||
|
const teardownSpy = sinon.spy(el.overlayEl, '_teardownOverlayCtrl');
|
||||||
|
// @ts-expect-error [allow-protected] in tests
|
||||||
|
const spyDisconnected = sinon.spy(el.overlayEl, '_isPermanentlyDisconnected');
|
||||||
|
// @ts-expect-error [allow-protected] in tests
|
||||||
|
const setupSpy = sinon.spy(el.overlayEl, '_setupOverlayCtrl');
|
||||||
|
|
||||||
|
el.switched = true;
|
||||||
|
// render the new content
|
||||||
|
await el.updateComplete;
|
||||||
|
// wait for the teardown to complete
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(spyDisconnected.callCount).to.equal(1);
|
||||||
|
expect(teardownSpy.callCount).to.equal(1);
|
||||||
|
expect(setupSpy.callCount).to.equal(0);
|
||||||
|
|
||||||
|
el.switched = false;
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(setupSpy.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly removes event listeners when disconnected from dom', async () => {
|
||||||
|
const el = /** @type {OverlayEl} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${tag}>
|
||||||
|
<div slot="content">content</div>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
</${tag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventRemoveSpy = sinon.spy(el._overlayCtrl, 'removeEventListener');
|
||||||
|
el.parentNode?.removeChild(el);
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(eventRemoveSpy.callCount).to.equal(0);
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
const hasOneInstanceFor = (
|
||||||
|
/** @type {string} */ eventName,
|
||||||
|
/** @type {any[][]} */ eventsToSearch,
|
||||||
|
/** @type {string} */ methodToRemove,
|
||||||
|
) =>
|
||||||
|
Boolean(
|
||||||
|
eventsToSearch.filter(
|
||||||
|
([eventToSearch, methodToSearch]) =>
|
||||||
|
eventToSearch === eventName && methodToSearch === methodToRemove,
|
||||||
|
).length,
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-expect-error [allow-private] in tests
|
||||||
|
expect(hasOneInstanceFor('show', eventRemoveSpy.args, el.__onOverlayCtrlShow)).to.be.true;
|
||||||
|
// @ts-expect-error [allow-private] in tests
|
||||||
|
expect(hasOneInstanceFor('hide', eventRemoveSpy.args, el.__onOverlayCtrlHide)).to.be.true;
|
||||||
|
// @ts-expect-error [allow-private] in tests
|
||||||
|
expect(hasOneInstanceFor('before-show', eventRemoveSpy.args, el.__onBeforeShow)).to.be.true;
|
||||||
|
// @ts-expect-error [allow-private] in tests
|
||||||
|
expect(hasOneInstanceFor('before-hide', eventRemoveSpy.args, el.__onBeforeHide)).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`OverlayMixin${suffix} nested`, () => {
|
describe(`OverlayMixin${suffix} nested`, () => {
|
||||||
|
|
@ -388,6 +562,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
||||||
const moveTarget = /** @type {OverlayEl} */ (await fixture('<div id="target"></div>'));
|
const moveTarget = /** @type {OverlayEl} */ (await fixture('<div id="target"></div>'));
|
||||||
moveTarget.appendChild(el);
|
moveTarget.appendChild(el);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(getGlobalOverlayCtrls().length).to.equal(1);
|
expect(getGlobalOverlayCtrls().length).to.equal(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -423,6 +423,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
*/
|
*/
|
||||||
_teardownOverlayCtrl() {
|
_teardownOverlayCtrl() {
|
||||||
super._teardownOverlayCtrl();
|
super._teardownOverlayCtrl();
|
||||||
|
|
||||||
this._overlayCtrl.removeEventListener('show', this.__overlayOnShow);
|
this._overlayCtrl.removeEventListener('show', this.__overlayOnShow);
|
||||||
this._overlayCtrl.removeEventListener('before-show', this.__overlayBeforeShow);
|
this._overlayCtrl.removeEventListener('before-show', this.__overlayBeforeShow);
|
||||||
this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide);
|
this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide);
|
||||||
|
|
@ -434,11 +435,12 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
async _alignInvokerWidth() {
|
async _alignInvokerWidth() {
|
||||||
|
await this.updateComplete;
|
||||||
|
|
||||||
if (!this._overlayCtrl?.content) {
|
if (!this._overlayCtrl?.content) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updateComplete;
|
|
||||||
const initContentDisplay = this._overlayCtrl.content.style.display;
|
const initContentDisplay = this._overlayCtrl.content.style.display;
|
||||||
const initContentMinWidth = this._overlayCtrl.contentWrapperNode.style.minWidth;
|
const initContentMinWidth = this._overlayCtrl.contentWrapperNode.style.minWidth;
|
||||||
const initContentWidth = this._overlayCtrl.contentWrapperNode.style.width;
|
const initContentWidth = this._overlayCtrl.contentWrapperNode.style.width;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import '@lion/ui/define/lion-select-rich.js';
|
||||||
import '@lion/ui/define/lion-listbox.js';
|
import '@lion/ui/define/lion-listbox.js';
|
||||||
import '@lion/ui/define/lion-option.js';
|
import '@lion/ui/define/lion-option.js';
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
|
import sinon from 'sinon';
|
||||||
import {
|
import {
|
||||||
fixture as _fixture,
|
fixture as _fixture,
|
||||||
unsafeStatic,
|
unsafeStatic,
|
||||||
|
|
@ -711,6 +712,49 @@ describe('lion-select-rich', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Teardown', () => {
|
||||||
|
it('correctly removes event listeners when disconnected from dom', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich label="age">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const { _overlayCtrl } = getSelectRichMembers(el);
|
||||||
|
const eventRemoveSpy = sinon.spy(_overlayCtrl, 'removeEventListener');
|
||||||
|
|
||||||
|
el.parentNode?.removeChild(el);
|
||||||
|
|
||||||
|
expect(eventRemoveSpy.callCount).to.equal(0);
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(eventRemoveSpy.callCount).to.equal(0);
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
const hasOneInstanceFor = (
|
||||||
|
/** @type {string} */ eventName,
|
||||||
|
/** @type {any[][]} */ eventsToSearch,
|
||||||
|
/** @type {string} */ methodToRemove,
|
||||||
|
) =>
|
||||||
|
Boolean(
|
||||||
|
eventsToSearch.filter(
|
||||||
|
([eventToSearch, methodToSearch]) =>
|
||||||
|
eventToSearch === eventName && methodToSearch === methodToRemove,
|
||||||
|
).length,
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-expect-error [allow-private] in tests
|
||||||
|
expect(hasOneInstanceFor('show', eventRemoveSpy.args, el.__overlayOnShow)).to.be.true;
|
||||||
|
// @ts-expect-error [allow-private] in tests
|
||||||
|
expect(hasOneInstanceFor('before-show', eventRemoveSpy.args, el.__overlayBeforeShow)).to.be
|
||||||
|
.true;
|
||||||
|
// @ts-expect-error [allow-private] in tests
|
||||||
|
expect(hasOneInstanceFor('hide', eventRemoveSpy.args, el.__overlayOnHide)).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Subclassers', () => {
|
describe('Subclassers', () => {
|
||||||
it('allows to override the type of overlay', async () => {
|
it('allows to override the type of overlay', async () => {
|
||||||
const mySelectTagString = defineCE(
|
const mySelectTagString = defineCE(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue