diff --git a/.changeset/friendly-dolphins-give.md b/.changeset/friendly-dolphins-give.md
new file mode 100644
index 000000000..161aa715b
--- /dev/null
+++ b/.changeset/friendly-dolphins-give.md
@@ -0,0 +1,15 @@
+---
+'@lion/overlays': minor
+'@lion/select-rich': minor
+'@lion/dialog': minor
+'@lion/input-datepicker': minor
+'@lion/tooltip': minor
+'@lion/form-integrations': minor
+---
+
+- Make the OverlayController constructor phase synchronous.
+- Trigger a setup of the OverlayController on every connectedCallback
+- Execute a new OverlayController after (shadowDom) rendering of the element is done
+- Teardown the OverlayController on every disconnectedCallback
+- This means moving a dialog triggers teardown in the old location and setup in the new location
+- Restore the original light dom (if needed) in the teardown phase of the OverlayController
diff --git a/package.json b/package.json
index e2a033087..516f44c9a 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"build:docs": "wca analyze \"packages/tabs/**/*.js\"",
"build:types": "tsc -p tsconfig.build.types.json",
"bundlesize": "rollup -c bundlesize/rollup.config.js && bundlesize",
- "debug": "web-test-runner \"packages/dialog/test/**/*.test.js\" --watch",
+ "debug": "web-test-runner \"packages/input-datepicker/test/**/*.test.js\" --watch",
"dev-server": "es-dev-server",
"format": "npm run format:eslint && npm run format:prettier",
"format:eslint": "eslint --ext .js,.html . --fix",
@@ -48,7 +48,7 @@
"@storybook/addon-a11y": "~5.0.0",
"@types/chai-dom": "^0.0.8",
"@web/dev-server-legacy": "^0.1.1",
- "@web/test-runner": "^0.7.3",
+ "@web/test-runner": "^0.7.13",
"@web/test-runner-browserstack": "^0.1.1",
"@web/test-runner-playwright": "^0.5.1",
"@web/test-runner-puppeteer": "^0.6.1",
diff --git a/packages/form-core/test-suites/ValidateMixin.suite.js b/packages/form-core/test-suites/ValidateMixin.suite.js
index 785635196..4bc7ae2a1 100644
--- a/packages/form-core/test-suites/ValidateMixin.suite.js
+++ b/packages/form-core/test-suites/ValidateMixin.suite.js
@@ -721,7 +721,6 @@ export function runValidateMixinSuite(customConfig) {
.modelValue=${''}
>${lightDom}${tag}>
`));
- console.log(el._inputNode);
expect(el._inputNode?.getAttribute('aria-required')).to.equal('true');
el.validators = [];
expect(el._inputNode?.getAttribute('aria-required')).to.be.null;
diff --git a/packages/form-integrations/docs/60-dialog-integration.md b/packages/form-integrations/docs/60-dialog-integration.md
new file mode 100644
index 000000000..356256b1f
--- /dev/null
+++ b/packages/form-integrations/docs/60-dialog-integration.md
@@ -0,0 +1,40 @@
+[//]: # 'AUTO INSERT HEADER PREPUBLISH'
+
+# Forms in a dialog
+
+```js script
+import { html } from 'lit-html';
+import '@lion/dialog/lion-dialog.js';
+import '@lion/select-rich/lion-select-rich.js';
+import '@lion/select-rich/lion-options.js';
+import '@lion/select-rich/lion-option.js';
+
+export default {
+ title: 'Forms/System/Dialog integrations',
+};
+```
+
+Opening a Rich Select inside a dialog
+
+```js story
+export const main = () => html`
+
+
+
+
+
+ Red
+ Hotpink
+ Teal
+
+
+
+
+
+`;
+```
diff --git a/packages/input-datepicker/src/LionInputDatepicker.js b/packages/input-datepicker/src/LionInputDatepicker.js
index 9b1d79563..d719ec807 100644
--- a/packages/input-datepicker/src/LionInputDatepicker.js
+++ b/packages/input-datepicker/src/LionInputDatepicker.js
@@ -207,7 +207,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
}
/**
- * Defining this overlay as a templates lets OverlayInteraceMixin
+ * Defining this overlay as a templates from OverlayMixin
* this is our source to give as .contentNode to OverlayController.
* Important: do not change the name of this method.
*/
@@ -283,7 +283,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
}
async __openCalendarOverlay() {
- this._overlayCtrl.show();
+ await this._overlayCtrl.show();
await Promise.all([
this._overlayCtrl.contentNode.updateComplete,
this._calendarNode.updateComplete,
diff --git a/packages/input-datepicker/test/lion-input-datepicker.test.js b/packages/input-datepicker/test/lion-input-datepicker.test.js
index 283b4de03..d4ef8c0c4 100644
--- a/packages/input-datepicker/test/lion-input-datepicker.test.js
+++ b/packages/input-datepicker/test/lion-input-datepicker.test.js
@@ -175,6 +175,7 @@ describe('', () => {
const el = await fixture(html``);
const elObj = new DatepickerInputObject(el);
await elObj.openCalendar();
+ await aTimeout();
expect(isSameDate(elObj.calendarEl.focusedDate, elObj.calendarEl.centralDate)).to.be.true;
});
diff --git a/packages/overlays/src/OverlayController.js b/packages/overlays/src/OverlayController.js
index 18a6a8650..b1ad9dc58 100644
--- a/packages/overlays/src/OverlayController.js
+++ b/packages/overlays/src/OverlayController.js
@@ -268,8 +268,8 @@ export class OverlayController {
// }
}
- async _init({ cfgToAdd }) {
- this.__initcontentWrapperNode({ cfgToAdd });
+ _init({ cfgToAdd }) {
+ this.__initContentWrapperNode({ cfgToAdd });
this.__initConnectionTarget();
if (this.placementMode === 'local') {
@@ -310,7 +310,7 @@ export class OverlayController {
* @desc Cleanup ._contentWrapperNode. We do this, because creating a fresh wrapper
* can lead to problems with event listeners...
*/
- __initcontentWrapperNode({ cfgToAdd }) {
+ __initContentWrapperNode({ cfgToAdd }) {
if (this.config.contentWrapperNode && this.placementMode === 'local') {
/** config [l2],[l3],[l4] */
this._contentWrapperNode = this.config.contentWrapperNode;
@@ -568,7 +568,7 @@ export class OverlayController {
* @param {object} config
* @param {'init'|'show'|'hide'|'teardown'} config.phase
*/
- async _handleFeatures({ phase }) {
+ _handleFeatures({ phase }) {
this._handleZIndex({ phase });
if (this.preventsScroll) {
@@ -856,11 +856,15 @@ export class OverlayController {
teardown() {
this._handleFeatures({ phase: 'teardown' });
+ if (this.placementMode === 'global' && this.__isContentNodeProjected) {
+ this.__originalContentParent.appendChild(this.contentNode);
+ }
+
// Remove the content node wrapper from the global rootnode
- this._teardowncontentWrapperNode();
+ this._teardownContentWrapperNode();
}
- _teardowncontentWrapperNode() {
+ _teardownContentWrapperNode() {
if (
this.placementMode === 'global' &&
this._contentWrapperNode &&
diff --git a/packages/overlays/src/OverlayMixin.js b/packages/overlays/src/OverlayMixin.js
index 0a619bdb6..a007dba48 100644
--- a/packages/overlays/src/OverlayMixin.js
+++ b/packages/overlays/src/OverlayMixin.js
@@ -22,11 +22,8 @@ export const OverlayMixin = dedupeMixin(
constructor() {
super();
this.opened = false;
+ this.__needsSetup = true;
this.config = {};
-
- this._overlaySetupComplete = new Promise(resolve => {
- this.__overlaySetupCompleteResolve = resolve;
- });
}
get config() {
@@ -134,52 +131,24 @@ export const OverlayMixin = dedupeMixin(
}
connectedCallback() {
- if (super.connectedCallback) {
- super.connectedCallback();
- }
-
- // Wait for DOM to be ready before setting up the overlay, else extensions like rich select breaks
+ super.connectedCallback();
+ // we do a setup after every connectedCallback as firstUpdated will only be called once
+ this.__needsSetup = true;
this.updateComplete.then(() => {
- if (!this.__isOverlaySetup) {
+ if (this.__needsSetup) {
this._setupOverlayCtrl();
}
+ this.__needsSetup = false;
});
-
- // When dom nodes are being moved around (meaning connected/disconnected are being fired
- // repeatedly), we need to delay the teardown until we find a 'permanent disconnect'
- if (this.__rejectOverlayDisconnectComplete) {
- // makes sure _overlayDisconnectComplete never resolves: we don't want a teardown
- this.__rejectOverlayDisconnectComplete();
- }
}
disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
-
- if (!this._overlayCtrl) {
- return;
+ if (this._overlayCtrl) {
+ this._teardownOverlayCtrl();
}
-
- this._overlayDisconnectComplete = new Promise((resolve, reject) => {
- this.__resolveOverlayDisconnectComplete = resolve;
- this.__rejectOverlayDisconnectComplete = reject;
- });
-
- setTimeout(() => {
- // we start the teardown below
- this.__resolveOverlayDisconnectComplete();
- });
-
- // We need to prevent that we create a setup/teardown cycle during startup, where it
- // is common that the overlay system moves around nodes. Therefore, we make the
- // teardown async, so that it only happens when we are permanently disconnecting from dom
- this._overlayDisconnectComplete
- .then(() => {
- this._teardownOverlayCtrl();
- })
- .catch(() => {});
}
get _overlayInvokerNode() {
@@ -213,15 +182,12 @@ export const OverlayMixin = dedupeMixin(
this.__syncToOverlayController();
this.__setupSyncFromOverlayController();
this._setupOpenCloseListeners();
- this.__overlaySetupCompleteResolve();
- this.__isOverlaySetup = true;
}
_teardownOverlayCtrl() {
this._teardownOpenCloseListeners();
this.__teardownSyncFromOverlayController();
this._overlayCtrl.teardown();
- this.__isOverlaySetup = false;
}
/**
diff --git a/packages/overlays/test-suites/OverlayMixin.suite.js b/packages/overlays/test-suites/OverlayMixin.suite.js
index 9e18bb788..b9f1b0c96 100644
--- a/packages/overlays/test-suites/OverlayMixin.suite.js
+++ b/packages/overlays/test-suites/OverlayMixin.suite.js
@@ -195,10 +195,10 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
it('supports nested overlays', async () => {
const el = await fixture(html`
- <${tag}>
+ <${tag} id="main-dialog">
open nested overlay:
- <${tag}>
+ <${tag} id="sub-dialog">
Nested content
@@ -222,6 +222,23 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
expect(nestedOverlayEl._overlayCtrl.contentNode).to.be.displayed;
});
+ it('[global] allows for moving of the element', async () => {
+ const el = await fixture(html`
+ <${tag}>
+
content of the nested overlay
+
+ ${tag}>
+ `);
+ if (el._overlayCtrl.placementMode === 'global') {
+ expect(getGlobalOverlayNodes().length).to.equal(1);
+
+ const moveTarget = await fixture('');
+ moveTarget.appendChild(el);
+ await el.updateComplete;
+ expect(getGlobalOverlayNodes().length).to.equal(1);
+ }
+ });
+
it('reconstructs the overlay when disconnected and reconnected to DOM (support for nested overlay nodes)', async () => {
const nestedEl = await fixture(html`
<${tag} id="nest">
@@ -241,15 +258,12 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
`);
if (el._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 overlayContainerNodes = getGlobalOverlayNodes();
expect(overlayContainerNodes.length).to.equal(2);
- const lastContentNodeInContainer = overlayContainerNodes[0];
+ const lastContentNodeInContainer = overlayContainerNodes[1];
// Check that the last container is the nested one with the intended content
- expect(lastContentNodeInContainer.firstElementChild.innerText).to.equal(
+ expect(lastContentNodeInContainer.firstElementChild.firstChild.textContent).to.equal(
'content of the nested overlay',
);
expect(lastContentNodeInContainer.firstElementChild.slot).to.equal('content');
@@ -259,43 +273,5 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
expect(contentNode.innerText).to.equal('content of the nested overlay');
}
});
-
- it("doesn't tear down controller when dom nodes are being moved around", async () => {
- const nestedEl = await fixture(html`
- <${tag} id="nest">
-