fix: regression of issue: ShadowHost can't be a string (issue 941) (#1092)

This commit is contained in:
Yun Feng
2026-04-01 12:00:00 +08:00
committed by GitHub
parent bc2b945436
commit a9c01eb8d0
3 changed files with 94 additions and 19 deletions

View File

@@ -27,6 +27,7 @@ import {
isSerializedIframe, isSerializedIframe,
isSerializedStylesheet, isSerializedStylesheet,
inDom, inDom,
getShadowHost,
} from '../utils'; } from '../utils';
type DoubleLinkedListNode = { type DoubleLinkedListNode = {
@@ -268,18 +269,11 @@ export default class MutationBuffer {
return nextId; return nextId;
}; };
const pushAdd = (n: Node) => { const pushAdd = (n: Node) => {
let shadowHost: Element | null = null;
if (
n.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE &&
(n.getRootNode() as ShadowRoot).host
)
shadowHost = (n.getRootNode() as ShadowRoot).host;
if (!n.parentNode || !inDom(n)) { if (!n.parentNode || !inDom(n)) {
return; return;
} }
const parentId = isShadowRoot(n.parentNode) const parentId = isShadowRoot(n.parentNode)
? this.mirror.getId(shadowHost) ? this.mirror.getId(getShadowHost(n))
: this.mirror.getId(n.parentNode); : this.mirror.getId(n.parentNode);
const nextId = getNextId(n); const nextId = getNextId(n);
if (parentId === -1 || nextId === -1) { if (parentId === -1 || nextId === -1) {

View File

@@ -519,16 +519,29 @@ export class StyleSheetMirror {
} }
} }
export function getRootShadowHost(n: Node): Node | null { /**
const shadowHost = (n.getRootNode() as ShadowRoot).host; * Get the direct shadow host of a node in shadow dom. Returns null if it is not in a shadow dom.
// If n is in a nested shadow dom. */
let rootShadowHost = shadowHost; export function getShadowHost(n: Node): Element | null {
let shadowHost: Element | null = null;
while ( if (
rootShadowHost?.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && n.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE &&
(rootShadowHost.getRootNode() as ShadowRoot).host (n.getRootNode() as ShadowRoot).host
) )
rootShadowHost = (rootShadowHost.getRootNode() as ShadowRoot).host; shadowHost = (n.getRootNode() as ShadowRoot).host;
return shadowHost;
}
/**
* Get the root shadow host of a node in nested shadow doms. Returns the node itself if it is not in a shadow dom.
*/
export function getRootShadowHost(n: Node): Node {
let rootShadowHost: Node = n;
let shadowHost: Element | null;
// If n is in a nested shadow dom.
while ((shadowHost = getShadowHost(rootShadowHost)))
rootShadowHost = shadowHost;
return rootShadowHost; return rootShadowHost;
} }
@@ -537,7 +550,7 @@ export function shadowHostInDom(n: Node): boolean {
const doc = n.ownerDocument; const doc = n.ownerDocument;
if (!doc) return false; if (!doc) return false;
const shadowHost = getRootShadowHost(n); const shadowHost = getRootShadowHost(n);
return Boolean(shadowHost && doc.contains(shadowHost)); return doc.contains(shadowHost);
} }
export function inDom(n: Node): boolean { export function inDom(n: Node): boolean {

View File

@@ -1,7 +1,13 @@
/** /**
* @jest-environment jsdom * @jest-environment jsdom
*/ */
import { StyleSheetMirror } from '../src/utils'; import {
getRootShadowHost,
StyleSheetMirror,
inDom,
shadowHostInDom,
getShadowHost,
} from '../src/utils';
describe('Utilities for other modules', () => { describe('Utilities for other modules', () => {
describe('StyleSheetMirror', () => { describe('StyleSheetMirror', () => {
@@ -75,4 +81,66 @@ describe('Utilities for other modules', () => {
expect(mirror.add(new CSSStyleSheet())).toBe(1); expect(mirror.add(new CSSStyleSheet())).toBe(1);
}); });
}); });
describe('inDom()', () => {
it('should get correct result given nested shadow doms', () => {
const shadowHost = document.createElement('div');
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
const shadowHost2 = document.createElement('div');
const shadowRoot2 = shadowHost2.attachShadow({ mode: 'open' });
const div = document.createElement('div');
shadowRoot.appendChild(shadowHost2);
shadowRoot2.appendChild(div);
// Not in Dom yet.
expect(getShadowHost(div)).toBe(shadowHost2);
expect(getRootShadowHost(div)).toBe(shadowHost);
expect(shadowHostInDom(div)).toBeFalsy();
expect(inDom(div)).toBeFalsy();
// Added to the Dom.
document.body.appendChild(shadowHost);
expect(getShadowHost(div)).toBe(shadowHost2);
expect(getRootShadowHost(div)).toBe(shadowHost);
expect(shadowHostInDom(div)).toBeTruthy();
expect(inDom(div)).toBeTruthy();
});
it('should get correct result given a normal node', () => {
const div = document.createElement('div');
// Not in Dom yet.
expect(getShadowHost(div)).toBeNull();
expect(getRootShadowHost(div)).toBe(div);
expect(shadowHostInDom(div)).toBeFalsy();
expect(inDom(div)).toBeFalsy();
// Added to the Dom.
document.body.appendChild(div);
expect(getShadowHost(div)).toBeNull();
expect(getRootShadowHost(div)).toBe(div);
expect(shadowHostInDom(div)).toBeTruthy();
expect(inDom(div)).toBeTruthy();
});
/**
* Given the textNode of a detached HTMLAnchorElement, getRootNode() will return the anchor element itself and its host property is a string.
* This corner case may cause an error in getRootShadowHost().
*/
it('should get correct result given the textNode of a detached HTMLAnchorElement', () => {
const a = document.createElement('a');
a.href = 'example.com';
a.textContent = 'something';
// Not in Dom yet.
expect(getShadowHost(a.childNodes[0])).toBeNull();
expect(getRootShadowHost(a.childNodes[0])).toBe(a.childNodes[0]);
expect(shadowHostInDom(a.childNodes[0])).toBeFalsy();
expect(inDom(a.childNodes[0])).toBeFalsy();
// Added to the Dom.
document.body.appendChild(a);
expect(getShadowHost(a.childNodes[0])).toBeNull();
expect(getRootShadowHost(a.childNodes[0])).toBe(a.childNodes[0]);
expect(shadowHostInDom(a.childNodes[0])).toBeTruthy();
expect(inDom(a.childNodes[0])).toBeTruthy();
});
});
}); });