Implement checking for "is declarative shadow root" in attachShadow()
With this CL, if a declarative shadow root is already present when
attachShadow() is called, the contents of this shadow root are
removed, and the existing shadow root is returned. This is per Step 5
of the new spec [1], to allow coexistence of declarative content and
custom element hydration code.
This CL also adds an exhaustive test of all element types, both those
supporting and disallowing shadow root attachment.
[1] https://whatpr.org/dom/858.html#concept-attach-a-shadow-root
Bug: 1042130
Change-Id: Ie9fc39eeb324934f3cb13ab2eb6bc8a82d99d667
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2159987
Commit-Queue: Mason Freed <masonfreed@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Reviewed-by: Kouhei Ueno <kouhei@chromium.org>
Auto-Submit: Mason Freed <masonfreed@chromium.org>
Cr-Commit-Position: refs/heads/master@{#761964}
diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
index 0422124..4703f4fa1 100644
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -3700,10 +3700,7 @@
// 4. If shadow host has a non-null shadow root whose is declarative shadow
// root property is false, then throw an "NotSupportedError" DOMException.
- // 5. TODO(masonfreed): If shadow host has a non-null shadow root whose is
- // declarative shadow root property is true, then remove all of shadow root’s
- // children, in tree order. Return shadow host’s shadow root.
- if (GetShadowRoot()) {
+ if (GetShadowRoot() && !GetShadowRoot()->IsDeclarativeShadowRoot()) {
return "Shadow root cannot be created on a host "
"which already hosts a shadow tree.";
}
@@ -3784,6 +3781,15 @@
GetDocument().SetShadowCascadeOrder(ShadowCascadeOrder::kShadowCascadeV1);
+ if (auto* shadow_root = GetShadowRoot()) {
+ // 5. If shadow host has a non-null shadow root whose "is declarative shadow
+ // root property is true, then remove all of shadow root’s children, in tree
+ // order. Return shadow host’s shadow root.
+ DCHECK(shadow_root->IsDeclarativeShadowRoot());
+ shadow_root->RemoveChildren();
+ return *shadow_root;
+ }
+
// 6. Let shadow be a new shadow root whose node document is shadow host’s
// node document, host is shadow host, and mode is mode.
// 9. Set shadow host’s shadow root to shadow.
diff --git a/third_party/blink/renderer/core/html/parser/html_tree_builder.cc b/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
index f07a1db..c6551c6 100644
--- a/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
+++ b/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
@@ -942,13 +942,19 @@
tree_.ActiveFormattingElements()->ClearToLastMarker();
template_insertion_modes_.pop_back();
ResetInsertionModeAppropriately();
- // Check for a declarative shadow root.
if (RuntimeEnabledFeatures::DeclarativeShadowDOMEnabled() &&
template_stack_item) {
DCHECK(template_stack_item->IsElementNode());
HTMLTemplateElement* template_element =
DynamicTo<HTMLTemplateElement>(template_stack_item->GetElement());
- if (template_element->IsDeclarativeShadowRoot()) {
+ // 9. If the start tag for the declarative template element did not have an
+ // attribute with the name "shadowroot" whose value was an ASCII
+ // case-insensitive match for the strings "open" or "closed", then stop this
+ // algorithm.
+ // 10. If the adjusted current node is the topmost element in the stack of
+ // open elements, then stop this algorithm.
+ if (template_element->IsDeclarativeShadowRoot() &&
+ shadow_host_stack_item->GetNode() != tree_.OpenElements()->RootNode()) {
DCHECK(shadow_host_stack_item);
DCHECK(shadow_host_stack_item->IsElementNode());
UseCounter::Count(shadow_host_stack_item->GetElement()->GetDocument(),
diff --git a/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-attachment.tentative.html b/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-attachment.tentative.html
new file mode 100644
index 0000000..0acdeb8
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-attachment.tentative.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<title>Declarative Shadow DOM Element Attachment</title>
+<link rel='author' title='Mason Freed' href='mailto:masonfreed@chromium.org'>
+<link rel='help' href='https://github.com/whatwg/dom/issues/831'>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/shadow-dom-utils.js'></script>
+
+<script>
+const shadowContent = '<span>Shadow tree</span><slot></slot>';
+function getDeclarativeContent(mode, delegatesFocus) {
+ const delegatesFocusText = delegatesFocus ? ' shadowrootdelegatesfocus' : '';
+ return `<template shadowroot=${mode}${delegatesFocusText}>${shadowContent}</template>`;
+}
+
+const lightDomTextContent = 'Light DOM';
+function addDeclarativeShadowRoot(elementType, mode, delegatesFocus) {
+ const declarativeString = `<${elementType} id=theelement>${getDeclarativeContent(mode, delegatesFocus)}
+ <span class='lightdom'>${lightDomTextContent}</span></${elementType}>`;
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = declarativeString;
+ const element = wrapper.querySelector('#theelement');
+ return {wrapper: wrapper, element: element};
+}
+
+function testElementType(allowed, nochildren, elementType, mode, delegatesFocus) {
+ var t = test(function() {
+ const nodes = addDeclarativeShadowRoot(elementType, mode, delegatesFocus);
+ if (allowed) {
+ const element = nodes.element;
+ assert_true(!!element, 'Unable to locate the element');
+ // Just one light DOM child, and no leftover template.
+ assert_true(!nodes.wrapper.querySelector('template'));
+ assert_equals(element.children.length, 1);
+ assert_equals(element.children[0].textContent, lightDomTextContent);
+ let originalShadowRoot = null;
+ if (mode === 'open') {
+ // TODO(masonfreed): Add a check for ElementInternals.shadowRoot once that exists.
+ assert_true(!!element.shadowRoot, 'Shadow root should be present');
+ assert_equals(element.shadowRoot.innerHTML, shadowContent, 'Correct shadow content');
+ originalShadowRoot = element.shadowRoot;
+ }
+
+ // Now, call attachShadow() and make sure we get back the same (original) shadowRoot, but empty.
+ const oppositeMode = (mode === 'open') ? 'closed' : 'open';
+ const newShadow = element.attachShadow({mode: oppositeMode}); // Should be no exception here
+ if (mode === 'open') {
+ // TODO(masonfreed): Add a check for ElementInternals.shadowRoot once that exists.
+ assert_equals(element.shadowRoot, originalShadowRoot, 'The same shadow root should be returned');
+ assert_equals(element.shadowRoot.innerHTML, '', 'Empty shadow content');
+ assert_equals(element.shadowRoot.mode, mode, 'Original shadow mode');
+ }
+ } else {
+ const wrapper = nodes.wrapper;
+ if (!nochildren) {
+ // Invalid elements should retain a <template> element child with a shadowroot attribute.
+ const template = nodes.wrapper.querySelector('template[shadowroot]');
+ assert_true(!!template);
+ assert_equals(template.getAttribute('shadowroot'), mode, `Template with shadowroot=${mode} should be left over`);
+ const span = nodes.wrapper.querySelector('span.lightdom');
+ assert_true(!!span);
+ assert_equals(span.textContent, lightDomTextContent);
+ if (nodes.element) {
+ // For some tags (e.g. <html>) there won't be an element inside wrapper.
+ assert_true(!nodes.element.shadowRoot, 'Shadow root should not be present');
+ }
+ }
+ }
+ }, `Declarative Shadow DOM as a child of <${elementType}>, with mode=${mode}, delegatesFocus=${delegatesFocus}. Should be ${allowed ? 'safelisted' : 'disallowed'}.`);
+}
+
+function runAllTests() {
+ const noChildElements = ['iframe','noscript','script','select','style','textarea','title'];
+ const noCheck = ['body'];
+ const safelisted = ATTACHSHADOW_SAFELISTED_ELEMENTS.filter(el => !noCheck.includes(el));
+ const disallowed = ATTACHSHADOW_DISALLOWED_ELEMENTS.filter(el => !noCheck.includes(el));
+ const minimumKnownElements = 113; // We should have at least this many elements in the lists from shadow-dom-utils.js.
+ assert_true(safelisted.length + disallowed.length + noChildElements.length >= minimumKnownElements,'All element types should be tested');
+ for (let delegatesFocus of [false, true]) {
+ for (let mode of ['open', 'closed', 'invalid']) {
+ for (let elementName of safelisted) {
+ testElementType(mode !== 'invalid', false, elementName, mode, delegatesFocus);
+ }
+ for (let elementName of disallowed) {
+ testElementType(false, noChildElements.includes(elementName), elementName, mode, delegatesFocus);
+ }
+ }
+ }
+}
+
+runAllTests();
+
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom.tentative.html b/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-basic.tentative.html
similarity index 100%
rename from third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom.tentative.html
rename to third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-basic.tentative.html