Skip to content

Commit

Permalink
Change the behavior of clonable to be more opt-in
Browse files Browse the repository at this point in the history
See the discussion here:

whatwg/html#10107 (comment)

The new consensus is that the old behavior was likely web-
incompatible, plus not very developer-desirable. The new behavior
adds a `shadowrootclonable` attribute for declarative shadow dom
to opt-in to clonable shadow roots.

This is a slight behavior change from the existing shipped
behavior, in that before the `clonable` concept was introduced,
*any* declarative shadow root within a `<template>` would be
cloned. Now, that behavior is opt in. So:

old:
  <template>
    <div>
      <template shadowrootmode=open>
        I do NOT get cloned!
      </template>
    </div>
  </template>

new:
  <template>
    <div>
      <template shadowrootmode=open shadowrootclonable>
        I get cloned!
      </template>
    </div>
  </template>

See these three spec PRs:
  whatwg/dom#1246
  whatwg/html#10069
  whatwg/html#10117

Bug: 1510466
Change-Id: Ice7c7579094eb08b882c4bb44f93045f23b8f222
  • Loading branch information
mfreed7 authored and chromium-wpt-export-bot committed Feb 2, 2024
1 parent 64b75a9 commit 3b4a768
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 28 deletions.
3 changes: 2 additions & 1 deletion resources/declarative-shadow-dom-polyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ function polyfill_declarative_shadow_dom(root) {
root.querySelectorAll("template[shadowrootmode]").forEach(template => {
const mode = template.getAttribute("shadowrootmode");
const delegatesFocus = template.hasAttribute("shadowrootdelegatesfocus");
const shadowRoot = template.parentNode.attachShadow({ mode, delegatesFocus });
const clonable = template.hasAttribute("shadowrootclonable");
const shadowRoot = template.parentNode.attachShadow({ mode, delegatesFocus, clonable });
shadowRoot.appendChild(template.content);
template.remove();
polyfill_declarative_shadow_dom(shadowRoot);
Expand Down
32 changes: 27 additions & 5 deletions shadow-dom/declarative/declarative-shadow-dom-basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@
assert_true(!!host.shadowRoot,"No shadow root found");
assert_false(host.shadowRoot.delegatesFocus,"delegatesFocus should be false without the shadowrootdelegatesfocus attribute");
}, 'Declarative Shadow DOM: delegates focus attribute');

test(() => {
const div = document.createElement('div');
div.setHTMLUnsafe(`
<div id="host">
<template shadowrootmode="open" shadowrootclonable>
</template>
</div>
`);
var host = div.querySelector('#host');
assert_true(!!host.shadowRoot,"No shadow root found");
assert_true(host.shadowRoot.clonable,"clonable should be true");
div.setHTMLUnsafe(`
<div id="host">
<template shadowrootmode="open">
</template>
</div>
`);
host = div.querySelector('#host');
assert_true(!!host.shadowRoot,"No shadow root found");
assert_false(host.shadowRoot.clonable,"clonable should be false without the shadowrootclonable attribute");
}, 'Declarative Shadow DOM: clonable attribute');
</script>

<div id="multi-host" style="display:none">
Expand Down Expand Up @@ -168,7 +190,7 @@

<template id="template-containing-shadow">
<div class="innerdiv">
<template shadowrootmode=open>Content</template>
<template shadowrootmode=open shadowrootclonable>Content</template>
</div>
</template>
<script>
Expand Down Expand Up @@ -205,13 +227,13 @@
assert_equals(innerDiv.querySelector('template'), null, "No leftover template node");
assert_not_equals(shadowRoot1,shadowRoot3,'Should not get back the same shadow root');

}, 'Declarative Shadow DOM: template containing declarative shadow root');
}, 'Declarative Shadow DOM: template containing declarative shadow root (with shadowrootclonable)');
</script>

<template id="template-containing-deep-shadow">
<div><div><div><div><div>
<div class="innerdiv">
<template shadowrootmode=open>Content</template>
<template shadowrootmode=open shadowrootclonable>Content</template>
</div>
</div></div></div></div></div>
</template>
Expand All @@ -230,7 +252,7 @@
<div>
<template id="inner-template">
<div class="innerdiv">
<template shadowrootmode=open>Content</template>
<template shadowrootmode=open shadowrootclonable>Content</template>
</div>
</template>
</div>
Expand All @@ -249,7 +271,7 @@

<template id="template-containing-ua-shadow">
<div class="innerdiv">
<template shadowrootmode=open>
<template shadowrootmode=open shadowrootclonable>
<video></video> <!--Assumed to have UA shadow root-->
</template>
</div>
Expand Down
14 changes: 6 additions & 8 deletions shadow-dom/declarative/declarative-shadow-dom-repeats.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@
</script>

<div id=open2>
<template shadowrootmode=open shadowrootdelegatesfocus>
Open, delegates focus (not the default),
named slot assignment (the default), clonable (the default for declarative)
<template shadowrootmode=open shadowrootdelegatesfocus shadowrootclonable>
Open, delegates focus (not the default), clonable (not the default)
named slot assignment (the default)
</template>
</div>

Expand All @@ -66,11 +66,9 @@
assert_throws_dom("NotSupportedError",() => {
open2.attachShadow({mode: "open", delegatesFocus: true, slotAssignment: "manual", clonable: true});
},'Mismatched shadow root slotAssignment should throw');
// See https://github.com/whatwg/html/issues/10107: the behavior of the
// clonable flag is still being discussed.
// assert_throws_dom("NotSupportedError",() => {
// open2.attachShadow({mode: "open", delegatesFocus: true, slotAssignment: "named", clonable: false});
// },'Mismatched shadow root clonable should throw');
assert_throws_dom("NotSupportedError",() => {
open2.attachShadow({mode: "open", delegatesFocus: true, slotAssignment: "named", clonable: false});
},'Mismatched shadow root clonable should throw');

const initialShadow = open2.shadowRoot;
const shadow = open2.attachShadow({mode: "open", delegatesFocus: true, slotAssignment: "named", clonable: true}); // Shouldn't throw
Expand Down
20 changes: 12 additions & 8 deletions shadow-dom/declarative/gethtml.tentative.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<body>

<script>
function testElementType(allowsShadowDom, elementType, runGetHTMLOnShadowRoot, declarativeShadowDom, mode, delegatesFocus, serializable) {
function testElementType(allowsShadowDom, elementType, runGetHTMLOnShadowRoot, declarativeShadowDom, mode, delegatesFocus, serializable, clonable) {
const t = test(t => {
// Create and attach element
let wrapper;
Expand All @@ -27,7 +27,7 @@

let shadowRoot;
const isOpen = mode === 'open';
let initDict = {mode: mode, delegatesFocus: delegatesFocus};
let initDict = {mode: mode, delegatesFocus: delegatesFocus, clonable};
let expectedSerializable = null;
switch (serializable) {
case undefined: expectedSerializable = false; break;
Expand All @@ -37,9 +37,10 @@
}
const delegatesAttr = delegatesFocus ? ' shadowrootdelegatesfocus=""' : '';
const serializableAttr = expectedSerializable ? ' serializable=""' : '';
const clonableAttr = clonable ? ' shadowrootclonable=""' : '';

if (allowsShadowDom && declarativeShadowDom) {
const html = `<${elementType}><template shadowrootmode=${mode}${delegatesAttr}${serializableAttr}>`;
const html = `<${elementType}><template shadowrootmode=${mode}${delegatesAttr}${serializableAttr}${clonableAttr}>`;
wrapper.setHTMLUnsafe(html);
if (isOpen) {
shadowRoot = wrapper.firstElementChild.shadowRoot;
Expand All @@ -58,11 +59,12 @@
assert_true(!allowsShadowDom || !!shadowRoot);

if (allowsShadowDom) {
const correctShadowHtml = `<template shadowrootmode="${mode}"${delegatesAttr}${serializableAttr}><slot></slot></template>`;
const correctShadowHtml = `<template shadowrootmode="${mode}"${delegatesAttr}${serializableAttr}${clonableAttr}><slot></slot></template>`;
const correctHtml = `<${elementType}>${correctShadowHtml}</${elementType}>`;
assert_equals(shadowRoot.mode,mode);
assert_equals(shadowRoot.delegatesFocus,delegatesFocus);
assert_equals(shadowRoot.serializable,expectedSerializable);
assert_equals(shadowRoot.clonable,clonable);
shadowRoot.appendChild(document.createElement('slot'));
const emptyElement = `<${elementType}></${elementType}>`;
if (isOpen) {
Expand Down Expand Up @@ -91,7 +93,7 @@
// ...and that the default for includeShadowRoots is false.
assert_equals(wrapper.getHTML(),wrapper.innerHTML,'The default for includeShadowRoots should be false');

}, `${runGetHTMLOnShadowRoot ? 'ShadowRoot' : 'Element'}.getHTML() on <${elementType}>${allowsShadowDom ? `, with ${declarativeShadowDom ? 'declarative' : 'imperative'} shadow, mode=${mode}, delegatesFocus=${delegatesFocus}, serializable=${serializable}.` : ''}`);
}, `${runGetHTMLOnShadowRoot ? 'ShadowRoot' : 'Element'}.getHTML() on <${elementType}>${allowsShadowDom ? `, with ${declarativeShadowDom ? 'declarative' : 'imperative'} shadow, mode=${mode}, delegatesFocus=${delegatesFocus}, serializable=${serializable}, clonable=${clonable}.` : ''}`);
}

function runAllTests() {
Expand All @@ -103,9 +105,11 @@
if (allowsShadowDom) {
for (const declarativeShadowDom of [false, true]) {
for (const delegatesFocus of [false, true]) {
for (const mode of ['open', 'closed']) {
for (const serializable of [undefined, 'false', 'true']) {
testElementType(true, elementName, runGetHTMLOnShadowRoot, declarativeShadowDom, mode, delegatesFocus, serializable);
for (const clonable of [false, true]) {
for (const mode of ['open', 'closed']) {
for (const serializable of [undefined, 'false', 'true']) {
testElementType(true, elementName, runGetHTMLOnShadowRoot, declarativeShadowDom, mode, delegatesFocus, serializable, clonable);
}
}
}
}
Expand Down
22 changes: 16 additions & 6 deletions shadow-dom/shadow-root-clonable.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,26 @@
div.setHTMLUnsafe('<div><template shadowrootmode=open><input></template></div>');
const root = div.firstElementChild.shadowRoot;
assert_true(!!root);
assert_true(root.clonable, "clonable is automatically true for declarative shadow root");
assert_true(!root.clonable, "clonable is *not* automatically true for declarative shadow root");

const clone = div.cloneNode(true);
const clonedRoot = clone.firstElementChild.shadowRoot;
assert_true(!clonedRoot,'no shadow root gets cloned');
}, "declarative shadow roots do *not* get clonable: true automatically");

test(() => {
const div = document.createElement("div");
div.setHTMLUnsafe('<div><template shadowrootmode=open shadowrootclonable><input></template></div>');
const root = div.firstElementChild.shadowRoot;
assert_true(!!root);
assert_true(root.clonable, "clonable gets added when shadowrootclonable is present");

const clone = div.cloneNode(true);
const clonedRoot = clone.firstElementChild.shadowRoot;
assert_true(!!clonedRoot);
assert_equals(clonedRoot.children.length, 1, "children count");
assert_equals(clonedRoot.children[0].localName, "input", "children content");
}, "declarative shadow roots get clonable: true automatically");
}, "declarative shadow roots can opt in to clonable with shadowrootclonable");
</script>

<template id="test">
Expand All @@ -70,8 +82,6 @@
assert_true(!!root);
const clone = template.content.cloneNode(true);
const clonedRoot = clone.querySelector('#host').shadowRoot;
assert_true(!!clonedRoot);
assert_equals(clonedRoot.children.length, 1, "children count");
assert_equals(clonedRoot.children[0].localName, "input", "children content");
}, "declarative shadow roots inside templates also get cloned automatically");
assert_true(!clonedRoot,'no shadow root gets cloned');
}, "declarative shadow roots inside templates do *not* get cloned automatically");
</script>

0 comments on commit 3b4a768

Please sign in to comment.