mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] utils: Correct validation of mount target in shadow DOM/iframe
Previously, `validateTarget` only checked if the target element or its host (if it was a ShadowRoot) was directly contained in the document body. This failed in cases where the target element was nested inside a shadow DOM, which itself was attached to the document. This commit introduces a new helper `isAttachedToDocument` that traverses through parent nodes and shadow roots to ensure that the target is ultimately attached to the given document. Additionally, it now throws a clear error if `document.defaultView` is missing, indicating that the target document is detached or invalid. This ensures proper validation of mount targets, including complex scenarios with shadow roots and iframes.
This commit is contained in:
committed by
Géry Debongnie
parent
fd3c194525
commit
9d378b0e7b
+32
-2
@@ -35,13 +35,43 @@ export function inOwnerDocument(el?: HTMLElement) {
|
|||||||
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the given element is contained in a specific root documnet:
|
||||||
|
* either directly or with a shadow root in between or in an iframe.
|
||||||
|
*/
|
||||||
|
function isAttachedToDocument(
|
||||||
|
element: HTMLElement | ShadowRoot,
|
||||||
|
documentElement: Document
|
||||||
|
): boolean {
|
||||||
|
let current: Node = element;
|
||||||
|
const shadowRoot = documentElement.defaultView!.ShadowRoot;
|
||||||
|
while (current) {
|
||||||
|
if (current === documentElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (current.parentNode) {
|
||||||
|
current = current.parentNode;
|
||||||
|
} else if (current instanceof shadowRoot && current.host) {
|
||||||
|
current = current.host;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
||||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||||
const document = target && target.ownerDocument;
|
const document = target && target.ownerDocument;
|
||||||
if (document) {
|
if (document) {
|
||||||
const HTMLElement = document.defaultView!.HTMLElement;
|
if (!document.defaultView) {
|
||||||
|
throw new OwlError(
|
||||||
|
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const HTMLElement = document.defaultView.HTMLElement;
|
||||||
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||||
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
if (!isAttachedToDocument(target, document)) {
|
||||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -27,6 +27,58 @@ exports[`shadow_dom can mount app 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app in closed shadow dom 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside a separate HTML document 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside a shadow child element 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside an element in a shadow root inside an iframe 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`shadow_dom useRef hook 1`] = `
|
exports[`shadow_dom useRef hook 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -29,6 +29,24 @@ describe("shadow_dom", () => {
|
|||||||
expect(status(comp)).toBe("destroyed");
|
expect(status(comp)).toBe("destroyed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can mount app in closed shadow dom", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
fixture.appendChild(container);
|
||||||
|
const shadow = container.attachShadow({ mode: "closed" });
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadow);
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
test("can bind event handler", async () => {
|
test("can bind event handler", async () => {
|
||||||
let a = 1;
|
let a = 1;
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
@@ -64,4 +82,73 @@ describe("shadow_dom", () => {
|
|||||||
await mountedProm;
|
await mountedProm;
|
||||||
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can mount app inside a shadow child element", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
const shadow = fixture.attachShadow({ mode: "open" });
|
||||||
|
const shadowDiv = document.createElement("div");
|
||||||
|
shadow.append(shadowDiv);
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadowDiv);
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can mount app inside a separate HTML document", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const separateDoc = document.implementation.createHTMLDocument();
|
||||||
|
const container = separateDoc.createElement("div");
|
||||||
|
separateDoc.body.appendChild(container);
|
||||||
|
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
let error: Error;
|
||||||
|
try {
|
||||||
|
await app.mount(container);
|
||||||
|
} catch (e) {
|
||||||
|
error = e as Error;
|
||||||
|
}
|
||||||
|
expect(error!).toBeDefined();
|
||||||
|
expect(error!.message).toBe(
|
||||||
|
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can mount app inside an element in a shadow root inside an iframe", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
fixture.appendChild(iframe);
|
||||||
|
|
||||||
|
const iframeDoc = iframe.contentDocument!;
|
||||||
|
const container = iframeDoc.createElement("div");
|
||||||
|
iframeDoc.body.appendChild(container);
|
||||||
|
|
||||||
|
const shadow = container.attachShadow({ mode: "open" });
|
||||||
|
|
||||||
|
const shadowTarget = iframeDoc.createElement("div");
|
||||||
|
shadow.appendChild(shadowTarget);
|
||||||
|
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadowTarget);
|
||||||
|
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
expect(iframeDoc.body.contains(container)).toBe(true);
|
||||||
|
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user