Skip to content

Commit de3e085

Browse files
refactor: improve closeElement comment and add conditional shadow DOM test
Co-authored-by: NullVoxPopuli <[email protected]> Agent-Logs-Url: https://github.com/emberjs/ember.js/sessions/e70854e9-f877-4c7d-81b1-ac34ddb3b3f9
1 parent 81a7992 commit de3e085

2 files changed

Lines changed: 56 additions & 11 deletions

File tree

packages/@glimmer-workspace/integration-tests/lib/suites/shadow-dom.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,4 +210,40 @@ export class ShadowDOMSuite extends RenderTest {
210210
'same shadow root instance reused (not recreated)'
211211
);
212212
}
213+
214+
@test
215+
'conditional <template shadowrootmode="open"> inside a host element attaches shadow root when rendered'() {
216+
if (typeof document === 'undefined' || !('attachShadow' in document.createElement('div'))) {
217+
this.assert.ok(true, 'Shadow DOM not supported, skipping');
218+
return;
219+
}
220+
221+
this.render(
222+
'<div class="host">{{#if this.useShadow}}<template shadowrootmode="open"><p>shadow content</p></template>{{else}}<div>regular content</div>{{/if}}</div>',
223+
{ useShadow: true }
224+
);
225+
226+
const rootEl = castToBrowser(this.element, 'HTML');
227+
const host = rootEl.querySelector('.host') as HTMLElement | null;
228+
229+
this.assert.ok(host !== null, 'host element exists');
230+
this.assert.ok(host?.shadowRoot !== null, 'shadow root is attached when useShadow=true');
231+
this.assert.strictEqual(
232+
host?.shadowRoot?.querySelector('p')?.textContent,
233+
'shadow content',
234+
'shadow content renders in shadow root'
235+
);
236+
237+
// Switch to regular content branch
238+
this.rerender({ useShadow: false });
239+
// The shadow root persists on the host element (platform behavior)
240+
// but the <div> regular content is rendered into the shadow root's slot
241+
// (since the shadow root owns the rendering for the host).
242+
// The important assertion is that no <template> element ended up in the DOM.
243+
this.assert.strictEqual(
244+
host?.querySelector('template'),
245+
null,
246+
'<template> element is not present in the DOM'
247+
);
248+
}
213249
}

packages/@glimmer/runtime/lib/vm/element-builder.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -296,18 +296,27 @@ export class NewTreeBuilder implements TreeBuilder {
296296
}
297297

298298
closeElement(): Nullable<ModifierInstance[]> {
299-
// Check if we are closing a shadow DOM section. Shadow roots (and document
300-
// fragments) have nodeType 11 (DOCUMENT_FRAGMENT_NODE), whereas regular
301-
// elements have nodeType 1 (ELEMENT_NODE).
302-
//
303-
// When we detect a shadow root as the current element, we need to pop the
304-
// block we pushed in flushElement (instead of using willCloseElement which
305-
// decrements the parent block's nesting counter).
306-
//
307-
// Note: this.element is typed as SimpleElement (nodeType 1), but at runtime
308-
// it may be a ShadowRoot (nodeType 11) that was stored during flushElement's
309-
// shadow DOM handling. We use an explicit nodeType check to detect this case.
310299
if (isShadowRootOrDocumentFragment(this.element)) {
300+
// A ShadowRoot (or DocumentFragment) is used as the rendering target for
301+
// declarative shadow DOM (`<template shadowrootmode="...">`) and
302+
// `{{#in-element}}` calls.
303+
//
304+
// Unlike a regular element, a shadow root:
305+
// 1. Has nodeType 11 (DOCUMENT_FRAGMENT_NODE) instead of 1 (ELEMENT_NODE)
306+
// 2. Must always be attached to a host element — it cannot exist as a
307+
// free-standing node — so its content must not be tracked in the
308+
// parent block's bounds (the host element is the visible boundary).
309+
//
310+
// Because of (2), `flushElement` pushed an extra *remote* block (isRemote=true)
311+
// for the shadow root content so that its nodes are excluded from the
312+
// enclosing block's first/last node tracking. We must pop that extra
313+
// block here rather than calling `willCloseElement()`, which would
314+
// incorrectly decrement the parent block's element-nesting counter.
315+
//
316+
// Note: `this.element` is typed as SimpleElement (nodeType 1), but at
317+
// runtime it may hold a ShadowRoot (nodeType 11) placed there during
318+
// `flushElement`'s shadow DOM handling. The explicit nodeType check in
319+
// `isShadowRootOrDocumentFragment` detects this case.
311320
this.popBlock();
312321
this.popElement();
313322
return this.popModifiers();

0 commit comments

Comments
 (0)