Change the behavior of clonable to be more opt-in

See the discussion here:

https://github.com/whatwg/html/issues/10107#issuecomment-1921837943

The existing *shipped* behavior (i.e. before the `clonable` concept
was introduced) was that any declarative shadow root *within a `<template>`* would be automatically cloned, but no others.

The semi-new behavior is the `clonable` bit concept, in which all
declarative shadow roots have their `clonable` bit set to true, so
they automatically get cloned by `cloneNode()`. That's regardless of
whether they are inside or outside a template.

The new consensus is that the "semi-new" clonable behavior is likely
web-incompatible, because clones will just start getting shadow roots
included. Plus it wasn't very developer-desirable. The new consensus
is therefore to add a `shadowrootclonable` attribute for declarative shadow dom that allows a shadow root to opt-in to this behavior, but
the default for all shadow roots will be `clonable=false`.

This CL implements the new consensus behind the ShadowRootClonable
flag. If the flag is false, the "shipped" behavior will be emulated
via setting `clonable` in an equivalent way.

See these three spec PRs:
  https://github.com/whatwg/dom/pull/1246
  https://github.com/whatwg/html/pull/10069
  https://github.com/whatwg/html/pull/10117

Bug: 1510466
Change-Id: Ice7c7579094eb08b882c4bb44f93045f23b8f222
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5260748
Reviewed-by: David Baron <dbaron@chromium.org>
Auto-Submit: Mason Freed <masonf@chromium.org>
Commit-Queue: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1258910}
diff --git a/scroll-animations/css/scroll-timeline-name-shadow.html b/scroll-animations/css/scroll-timeline-name-shadow.html
index d165335..a535b2a 100644
--- a/scroll-animations/css/scroll-timeline-name-shadow.html
+++ b/scroll-animations/css/scroll-timeline-name-shadow.html
@@ -34,7 +34,7 @@
   </style>
   <div class=scroller>
     <div class=scroller>
-      <template shadowrootmode=open>
+      <template shadowrootmode=open shadowrootclonable>
         <style>
           :host {
             scroll-timeline: --timeline y;
@@ -71,7 +71,7 @@
     }
   </style>
   <div class=host>
-    <template shadowrootmode=open>
+    <template shadowrootmode=open shadowrootclonable>
       <style>
         ::slotted(.scroller) {
           scroll-timeline: --timeline y;
@@ -108,7 +108,7 @@
     }
   </style>
   <div class=host>
-    <template shadowrootmode=open>
+    <template shadowrootmode=open shadowrootclonable>
       <style>
           /* Not using 'anim' at document scope, due to https://crbug.com/1334534 */
           @keyframes anim2 {
@@ -152,7 +152,7 @@
   </style>
   <div class=scroller>
     <div class=host>
-      <template shadowrootmode=open>
+      <template shadowrootmode=open shadowrootclonable>
         <style>
           div {
             scroll-timeline: --timeline y;
diff --git a/scroll-animations/css/view-timeline-name-shadow.html b/scroll-animations/css/view-timeline-name-shadow.html
index ad15f56..11902a3 100644
--- a/scroll-animations/css/view-timeline-name-shadow.html
+++ b/scroll-animations/css/view-timeline-name-shadow.html
@@ -36,7 +36,7 @@
   <div class=scroller>
     <div>
       <div class=target>
-        <template shadowrootmode=open>
+        <template shadowrootmode=open shadowrootclonable>
           <style>
             :host {
               view-timeline: --timeline y;
@@ -73,7 +73,7 @@
   </style>
   <div class=scroller>
     <div class=host>
-      <template shadowrootmode=open>
+      <template shadowrootmode=open shadowrootclonable>
         <style>
           ::slotted(.target) {
             view-timeline: --timeline y;
@@ -109,7 +109,7 @@
     }
   </style>
   <div class=host>
-    <template shadowrootmode=open>
+    <template shadowrootmode=open shadowrootclonable>
       <style>
           /* Not using 'anim' at document scope, due to https://crbug.com/1334534 */
           @keyframes anim2 {
@@ -153,7 +153,7 @@
   </style>
   <div class=scroller>
     <div class=host>
-      <template shadowrootmode=open>
+      <template shadowrootmode=open shadowrootclonable>
         <style>
           div {
             view-timeline: --timeline y;
diff --git a/shadow-dom/declarative/declarative-shadow-dom-basic.html b/shadow-dom/declarative/declarative-shadow-dom-basic.html
index 8799978..8bc6bec 100644
--- a/shadow-dom/declarative/declarative-shadow-dom-basic.html
+++ b/shadow-dom/declarative/declarative-shadow-dom-basic.html
@@ -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">
@@ -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>
@@ -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>
@@ -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>
@@ -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>
diff --git a/shadow-dom/declarative/declarative-shadow-dom-repeats.html b/shadow-dom/declarative/declarative-shadow-dom-repeats.html
index 5a992c6..69f5c0f 100644
--- a/shadow-dom/declarative/declarative-shadow-dom-repeats.html
+++ b/shadow-dom/declarative/declarative-shadow-dom-repeats.html
@@ -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>
 
@@ -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
diff --git a/shadow-dom/declarative/gethtml.tentative.html b/shadow-dom/declarative/gethtml.tentative.html
index 564caaa..c48230c 100644
--- a/shadow-dom/declarative/gethtml.tentative.html
+++ b/shadow-dom/declarative/gethtml.tentative.html
@@ -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;
@@ -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;
@@ -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;
@@ -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) {
@@ -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() {
@@ -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);
+                }
               }
             }
           }
diff --git a/shadow-dom/shadow-root-clonable.html b/shadow-dom/shadow-root-clonable.html
index 6272d11..a8d4a4c 100644
--- a/shadow-dom/shadow-root-clonable.html
+++ b/shadow-dom/shadow-root-clonable.html
@@ -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_false(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">
@@ -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>