Skip to content

Commit

Permalink
Add ability to clone a template containing declarative Shadow DOM
Browse files Browse the repository at this point in the history
Prior to this CL, this would not work correctly:

<template id=target>
  <div>
    <template shadowroot=open>Content</template>
  </div>
</template>

<script>
  document.body.appendChild(target.content.cloneNode(true));
</script>

The inner declarative Shadow DOM (<template shadowroot>) would not get
cloned from the template content to the clone. With this CL, it now
works correctly. Several tests were added to test nested template
and declarative Shadow DOM nodes.

This CL mirrors the DOM spec changes made in [1].

[1] whatwg/dom#858

Bug: 1042130
Change-Id: I05792e038dc694ac00d13531c657afaed754f747
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2175062
Reviewed-by: Kouhei Ueno <[email protected]>
Commit-Queue: Mason Freed <[email protected]>
Auto-Submit: Mason Freed <[email protected]>
Cr-Commit-Position: refs/heads/master@{#766528}
  • Loading branch information
mfreed7 authored and Commit Bot committed May 7, 2020
1 parent fd7ffc5 commit eb7a199
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 21 deletions.
6 changes: 4 additions & 2 deletions third_party/blink/renderer/core/dom/container_node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1047,9 +1047,11 @@ bool ContainerNode::ChildrenChangedAllChildrenRemovedNeedsList() const {
return false;
}

void ContainerNode::CloneChildNodesFrom(const ContainerNode& node) {
void ContainerNode::CloneChildNodesFrom(const ContainerNode& node,
CloneChildrenFlag flag) {
DCHECK_NE(flag, CloneChildrenFlag::kSkip);
for (const Node& child : NodeTraversal::ChildrenOf(node))
AppendChild(child.Clone(GetDocument(), CloneChildrenFlag::kClone));
AppendChild(child.Clone(GetDocument(), flag));
}

PhysicalRect ContainerNode::BoundingBox() const {
Expand Down
2 changes: 1 addition & 1 deletion third_party/blink/renderer/core/dom/container_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class CORE_EXPORT ContainerNode : public Node {
void RemoveChildren(
SubtreeModificationAction = kDispatchSubtreeModifiedEvent);

void CloneChildNodesFrom(const ContainerNode&);
void CloneChildNodesFrom(const ContainerNode&, CloneChildrenFlag);

void AttachLayoutTree(AttachContext&) override;
void DetachLayoutTree(bool performing_reattach = false) override;
Expand Down
4 changes: 2 additions & 2 deletions third_party/blink/renderer/core/dom/document.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5020,8 +5020,8 @@ Node* Document::Clone(Document& factory, CloneChildrenFlag flag) const {
<< "Document::Clone() doesn't support importNode mode.";
Document* clone = CloneDocumentWithoutChildren();
clone->CloneDataFromDocument(*this);
if (flag == CloneChildrenFlag::kClone)
clone->CloneChildNodesFrom(*this);
if (flag != CloneChildrenFlag::kSkip)
clone->CloneChildNodesFrom(*this, flag);
return clone;
}

Expand Down
4 changes: 2 additions & 2 deletions third_party/blink/renderer/core/dom/document_fragment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ bool DocumentFragment::ChildTypeAllowed(NodeType type) const {

Node* DocumentFragment::Clone(Document& factory, CloneChildrenFlag flag) const {
DocumentFragment* clone = Create(factory);
if (flag == CloneChildrenFlag::kClone)
clone->CloneChildNodesFrom(*this);
if (flag != CloneChildrenFlag::kSkip)
clone->CloneChildNodesFrom(*this, flag);
return clone;
}

Expand Down
38 changes: 33 additions & 5 deletions third_party/blink/renderer/core/dom/element.cc
Original file line number Diff line number Diff line change
Expand Up @@ -631,8 +631,36 @@ bool Element::IsFocusableStyle() const {
}

Node* Element::Clone(Document& factory, CloneChildrenFlag flag) const {
return flag == CloneChildrenFlag::kClone ? &CloneWithChildren(&factory)
: &CloneWithoutChildren(&factory);
if (flag == CloneChildrenFlag::kSkip)
return &CloneWithoutChildren(&factory);
Element* copy = &CloneWithChildren(&factory);
// 7. If node is a shadow host and the clone shadows flag is set, run these
// steps:
if (flag == CloneChildrenFlag::kCloneWithShadows) {
DCHECK(RuntimeEnabledFeatures::DeclarativeShadowDOMEnabled());
auto* shadow_root = GetShadowRoot();
if (shadow_root && (shadow_root->GetType() == ShadowRootType::kOpen ||
shadow_root->GetType() == ShadowRootType::kClosed)) {
// 7.1 Run attach a shadow root with shadow host equal to copy, mode equal
// to node’s shadow root’s mode, and delegates focus equal to node’s
// shadow root’s delegates focus.
ShadowRoot& cloned_shadow_root = copy->AttachShadowRootInternal(
shadow_root->GetType(),
shadow_root->delegatesFocus() ? FocusDelegation::kDelegateFocus
: FocusDelegation::kNone,
shadow_root->GetSlotAssignmentMode());
// 7.2 If node’s shadow root’s "is declarative shadow root" is true, then
// set copy’s shadow root’s "is declarative shadow root" property to true.
cloned_shadow_root.SetIsDeclarativeShadowRoot(
shadow_root->IsDeclarativeShadowRoot());
// 7.3 If the clone children flag is set, clone all the children of node’s
// shadow root and append them to copy’s shadow root, with document as
// specified, the clone children flag being set, and the clone shadows
// flag being set.
cloned_shadow_root.CloneChildNodesFrom(*shadow_root, flag);
}
}
return copy;
}

Element& Element::CloneWithChildren(Document* nullable_factory) const {
Expand All @@ -644,7 +672,7 @@ Element& Element::CloneWithChildren(Document* nullable_factory) const {

clone.CloneAttributesFrom(*this);
clone.CloneNonAttributePropertiesFrom(*this, CloneChildrenFlag::kClone);
clone.CloneChildNodesFrom(*this);
clone.CloneChildNodesFrom(*this, CloneChildrenFlag::kClone);
return clone;
}

Expand Down Expand Up @@ -3601,8 +3629,8 @@ const char* Element::ErrorMessageForAttachShadow() const {
}
}

// 4. If shadow host has a non-null shadow root whose is declarative shadow
// root property is false, then throw an "NotSupportedError" DOMException.
// 4. If shadow host has a non-null shadow root whose "is declarative shadow
// root" property is false, then throw an "NotSupportedError" DOMException.
if (GetShadowRoot() && !GetShadowRoot()->IsDeclarativeShadowRoot()) {
return "Shadow root cannot be created on a host "
"which already hosts a shadow tree.";
Expand Down
16 changes: 11 additions & 5 deletions third_party/blink/renderer/core/dom/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -966,18 +966,24 @@ void Node::remove() {
Node* Node::cloneNode(bool deep, ExceptionState& exception_state) const {
// https://dom.spec.whatwg.org/#dom-node-clonenode

// 1. If context object is a shadow root, then throw a
// "NotSupportedError" DOMException.
// 1. If this is a shadow root, then throw a "NotSupportedError" DOMException.
if (IsShadowRoot()) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"ShadowRoot nodes are not clonable.");
return nullptr;
}

// 2. Return a clone of the context object, with the clone children
// flag set if deep is true.
// 2. Return a clone of this, with the clone children flag set if deep is
// true, and the clone shadows flag set if this is a DocumentFragment whose
// host is an HTML template element.
auto* fragment = DynamicTo<DocumentFragment>(this);
bool clone_shadows_flag =
fragment && fragment->IsTemplateContent() &&
RuntimeEnabledFeatures::DeclarativeShadowDOMEnabled();
return Clone(GetDocument(),
deep ? CloneChildrenFlag::kClone : CloneChildrenFlag::kSkip);
deep ? (clone_shadows_flag ? CloneChildrenFlag::kCloneWithShadows
: CloneChildrenFlag::kClone)
: CloneChildrenFlag::kSkip);
}

Node* Node::cloneNode(bool deep) const {
Expand Down
2 changes: 1 addition & 1 deletion third_party/blink/renderer/core/dom/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ enum class SlotChangeType {
kSuppressSlotChangeEvent,
};

enum class CloneChildrenFlag { kClone, kSkip };
enum class CloneChildrenFlag { kSkip, kClone, kCloneWithShadows };

// Whether or not to force creation of a legacy layout object (i.e. disallow
// LayoutNG).
Expand Down
2 changes: 2 additions & 0 deletions third_party/blink/renderer/core/dom/shadow_root.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ class CORE_EXPORT ShadowRoot final : public DocumentFragment, public TreeScope {
}

void SetIsDeclarativeShadowRoot(bool flag) {
DCHECK(!flag || GetType() == ShadowRootType::kOpen ||
GetType() == ShadowRootType::kClosed);
is_declarative_shadow_root_ = flag;
}
bool IsDeclarativeShadowRoot() const { return is_declarative_shadow_root_; }
Expand Down
4 changes: 2 additions & 2 deletions third_party/blink/renderer/core/html/html_template_element.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ void HTMLTemplateElement::CloneNonAttributePropertiesFrom(
CloneChildrenFlag flag) {
if (flag == CloneChildrenFlag::kSkip)
return;

DCHECK_NE(flag, CloneChildrenFlag::kCloneWithShadows);
auto& html_template_element = To<HTMLTemplateElement>(source);
if (html_template_element.content())
content()->CloneChildNodesFrom(*html_template_element.content());
content()->CloneChildNodesFrom(*html_template_element.content(), flag);
}

void HTMLTemplateElement::DidMoveToNewDocument(Document& old_document) {
Expand Down
3 changes: 2 additions & 1 deletion third_party/blink/renderer/core/html/track/vtt/vtt_cue.cc
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,8 @@ VTTCueBox* VTTCue::GetDisplayTree() {
CreateVTTNodeTree();

cue_background_box_->RemoveChildren();
cue_background_box_->CloneChildNodesFrom(*vtt_node_tree_);
cue_background_box_->CloneChildNodesFrom(*vtt_node_tree_,
CloneChildrenFlag::kClone);

if (!region()) {
VTTDisplayParameters display_parameters = CalculateDisplayParameters();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,76 @@

</script>

<template id="template-containing-shadow">
<div class="innerdiv">
<template shadowroot=open>Content</template>
</div>
</template>
<script>
test(() => {
const template = document.querySelector('#template-containing-shadow');
const host1 = document.createElement('div');
document.body.appendChild(host1);
host1.appendChild(template.content.cloneNode(true));
let innerDiv = host1.querySelector('div.innerdiv');
const shadowRoot1 = innerDiv.shadowRoot;
assert_true(!!shadowRoot1,"Inner div should have a shadow root");
assert_equals(innerDiv.querySelector('template'), null, "No leftover template node");

const host2 = document.createElement('div');
document.body.appendChild(host2);
host2.appendChild(template.content.cloneNode(true));
innerDiv = host2.querySelector('div.innerdiv');
const shadowRoot2 = innerDiv.shadowRoot;
assert_true(!!shadowRoot2,"Inner div should have a shadow root");
assert_equals(innerDiv.querySelector('template'), null, "No leftover template node");

assert_not_equals(shadowRoot1,shadowRoot2,'Should not get back the same shadow root');
}, 'Declarative Shadow DOM: template containing declarative shadow root');
</script>

<template id="template-containing-ua-shadow">
<div class="innerdiv">
<template shadowroot=open>
<video></video> <!--Assumed to have UA shadow root-->
</template>
</div>
</template>
<script>
test(() => {
const template = document.querySelector('#template-containing-ua-shadow');
const host = document.createElement('div');
document.body.appendChild(host);
// Mostly make sure clone of template *does* clone the
// shadow root, and doesn't crash on cloning the <video>.
host.appendChild(template.content.cloneNode(true));
let innerDiv = host.querySelector('div.innerdiv');
const shadowRoot = innerDiv.shadowRoot;
assert_true(!!shadowRoot,"Inner div should have a shadow root");
assert_equals(innerDiv.querySelector('template'), null, "No leftover template node");
assert_true(!!innerDiv.shadowRoot.querySelector('video'),'Video element should be present');
}, 'Declarative Shadow DOM: template containing declarative shadow root and UA shadow root');
</script>

<template id="template-containing-ua-shadow-closed">
<div class="innerdiv">
<template shadowroot=closed>
<video></video> <!--Assumed to have UA shadow root-->
</template>
</div>
</template>
<script>
test(() => {
const template = document.querySelector('#template-containing-ua-shadow-closed');
const host = document.createElement('div');
document.body.appendChild(host);
host.appendChild(template.content.cloneNode(true));
let innerDiv = host.querySelector('div.innerdiv');
// TODO(masonfreed): once ElementInternals.shadowRoot exists, check for it here.
assert_true(!innerDiv.shadowRoot,"Inner div should have a closed shadow root");
}, 'Declarative Shadow DOM: template containing closed declarative shadow root and UA shadow root');
</script>

<template id="root-element-shadow">
<template shadowroot=open>Content</template>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
assert_equals(n.content, null, 'Declarative template content should *still* be null');
assert_equals(n.innerHTML, "", 'Declarative template innerHTML should *still* be empty');
assert_equals(n.getInnerHTML({includeShadowRoots: true}), "", 'Declarative template getInnerHTML() should *still* be empty');

// Try cloning the in-progress declarative template - shouldn't work.
const clone = n.cloneNode(true);
assert_equals(clone.children.length,0,'Clone should not contain anything');
assert_equals(clone.content.children.length,0,'Clone should not contain anything');
break;
case 'noroot':
// Make sure adding 'shadowroot' attribute doesn't trigger a shadow root,
Expand Down

0 comments on commit eb7a199

Please sign in to comment.