Shadow DOM: shadow trees, slots and scoped styles - #892
LinuxBoy-96 wants to merge 3 commits into
Conversation
|
This is the .html file I used to test this PR |
yinnho
left a comment
There was a problem hiding this comment.
Reviewing against the model we converged on in #889 — we shipped the same shape in our own renderer (children never rewritten, composed view built at box construction), so most of this is confirmation from the other side of that decision, plus one bug we think is real.
Inheritance direction — your traversal_parent for slotted nodes is right (Chrome-verified). This was the subtlest call in the PR, so here's ground truth from local Chrome (headless, 152):
<div style="color:blue"><my-e>hello <span>light text</span></my-e></div>
<script>
const sr = document.querySelector('my-e').attachShadow({mode:'open'});
sr.innerHTML = '<slot style="color:red"></slot>';
</script>- computed
colorof the slotted<span>:rgb(255, 0, 0)— inherited from the slot, not from its light parent - with
color:redremoved: slot, slotted span, and a shadow<div>all computergb(0, 0, 255)— blue walks host → shadow content and host → slot → slotted
So flat-tree inheritance through the slot is what Chrome does; traversal_parent returning self.assigned_slot matches, and inherited properties set on shadow-tree elements correctly reach slotted light content. (What must NOT reach slotted content is ordinary selector matching — which ::slotted() being unimplemented already guarantees.)
Bug: in-document bookkeeping never crosses into shadow trees. attach_shadow snapshots in_document at attach time, and process_added_subtree/process_removed_subtree walk iter_subtree_mut, which only iterates children — the shadow root is deliberately not in the host's children, so:
const el = document.createElement('my-e'); // detached
const sr = el.attachShadow({mode: 'open'}); // root created without IS_IN_DOCUMENT
sr.innerHTML = '<link rel="stylesheet" href="a.css"><slot></slot>';
document.body.appendChild(el); // process_added_subtree walks light children onlyThe shadow subtree never becomes in-document, and since LoadStylesheet / LoadImage / style_nodes registration all happen inside that walk, a <link rel=stylesheet> inside this shadow tree never loads. Later appends into the tree read new_parent_is_in_document off the unflagged root, so nothing eager-loads afterwards either. The remove direction has the mirrored staleness: detaching a host leaves the shadow subtree flagged and its id-map entries live. The fix shape that worked for us: make just the added/removed bookkeeping walks visit shadow_root alongside children — not iter_subtree_mut itself, since light-semantics callers rely on it staying light.
One determinism question. add_shadow_stylesheet appends in registration order, which for <link>s is fetch-completion order — two same-specificity conflicting <link>s in one shadow tree whose loads land out of order will cascade nondeterministically. The document side has the same append-at-completion shape today, so this is a pre-existing family rather than a regression, but shadow trees concentrate <link> usage, so it may be worth sorting by owner node order per root at flush time. This bit us once: we collect shadow <style>s in document order precisely because iterating them from a HashMap reshuffled equal-specificity rules per run.
JS-face nit. attachShadow({ mode: "bogus" }) throws TypeError in Chrome (invalid ShadowRootMode); the binding here silently coerces to open. Looks like a one-line validation in attach_shadow.
Everything else held up against the scars on our implementation: switching Traverser to composed_children() keeps flat-tree order correct for sibling combinators, running assign_slots at resolve() entry closes the mutation→resolve staleness window for assigned_nodes/assigned_slot, and the rescope_stylesheets dance (script-built <style> registers document-wide first, moves to its root on append) is the same corner we had to handle separately.
|
Thanks — the in-document bug was real. Fixed in the shape you describe: a |
Implements shadow DOM on the model agreed in #<numéro de l'issue shadow-dom>:
Node::childrenis never rewritten, the composed tree is a view.Model
Node::childrenstays the light DOM, so index-based mutations(dioxus-native),
childNodes,querySelectorand text content keep DOMsemantics with no special cases.
Node::composed_children()yields ahost's shadow-root children, a
<slot>'s assigned nodes (its own childrenwhen nothing is assigned), otherwise
children. Style traversal(
Traverser), box construction (every container-iteration site inconstruct.rs, including the helperspush_hoisted_children_and_pseudos,find_inline_layout_embedded_boxesanditer_children_and_pseudos_mut,which now go through
iter_composed_children_mut) and, through theresulting layout tree, painting and hit testing walk the composed view.
Light-tree walks stay on
children; the accessor's doc comment states therule.
shadow-root, flaggedIS_SHADOW_ROOT, whoseparentis its host but which is not in the host'schildren. UA styleshadow-root { display: contents };display_style()reports
contentsfor it since Stylo never styles it.assigned_nodeson the slot,assigned_sloton the light child), refreshed byassign_slotsbeforestyle resolution (
slotattribute vsname, unnamed slot = default, textnodes to the default slot, first matching slot wins). Slotted nodes inherit
from their slot (
traversal_parent).goes to the host (
damage_box_owner), and damage propagation walks theshadow root's children so changes inside a shadow tree rebuild the host's
boxes. Stylesheets owned by a subtree are re-scoped when the subtree is
inserted (a script-built
<style>is registered before it is appended).Style scoping
TShadowRoot,TNode::as_shadow_root,TElement::{shadow_root, containing_shadow, containing_shadow_host, parent_node_is_shadow_root, is_html_slot_element, traversal_parent, traversal_children}are wired toStylo.
AuthorStyles<DocumentStyleSheet>;<style>/<link>inside a shadow tree register there instead of the documentstylist and are flushed in
resolve(). Document author sheets applyoutside shadow trees only, shadow sheets inside theirs:
:hostworks, pageCSS no longer leaks into components.
Script bindings
blitz-vibey-script:Element.prototype.attachShadow({ mode })andelement.shadowRoot(open roots only).Not in this PR
::slotted()(needsslotted_nodes/assigned_slotslices; follow-up).mode: closedis recorded, not enforced.<template shadowrootmode>).Tests
cargo test -p blitz-dom shadow(light DOM untouched, composition throughthe shadow root, named slots, removal).
<style>+:hostborder, default and named slots,no leak of page CSS into the component,
shadowRootidentity (in the PRthread).
shadow-dom/andcss/css-scoping/once the runner has thosedirectories.
WPT results
Subtests: 122 newly passing, 1 newly failing (net +121). Crashes: +1.
Full diff (80 changed tests)
Generated by the WPT workflow.