fix(overlays): support reconnecting overlay nodes inside other overlays
This commit is contained in:
parent
82f019dc82
commit
afd5ac96cc
3 changed files with 89 additions and 8 deletions
|
|
@ -126,17 +126,25 @@ export const OverlayMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(changedProperties) {
|
connectedCallback() {
|
||||||
super.firstUpdated(changedProperties);
|
if (super.connectedCallback) {
|
||||||
// we setup in firstUpdated so we can use nodes from light and shadowDom
|
super.connectedCallback();
|
||||||
this._setupOverlayCtrl();
|
}
|
||||||
|
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() {
|
disconnectedCallback() {
|
||||||
if (super.disconnectedCallback) {
|
if (super.disconnectedCallback) {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._overlayCtrl) {
|
if (this._overlayCtrl) {
|
||||||
|
this.__tornDown = true;
|
||||||
|
this.__overlayContentNodeWrapperBeforeTeardown = this._overlayContentNodeWrapper;
|
||||||
this._teardownOverlayCtrl();
|
this._teardownOverlayCtrl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,6 +186,13 @@ export const OverlayMixin = dedupeMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupOverlayCtrl() {
|
_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({
|
this._overlayCtrl = this._defineOverlay({
|
||||||
contentNode: this._overlayContentNode,
|
contentNode: this._overlayContentNode,
|
||||||
invokerNode: this._overlayInvokerNode,
|
invokerNode: this._overlayInvokerNode,
|
||||||
|
|
@ -186,6 +201,7 @@ export const OverlayMixin = dedupeMixin(
|
||||||
this.__syncToOverlayController();
|
this.__syncToOverlayController();
|
||||||
this.__setupSyncFromOverlayController();
|
this.__setupSyncFromOverlayController();
|
||||||
this._setupOpenCloseListeners();
|
this._setupOpenCloseListeners();
|
||||||
|
this.__overlaySetupCompleteResolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
_teardownOverlayCtrl() {
|
_teardownOverlayCtrl() {
|
||||||
|
|
@ -239,5 +255,13 @@ export const OverlayMixin = dedupeMixin(
|
||||||
this._overlayCtrl.hide();
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
withDropdownConfig,
|
withDropdownConfig,
|
||||||
withModalDialogConfig,
|
withModalDialogConfig,
|
||||||
} from '../index.js';
|
} from '../index.js';
|
||||||
|
|
||||||
import './demo-overlay-system.js';
|
import './demo-overlay-system.js';
|
||||||
import './applyDemoOverlayStyles.js';
|
import './applyDemoOverlayStyles.js';
|
||||||
import { ref as r } from './directives/ref.js';
|
import { ref as r } from './directives/ref.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 sinon from 'sinon';
|
||||||
|
import { overlays } from '../src/overlays.js';
|
||||||
|
|
||||||
export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) {
|
export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) {
|
||||||
describe(`OverlayMixin${suffix}`, () => {
|
describe(`OverlayMixin${suffix}`, () => {
|
||||||
|
|
@ -22,12 +23,12 @@ export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) {
|
||||||
it('syncs opened to overlayController', async () => {
|
it('syncs opened to overlayController', async () => {
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
expect(el.opened).to.be.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;
|
expect(el._overlayCtrl.isShown).to.be.true;
|
||||||
|
|
||||||
el.opened = false;
|
el.opened = false;
|
||||||
expect(el.opened).to.be.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;
|
expect(el._overlayCtrl.isShown).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -80,6 +81,8 @@ export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) {
|
||||||
<button slot="invoker">invoker button</button>
|
<button slot="invoker">invoker button</button>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
// Wait until it's done opening (handling features is async)
|
||||||
|
await nextFrame();
|
||||||
expect(beforeSpy).not.to.have.been.called;
|
expect(beforeSpy).not.to.have.been.called;
|
||||||
await el._overlayCtrl.hide();
|
await el._overlayCtrl.hide();
|
||||||
expect(beforeSpy).to.have.been.called;
|
expect(beforeSpy).to.have.been.called;
|
||||||
|
|
@ -137,4 +140,59 @@ export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) {
|
||||||
expect(el.opened).to.be.false;
|
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}>
|
||||||
|
<div slot="content">content of the nested overlay</div>
|
||||||
|
<button slot="invoker">invoker nested</button>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const mainEl = await fixture(html`
|
||||||
|
<${tag}>
|
||||||
|
<div slot="content">
|
||||||
|
open nested overlay:
|
||||||
|
${nestedEl}
|
||||||
|
</div>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue