Skip to content

Commit 532c9e3

Browse files
committed
fix: detect shadow root without instanceof in fallback selectors
1 parent ba23f44 commit 532c9e3

4 files changed

Lines changed: 37 additions & 5 deletions

File tree

scenario/shadow-dom-fallback.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<div>
88
<template shadowrootmode="open">
99
<!-- root -->
10-
<div><!-- expect: :scope > :nth-child(1) --></div>
10+
<div><!-- expect: :nth-child(1) --></div>
1111
<div></div>
1212
</template>
1313
</div>

src/selector-fallback.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getElementParents } from "./utilities-dom.js";
1+
import { getElementParents, isShadowRoot } from "./utilities-dom.js";
22
import { SELECTOR_SEPARATOR } from "./constants.js";
33
import { CSS_SELECTOR_TYPE, CssSelector, OPERATOR } from "./types.js";
44
import {
@@ -14,14 +14,14 @@ export function getElementFallbackSelector(
1414
root?: ParentNode,
1515
): CssSelector {
1616
const parentElements = getElementParents(element, root).reverse();
17-
const isShadowRoot = root instanceof ShadowRoot;
17+
const rootIsShadowRoot = isShadowRoot(root);
1818

1919
const elementsData = parentElements.map((element, index) => {
2020
const elementData = createElementData(
2121
element,
2222
[CSS_SELECTOR_TYPE.nthchild],
2323
// do not use child combinator for the first element in ShadowRoot
24-
isShadowRoot && index === 0 ? OPERATOR.NONE : OPERATOR.CHILD,
24+
rootIsShadowRoot && index === 0 ? OPERATOR.NONE : OPERATOR.CHILD,
2525
);
2626
(elementData.selectors.nthchild ?? []).forEach((selectorData) => {
2727
selectorData.include = true;
@@ -30,7 +30,7 @@ export function getElementFallbackSelector(
3030
});
3131

3232
// Don't use :scope prefix for ShadowRoot since it doesn't work correctly
33-
const prefix = isShadowRoot ? "" : root ? ":scope" : ":root";
33+
const prefix = rootIsShadowRoot ? "" : root ? ":scope" : ":root";
3434

3535
return [prefix, ...elementsData.map(constructElementSelector)].join("");
3636
}

src/utilities-dom.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ export function getParents(elements: Element[], root?: ParentNode): Element[] {
6161
);
6262
}
6363

64+
/**
65+
* Uses a nodeType check instead of instanceof to work across iframe
66+
* boundaries. A shadow root is the only fragment node with a host.
67+
*/
68+
export function isShadowRoot(input: unknown): input is ShadowRoot {
69+
return (
70+
typeof input === "object" &&
71+
input !== null &&
72+
"nodeType" in input &&
73+
(input as Node).nodeType === Node.DOCUMENT_FRAGMENT_NODE &&
74+
"host" in input
75+
);
76+
}
77+
6478
/**
6579
* Returns root node for given element. This needs to be used because of document-less environments, e.g. jsdom.
6680
*/

test/selector-fallback.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,24 @@ describe("selector - fallback", function () {
101101
);
102102
});
103103

104+
it("should work with a shadow root in another document", () => {
105+
const iframe = document.body.appendChild(document.createElement("iframe"));
106+
const iframeDoc = iframe.contentDocument;
107+
const host = iframeDoc.body.appendChild(iframeDoc.createElement("div"));
108+
const shadowRoot = host.attachShadow({ mode: "open" });
109+
const targetElement = shadowRoot.appendChild(
110+
iframeDoc.createElement("div"),
111+
);
112+
shadowRoot.appendChild(iframeDoc.createElement("div"));
113+
114+
const result = getFallbackSelector([targetElement], shadowRoot);
115+
116+
assert.equal(result, ":nth-child(1)");
117+
assert.deepEqual([...shadowRoot.querySelectorAll(result)], [targetElement]);
118+
119+
iframe.remove();
120+
});
121+
104122
it("should work with elements in iframe", () => {
105123
const iframe = document.createElement("iframe");
106124
iframe.style.display = "none";

0 commit comments

Comments
 (0)