Skip to content

Commit bedec7c

Browse files
committed
fix: resuming nested container in shadow root
1 parent 33a15ac commit bedec7c

File tree

5 files changed

+108
-5
lines changed

5 files changed

+108
-5
lines changed

.changeset/ten-emus-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: resuming nested container in shadow root

packages/docs/src/routes/playground/parser/html/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default component$(() => {
4646

4747
// Try to get Qwik container if available
4848
try {
49-
const container = _getDomContainer(doc.documentElement);
49+
const container = _getDomContainer(doc.body.firstElementChild!);
5050
if (container) {
5151
output += '// Qwik Container Found:\n';
5252
output += `- Container Type: ${container.qContainer}\n`;

packages/qwik/src/core/client/process-vnode-data.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,14 +295,15 @@ export function processVNodeData(document: Document) {
295295
const shadowRootContainer = node as Element | null;
296296
const shadowRoot = shadowRootContainer?.shadowRoot;
297297
if (shadowRoot) {
298+
const firstShadowRootChild = firstChild(shadowRoot)!;
298299
walkContainer(
299300
// we need to create a new walker for the shadow root
300301
document.createTreeWalker(
301-
shadowRoot,
302+
firstShadowRootChild,
302303
0x1 /* NodeFilter.SHOW_ELEMENT */ | 0x80 /* NodeFilter.SHOW_COMMENT */
303304
),
304305
null,
305-
firstChild(shadowRoot),
306+
firstShadowRootChild,
306307
null,
307308
'',
308309
null!,

packages/qwik/src/core/client/process-vnode-data.unit.tsx

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,46 @@
11
import { describe, expect, it } from 'vitest';
2-
import { createDocument } from '../../testing/document';
2+
import { createDocument, mockAttachShadow } from '../../testing/document';
33
import '../../testing/vdom-diff.unit-util';
44
import { VNodeDataSeparator } from '../shared/vnode-data-types';
55
import { getDomContainer } from './dom-container';
66
import { processVNodeData } from './process-vnode-data';
77
import type { ClientContainer } from './types';
88
import { QContainerValue } from '../shared/types';
99
import { QContainerAttr } from '../shared/utils/markers';
10+
import { vnode_getFirstChild } from './vnode';
11+
import { Fragment } from '@qwik.dev/core';
1012

1113
describe('processVnodeData', () => {
14+
it('should process shadow root container', () => {
15+
const [, container] = process(`
16+
<html q:container="paused">
17+
<head :></head>
18+
<body :>
19+
<div q:shadowRoot>
20+
<template>
21+
<div q:container="paused">
22+
<button>
23+
0
24+
</button>
25+
<script type="qwik/vnode">
26+
~{1}!~
27+
</script>
28+
</div>
29+
</template>
30+
</div>
31+
</body>
32+
</html>
33+
`);
34+
vnode_getFirstChild(container.rootVNode);
35+
expect(container.rootVNode).toMatchVDOM(
36+
<div {...qContainerPaused}>
37+
<Fragment>
38+
<button>0</button>
39+
</Fragment>
40+
</div>
41+
);
42+
});
43+
1244
it('should parse simple case', () => {
1345
const [container] = process(`
1446
<html q:container="paused">
@@ -186,10 +218,35 @@ function process(html: string): ClientContainer[] {
186218
html = html.replace(/\n\s*/g, '');
187219
// console.log(html);
188220
const document = createDocument({ html });
221+
const templates = Array.from(document.querySelectorAll('template'));
222+
for (const template of templates) {
223+
const parent = template.parentElement!;
224+
if (parent.hasAttribute('q:shadowroot')) {
225+
const content = (template as any).content;
226+
mockAttachShadow(parent);
227+
const shadowRoot = (parent as any).attachShadow({ mode: 'open' });
228+
shadowRoot.append(content);
229+
template.remove();
230+
}
231+
}
189232
processVNodeData(document);
190-
return Array.from(document.querySelectorAll('[q\\:container="paused"]')).map(getDomContainer);
233+
234+
const containers: Element[] = [];
235+
findContainers(document, containers);
236+
237+
return containers.map(getDomContainer);
191238
}
192239

240+
const findContainers = (element: Document | ShadowRoot, containers: Element[]) => {
241+
Array.from(element.querySelectorAll('[q\\:container]')).forEach((container) => {
242+
containers.push(container);
243+
});
244+
element.querySelectorAll('[q\\:shadowroot]').forEach((parent) => {
245+
const shadowRoot = parent.shadowRoot;
246+
shadowRoot && findContainers(shadowRoot, containers);
247+
});
248+
};
249+
193250
function encodeVNode(data: Record<number, string> = {}) {
194251
const keys = Object.keys(data)
195252
.map((key) => parseInt(key, 10))

packages/qwik/src/testing/document.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
import type { MockDocumentOptions, MockWindow } from './types';
22
import qwikDom from '@qwik.dev/dom';
33
import { normalizeUrl } from './util';
4+
const domino = qwikDom as any;
45

6+
export function mockAttachShadow(el: Element) {
7+
if (typeof (el as any).attachShadow !== 'function') {
8+
(el as any).attachShadow = function (opts: any) {
9+
const sr = new MockShadowRoot(el);
10+
(el as any).shadowRoot = sr;
11+
return sr;
12+
};
13+
}
14+
if (typeof (el as any).hasAttribute !== 'function') {
15+
(el as any).hasAttribute = function (attr: string) {
16+
return el.getAttribute(attr) !== null;
17+
};
18+
}
19+
return el;
20+
}
521
/**
622
* Create emulated `Document` for server environment. Does not implement the full browser `document`
723
* and `window` API. This api may be removed in the future.
@@ -75,3 +91,27 @@ export function ensureGlobals(doc: any, opts?: MockDocumentOptions) {
7591
const noop = () => {};
7692

7793
const QWIK_DOC = Symbol();
94+
95+
class MockShadowRoot extends domino.impl.DocumentFragment {
96+
nodeType = 11; // DOCUMENT_FRAGMENT_NODE
97+
host: Element;
98+
99+
constructor(host: Element) {
100+
super();
101+
this.host = host;
102+
this.ownerDocument = host.ownerDocument;
103+
}
104+
105+
append(...nodes: any[]) {
106+
for (const node of nodes) {
107+
if (node.nodeType === 11) {
108+
// document fragment
109+
for (const child of Array.from(node.childNodes)) {
110+
this.appendChild(child as any);
111+
}
112+
} else {
113+
this.appendChild(node);
114+
}
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)