diff --git a/packages/overlays/src/OverlayMixin.js b/packages/overlays/src/OverlayMixin.js index 0eddac764..41ec83803 100644 --- a/packages/overlays/src/OverlayMixin.js +++ b/packages/overlays/src/OverlayMixin.js @@ -126,17 +126,25 @@ export const OverlayMixin = dedupeMixin( } } - firstUpdated(changedProperties) { - super.firstUpdated(changedProperties); - // we setup in firstUpdated so we can use nodes from light and shadowDom - this._setupOverlayCtrl(); + connectedCallback() { + if (super.connectedCallback) { + super.connectedCallback(); + } + this._overlaySetupComplete = new Promise(resolve => { + this.__overlaySetupCompleteResolve = resolve; + }); + // Wait for DOM to be ready before setting up the overlay + this.updateComplete.then(() => this._setupOverlayCtrl()); } disconnectedCallback() { if (super.disconnectedCallback) { super.disconnectedCallback(); } + if (this._overlayCtrl) { + this.__tornDown = true; + this.__overlayContentNodeWrapperBeforeTeardown = this._overlayContentNodeWrapper; this._teardownOverlayCtrl(); } } @@ -178,6 +186,13 @@ export const OverlayMixin = dedupeMixin( } _setupOverlayCtrl() { + // When we reconnect, this is for recovering from disconnectedCallback --> teardown which removes the + // the content node wrapper contents (which is necessary for global overlays to remove them from bottom of body) + if (this.__tornDown) { + this.__reappendContentNodeWrapperNodes(); + this.__tornDown = false; + } + this._overlayCtrl = this._defineOverlay({ contentNode: this._overlayContentNode, invokerNode: this._overlayInvokerNode, @@ -186,6 +201,7 @@ export const OverlayMixin = dedupeMixin( this.__syncToOverlayController(); this.__setupSyncFromOverlayController(); this._setupOpenCloseListeners(); + this.__overlaySetupCompleteResolve(); } _teardownOverlayCtrl() { @@ -239,5 +255,13 @@ export const OverlayMixin = dedupeMixin( this._overlayCtrl.hide(); } } + + // TODO: Simplify this logic of tearing down / reappending overlay content node wrapper + // after we have moved this wrapper to ShadowDOM. + __reappendContentNodeWrapperNodes() { + Array.from(this.__overlayContentNodeWrapperBeforeTeardown.children).forEach(child => { + this.appendChild(child); + }); + } }, ); diff --git a/packages/overlays/stories/20-index.stories.mdx b/packages/overlays/stories/20-index.stories.mdx index a09bca9be..5ccba7414 100644 --- a/packages/overlays/stories/20-index.stories.mdx +++ b/packages/overlays/stories/20-index.stories.mdx @@ -5,7 +5,6 @@ import { withDropdownConfig, withModalDialogConfig, } from '../index.js'; - import './demo-overlay-system.js'; import './applyDemoOverlayStyles.js'; import { ref as r } from './directives/ref.js'; diff --git a/packages/overlays/test-suites/OverlayMixin.suite.js b/packages/overlays/test-suites/OverlayMixin.suite.js index 15b75ca4c..c0b5a635e 100644 --- a/packages/overlays/test-suites/OverlayMixin.suite.js +++ b/packages/overlays/test-suites/OverlayMixin.suite.js @@ -1,5 +1,6 @@ -import { expect, fixture, html, aTimeout } from '@open-wc/testing'; +import { expect, fixture, html, nextFrame } from '@open-wc/testing'; import sinon from 'sinon'; +import { overlays } from '../src/overlays.js'; export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) { describe(`OverlayMixin${suffix}`, () => { @@ -22,12 +23,12 @@ export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) { it('syncs opened to overlayController', async () => { el.opened = true; expect(el.opened).to.be.true; - await aTimeout(); // overlayCtrl show/hide is async + await nextFrame(); // overlayCtrl show/hide is async expect(el._overlayCtrl.isShown).to.be.true; el.opened = false; expect(el.opened).to.be.false; - await aTimeout(0); // overlayCtrl show/hide is async + await nextFrame(); // overlayCtrl show/hide is async expect(el._overlayCtrl.isShown).to.be.false; }); @@ -80,6 +81,8 @@ export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) { `); + // Wait until it's done opening (handling features is async) + await nextFrame(); expect(beforeSpy).not.to.have.been.called; await el._overlayCtrl.hide(); expect(beforeSpy).to.have.been.called; @@ -137,4 +140,59 @@ export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) { expect(el.opened).to.be.false; }); }); + + describe(`OverlayMixin${suffix} nested`, () => { + it('reconstructs the overlay when disconnected and reconnected to DOM (support for nested overlay nodes)', async () => { + const nestedEl = await fixture(html` + <${tag}> +
content of the nested overlay
+ + + `); + + const mainEl = await fixture(html` + <${tag}> +
+ open nested overlay: + ${nestedEl} +
+ + + `); + + if (mainEl._overlayCtrl.placementMode === 'global') { + // Specifically checking the output in global root node, because the _contentOverlayNode still references + // the node that was removed in the teardown but hasn't been garbage collected due to reference to it still existing.. + + // Find the outlets that are not backdrop outlets + const outletsInGlobalRootNode = Array.from(overlays.globalRootNode.children).filter( + child => + child.slot === '_overlay-shadow-outlet' && + !child.classList.contains('global-overlays__backdrop'), + ); + + // Check the last one, which is the most nested one + const lastContentNodeInContainer = + outletsInGlobalRootNode[outletsInGlobalRootNode.length - 1]; + expect(outletsInGlobalRootNode.length).to.equal(2); + + // Check that it indeed has the intended content + expect(lastContentNodeInContainer.firstElementChild.innerText).to.equal( + 'content of the nested overlay', + ); + expect(lastContentNodeInContainer.firstElementChild.slot).to.equal('content'); + } else { + const actualNestedOverlay = mainEl._overlayContentNode.firstElementChild; + const outletNode = Array.from(actualNestedOverlay.children).find( + child => child.slot === '_overlay-shadow-outlet', + ); + const contentNode = Array.from(outletNode.children).find(child => child.slot === 'content'); + + expect(contentNode).to.not.be.undefined; + expect(contentNode.innerText).to.equal('content of the nested overlay'); + } + + expect(true).to.be.true; + }); + }); }