Commit 16f1bfbe authored by Mason Freed's avatar Mason Freed Committed by Commit Bot

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: default avatarKent Tamura <tkent@chromium.org>
Reviewed-by: default avatarKouhei Ueno <kouhei@chromium.org>
Auto-Submit: Mason Freed <masonfreed@chromium.org>
Cr-Commit-Position: refs/heads/master@{#761964}
parent ed35f417
...@@ -3700,10 +3700,7 @@ const char* Element::ErrorMessageForAttachShadow() const { ...@@ -3700,10 +3700,7 @@ const char* Element::ErrorMessageForAttachShadow() const {
// 4. If shadow host has a non-null shadow root whose is declarative shadow // 4. If shadow host has a non-null shadow root whose is declarative shadow
// root property is false, then throw an "NotSupportedError" DOMException. // root property is false, then throw an "NotSupportedError" DOMException.
// 5. TODO(masonfreed): If shadow host has a non-null shadow root whose is if (GetShadowRoot() && !GetShadowRoot()->IsDeclarativeShadowRoot()) {
// 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()) {
return "Shadow root cannot be created on a host " return "Shadow root cannot be created on a host "
"which already hosts a shadow tree."; "which already hosts a shadow tree.";
} }
...@@ -3784,6 +3781,15 @@ ShadowRoot& Element::AttachShadowRootInternal( ...@@ -3784,6 +3781,15 @@ ShadowRoot& Element::AttachShadowRootInternal(
GetDocument().SetShadowCascadeOrder(ShadowCascadeOrder::kShadowCascadeV1); 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 // 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. // node document, host is shadow host, and mode is mode.
// 9. Set shadow host’s shadow root to shadow. // 9. Set shadow host’s shadow root to shadow.
......
...@@ -942,13 +942,19 @@ bool HTMLTreeBuilder::ProcessTemplateEndTag(AtomicHTMLToken* token) { ...@@ -942,13 +942,19 @@ bool HTMLTreeBuilder::ProcessTemplateEndTag(AtomicHTMLToken* token) {
tree_.ActiveFormattingElements()->ClearToLastMarker(); tree_.ActiveFormattingElements()->ClearToLastMarker();
template_insertion_modes_.pop_back(); template_insertion_modes_.pop_back();
ResetInsertionModeAppropriately(); ResetInsertionModeAppropriately();
// Check for a declarative shadow root.
if (RuntimeEnabledFeatures::DeclarativeShadowDOMEnabled() && if (RuntimeEnabledFeatures::DeclarativeShadowDOMEnabled() &&
template_stack_item) { template_stack_item) {
DCHECK(template_stack_item->IsElementNode()); DCHECK(template_stack_item->IsElementNode());
HTMLTemplateElement* template_element = HTMLTemplateElement* template_element =
DynamicTo<HTMLTemplateElement>(template_stack_item->GetElement()); 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);
DCHECK(shadow_host_stack_item->IsElementNode()); DCHECK(shadow_host_stack_item->IsElementNode());
UseCounter::Count(shadow_host_stack_item->GetElement()->GetDocument(), UseCounter::Count(shadow_host_stack_item->GetElement()->GetDocument(),
......
<!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>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment