Skip to content

Commit da22fdb

Browse files
authored
fix: allow the popup dialog to close when it is setup by lit cache (#2563)
1 parent 9ac3fa4 commit da22fdb

File tree

5 files changed

+137
-7
lines changed

5 files changed

+137
-7
lines changed

.changeset/six-ghosts-crash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@lion/ui': patch
3+
---
4+
5+
[overlays] allow the popup dialog to close when it is setup by `lit` `cache`

packages/ui/components/dialog/test/lion-dialog.test.js

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
/* eslint-disable lit-a11y/no-autofocus */
2-
import { expect, fixture as _fixture, html, unsafeStatic, aTimeout } from '@open-wc/testing';
2+
import {
3+
expect,
4+
fixture as _fixture,
5+
html,
6+
unsafeStatic,
7+
aTimeout,
8+
defineCE,
9+
waitUntil,
10+
} from '@open-wc/testing';
11+
import { cache } from 'lit/directives/cache.js';
12+
import { LitElement, nothing } from 'lit';
313
import { runOverlayMixinSuite } from '../../overlays/test-suites/OverlayMixin.suite.js';
414
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
515
import '../test-helpers/test-router.js';
616
import '@lion/ui/define/lion-dialog.js';
17+
import '@lion/ui/define/lion-tabs.js';
718

819
/**
920
* @typedef {import('../src/LionDialog.js').LionDialog} LionDialog
@@ -296,5 +307,107 @@ describe('lion-dialog', () => {
296307
await dialogAfterRouteChange._overlayCtrl._showComplete;
297308
expect(dialogAfterRouteChange.opened).to.be.true;
298309
});
310+
311+
it('should close the popup dialog after rendered from cache', async () => {
312+
/**
313+
*
314+
* @param {Event} e
315+
* @returns
316+
*/
317+
const closeButtonHandler = e =>
318+
e.target?.dispatchEvent(new Event('close-overlay', { bubbles: true }));
319+
const dialog = html` <lion-dialog>
320+
<button slot="invoker" class="invoker-button">Click me to open dialog</button>
321+
<div slot="content" class="demo-dialog-content">
322+
Hello! You can close this dialog here:
323+
<button class="close-button" @click="${closeButtonHandler}"></button>
324+
</div>
325+
</lion-dialog>`;
326+
327+
/**
328+
* Note, inactive tab content is **destroyed** on every tab switch.
329+
*/
330+
class Wrapper extends LitElement {
331+
static properties = {
332+
...super.properties,
333+
activeTabIndex: { type: Number },
334+
};
335+
336+
constructor() {
337+
super();
338+
this.activeTabIndex = 0;
339+
}
340+
341+
/**
342+
* @param {number} index
343+
*/
344+
changeActiveTabIndex(index) {
345+
this.activeTabIndex = index;
346+
}
347+
348+
render() {
349+
const changeActiveTabIndexRef = this.changeActiveTabIndex.bind(this);
350+
return html`
351+
<lion-tabs>
352+
<button slot="tab" class="first-button" @click=${() => changeActiveTabIndexRef(0)}>
353+
First
354+
</button>
355+
<p slot="panel">${cache(this.activeTabIndex === 0 ? dialog : nothing)}</p>
356+
<button slot="tab" class="second-button" @click=${() => changeActiveTabIndexRef(1)}>
357+
Second
358+
</button>
359+
<p slot="panel">Info page with lots of information about us.</p>
360+
</lion-tabs>
361+
`;
362+
}
363+
}
364+
365+
const wrapperFixture = /** @type {(arg: TemplateResult) => Promise<Wrapper>} */ (_fixture);
366+
const tagString = defineCE(Wrapper);
367+
const wrapperTag = unsafeStatic(tagString);
368+
const wrapperElement = /** @type {Wrapper} */ (
369+
await wrapperFixture(html`<${wrapperTag}></${wrapperTag}>`)
370+
);
371+
await wrapperElement.updateComplete;
372+
const wrapperElementShadowRoot = wrapperElement.shadowRoot;
373+
/**
374+
* @returns { HTMLElement | null | undefined }
375+
*/
376+
const getFirstButton = () => wrapperElementShadowRoot?.querySelector('.first-button');
377+
/**
378+
* @returns { HTMLElement | null | undefined }
379+
*/
380+
const getSecondButton = () => wrapperElementShadowRoot?.querySelector('.second-button');
381+
/**
382+
* @returns { HTMLElement | null | undefined }
383+
*/
384+
const getInvokerButton = () => wrapperElementShadowRoot?.querySelector('.invoker-button');
385+
/**
386+
* @returns { HTMLElement | null | undefined }
387+
*/
388+
const getCloseButton = () => wrapperElementShadowRoot?.querySelector('.close-button');
389+
/**
390+
* @returns { Element | null | undefined }
391+
*/
392+
const getDialog = () =>
393+
wrapperElementShadowRoot?.querySelector('lion-dialog')?.shadowRoot?.querySelector('dialog');
394+
// @ts-ignore
395+
const isDialogVisible = () => getDialog()?.checkVisibility() === true;
396+
const isDialogRendered = () =>
397+
!!wrapperElement.shadowRoot?.querySelector('lion-dialog')?.shadowRoot?.childNodes.length;
398+
getInvokerButton()?.click();
399+
await waitUntil(isDialogVisible);
400+
getCloseButton()?.click();
401+
await waitUntil(() => !isDialogVisible());
402+
getSecondButton()?.click();
403+
await waitUntil(() => !isDialogRendered());
404+
getFirstButton()?.click();
405+
await waitUntil(isDialogRendered);
406+
getInvokerButton()?.click();
407+
await waitUntil(isDialogVisible);
408+
getCloseButton()?.click();
409+
await waitUntil(() => !isDialogVisible());
410+
expect(isDialogVisible()).to.equal(false);
411+
});
299412
});
300413
});

packages/ui/components/overlays/src/OverlayMixin.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,16 +246,23 @@ export const OverlayMixinImplementation = superclass =>
246246

247247
/** @protected */
248248
_setupOverlayCtrl() {
249-
if (this._overlayCtrl) return;
250-
251-
/** @type {OverlayController} */
252-
this._overlayCtrl = this._defineOverlay({
249+
if (this.#hasSetup) return;
250+
const config = {
253251
contentNode: this._overlayContentNode,
254252
contentWrapperNode: this._overlayContentWrapperNode,
255253
invokerNode: this._overlayInvokerNode,
256254
referenceNode: this._overlayReferenceNode,
257255
backdropNode: this._overlayBackdropNode,
258-
});
256+
};
257+
258+
if (this._overlayCtrl) {
259+
// when `lit` `cache` attaches node to the DOM, register the controller back in the OverlaysManager
260+
this._overlayCtrl.updateConfig(config);
261+
} else {
262+
/** @type {OverlayController} */
263+
this._overlayCtrl = this._defineOverlay(config);
264+
}
265+
259266
this.__syncToOverlayController();
260267
this.__setupSyncFromOverlayController();
261268
this._setupOpenCloseListeners();

packages/ui/components/overlays/test-suites/OverlayMixin.suite.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,11 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
533533

534534
el.switched = false;
535535
await el.updateComplete;
536-
537536
expect(setupSpy.callCount).to.equal(1);
537+
538+
const elCtrl = /** @type {OverlayEl} */ (el.overlayEl)?._overlayCtrl;
539+
const isCtrlRegisteredAtOverlaysManager = elCtrl.manager.list.some(ctrl => elCtrl === ctrl);
540+
expect(isCtrlRegisteredAtOverlaysManager).to.equal(true);
538541
});
539542

540543
it('correctly removes event listeners when disconnected from dom', async () => {

packages/ui/components/overlays/test/OverlayController.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ async function createNestedEscControllers(parentContent) {
153153

154154
afterEach(() => {
155155
overlays.teardown();
156+
// clean document.body from the DOM nodes left by previous tests
157+
document.body.innerHTML = '';
156158
});
157159

158160
describe('OverlayController', () => {

0 commit comments

Comments
 (0)