Form-associated custom elements: Support the |anchor| argument of setValidity()

This CL follows the latest specification PR;
  https://github.com/whatwg/html/pull/4383

Add the |anchor| argument to setValidity().  ElementInternals has a data
member to store it.  ListedElement shows a validation bubble on
ValidationAnchor() result, which returns the data member if it's not
null, returns the target element otherwise.

Internals::isValidationMessageVisible() is updated so that it asks
ValidationMessageClient directly because we can show validation bubble
on any elements.

Bug: 905922
Change-Id: I20e1bdfeed6ab2635fd3f171e5160feeba170681
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1614644
Commit-Queue: Kent Tamura <tkent@chromium.org>
Reviewed-by: Hayato Ito <hayato@chromium.org>
Cr-Commit-Position: refs/heads/master@{#661295}
diff --git a/third_party/blink/renderer/core/dom/node.cc b/third_party/blink/renderer/core/dom/node.cc
index 61a9771..bb39343 100644
--- a/third_party/blink/renderer/core/dom/node.cc
+++ b/third_party/blink/renderer/core/dom/node.cc
@@ -1326,13 +1326,18 @@
   if (!node)
     return false;
 
-  if (this == node)
-    return true;
+  return this == node || IsShadowIncludingAncestorOf(*node);
+}
 
-  if (GetDocument() != node->GetDocument())
+bool Node::IsShadowIncludingAncestorOf(const Node& node) const {
+  // In the following case, contains(host) below returns true.
+  if (this == &node)
     return false;
 
-  if (isConnected() != node->isConnected())
+  if (GetDocument() != node.GetDocument())
+    return false;
+
+  if (isConnected() != node.isConnected())
     return false;
 
   auto* this_node = DynamicTo<ContainerNode>(this);
@@ -1341,9 +1346,9 @@
   if (!has_children && !has_shadow)
     return false;
 
-  for (; node; node = node->OwnerShadowHost()) {
-    if (GetTreeScope() == node->GetTreeScope())
-      return contains(node);
+  for (const Node* host = &node; host; host = host->OwnerShadowHost()) {
+    if (GetTreeScope() == host->GetTreeScope())
+      return contains(host);
   }
 
   return false;
diff --git a/third_party/blink/renderer/core/dom/node.h b/third_party/blink/renderer/core/dom/node.h
index d46e87a..c4babc4 100644
--- a/third_party/blink/renderer/core/dom/node.h
+++ b/third_party/blink/renderer/core/dom/node.h
@@ -639,7 +639,11 @@
 
   bool IsDescendantOf(const Node*) const;
   bool contains(const Node*) const;
+  // https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor
+  // TODO(tkent): The argument should be |const Node&|.
   bool IsShadowIncludingInclusiveAncestorOf(const Node*) const;
+  // https://dom.spec.whatwg.org/#concept-shadow-including-ancestor
+  bool IsShadowIncludingAncestorOf(const Node&) const;
   bool ContainsIncludingHostElements(const Node&) const;
   Node* CommonAncestor(const Node&,
                        ContainerNode* (*parent)(const Node&)) const;
diff --git a/third_party/blink/renderer/core/html/custom/element_internals.cc b/third_party/blink/renderer/core/html/custom/element_internals.cc
index bb726af..9d211c6 100644
--- a/third_party/blink/renderer/core/html/custom/element_internals.cc
+++ b/third_party/blink/renderer/core/html/custom/element_internals.cc
@@ -39,6 +39,7 @@
   visitor->Trace(value_);
   visitor->Trace(state_);
   visitor->Trace(validity_flags_);
+  visitor->Trace(validation_anchor_);
   ListedElement::Trace(visitor);
   ScriptWrappable::Trace(visitor);
 }
@@ -88,12 +89,19 @@
 
 void ElementInternals::setValidity(ValidityStateFlags* flags,
                                    ExceptionState& exception_state) {
-  setValidity(flags, String(), exception_state);
+  setValidity(flags, String(), nullptr, exception_state);
 }
 
 void ElementInternals::setValidity(ValidityStateFlags* flags,
                                    const String& message,
                                    ExceptionState& exception_state) {
+  setValidity(flags, message, nullptr, exception_state);
+}
+
+void ElementInternals::setValidity(ValidityStateFlags* flags,
+                                   const String& message,
+                                   Element* anchor,
+                                   ExceptionState& exception_state) {
   if (!IsTargetFormAssociated()) {
     exception_state.ThrowDOMException(
         DOMExceptionCode::kNotSupportedError,
@@ -109,7 +117,19 @@
         "first argument are true.");
     return;
   }
+  if (anchor && !Target().IsShadowIncludingAncestorOf(*anchor)) {
+    exception_state.ThrowDOMException(
+        DOMExceptionCode::kNotFoundError,
+        "The Element argument should be a shadow-including descendant of the "
+        "target element.");
+    return;
+  }
+
+  if (validation_anchor_ && validation_anchor_ != anchor) {
+    HideVisibleValidationMessage();
+  }
   validity_flags_ = flags;
+  validation_anchor_ = anchor;
   SetCustomValidationMessage(message);
   SetNeedsValidityCheck();
 }
@@ -157,6 +177,10 @@
   return String();
 }
 
+Element& ElementInternals::ValidationAnchor() const {
+  return validation_anchor_ ? *validation_anchor_ : Target();
+}
+
 bool ElementInternals::checkValidity(ExceptionState& exception_state) {
   if (!IsTargetFormAssociated()) {
     exception_state.ThrowDOMException(
diff --git a/third_party/blink/renderer/core/html/custom/element_internals.h b/third_party/blink/renderer/core/html/custom/element_internals.h
index fff80f92..2000c55 100644
--- a/third_party/blink/renderer/core/html/custom/element_internals.h
+++ b/third_party/blink/renderer/core/html/custom/element_internals.h
@@ -38,6 +38,10 @@
   void setValidity(ValidityStateFlags* flags,
                    const String& message,
                    ExceptionState& exception_state);
+  void setValidity(ValidityStateFlags* flags,
+                   const String& message,
+                   Element* anchor,
+                   ExceptionState& exception_state);
   bool willValidate(ExceptionState& exception_state) const;
   ValidityState* validity(ExceptionState& exception_state);
   String ValidationMessageForBinding(ExceptionState& exception_state);
@@ -66,6 +70,7 @@
   bool CustomError() const override;
   String validationMessage() const override;
   String ValidationSubMessage() const override;
+  Element& ValidationAnchor() const override;
   void DisabledStateMightBeChanged() override;
   bool ClassSupportsStateRestore() const override;
   bool ShouldSaveAndRestoreFormControlState() const override;
@@ -78,6 +83,7 @@
   ControlValue state_;
   bool is_disabled_ = false;
   Member<ValidityStateFlags> validity_flags_;
+  Member<Element> validation_anchor_;
 
   DISALLOW_COPY_AND_ASSIGN(ElementInternals);
 };
diff --git a/third_party/blink/renderer/core/html/custom/element_internals.idl b/third_party/blink/renderer/core/html/custom/element_internals.idl
index 1786ffd..438c7ad 100644
--- a/third_party/blink/renderer/core/html/custom/element_internals.idl
+++ b/third_party/blink/renderer/core/html/custom/element_internals.idl
@@ -16,7 +16,7 @@
 
   [RaisesException] readonly attribute HTMLFormElement? form;
 
-  [RaisesException] void setValidity(ValidityStateFlags flags, optional DOMString message);
+  [RaisesException] void setValidity(ValidityStateFlags flags, optional DOMString message, optional Element anchor);
   [RaisesException] readonly attribute boolean willValidate;
   [RaisesException] readonly attribute ValidityState validity;
   [RaisesException, ImplementedAs=ValidationMessageForBinding] readonly attribute DOMString validationMessage;
diff --git a/third_party/blink/renderer/core/html/forms/html_form_element.cc b/third_party/blink/renderer/core/html/forms/html_form_element.cc
index 52cc046..2ab7003 100644
--- a/third_party/blink/renderer/core/html/forms/html_form_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_form_element.cc
@@ -234,7 +234,7 @@
 
   // Focus on the first focusable control and show a validation message.
   for (const auto& unhandled : unhandled_invalid_controls) {
-    if (unhandled->ToHTMLElement().IsFocusable()) {
+    if (unhandled->ValidationAnchorOrHostIsFocusable()) {
       unhandled->ShowValidationMessage();
       UseCounter::Count(GetDocument(),
                         WebFeature::kFormValidationShowedMessage);
@@ -244,7 +244,7 @@
   // Warn about all of unfocusable controls.
   if (GetDocument().GetFrame()) {
     for (const auto& unhandled : unhandled_invalid_controls) {
-      if (unhandled->ToHTMLElement().IsFocusable())
+      if (unhandled->ValidationAnchorOrHostIsFocusable())
         continue;
       String message(
           "An invalid form control with name='%name' is not focusable.");
diff --git a/third_party/blink/renderer/core/html/forms/listed_element.cc b/third_party/blink/renderer/core/html/forms/listed_element.cc
index 82fb150..ae88649 100644
--- a/third_party/blink/renderer/core/html/forms/listed_element.cc
+++ b/third_party/blink/renderer/core/html/forms/listed_element.cc
@@ -396,14 +396,15 @@
 }
 
 void ListedElement::UpdateVisibleValidationMessage() {
-  HTMLElement& element = ToHTMLElement();
+  const Element& element = ValidationAnchor();
   Page* page = element.GetDocument().GetPage();
   if (!page || !page->IsPageVisible() || element.GetDocument().UnloadStarted())
     return;
   if (page->Paused())
     return;
   String message;
-  if (element.GetLayoutObject() && WillValidate())
+  if (element.GetLayoutObject() && WillValidate() &&
+      ToHTMLElement().IsShadowIncludingInclusiveAncestorOf(&element))
     message = validationMessage().StripWhiteSpace();
 
   has_validation_message_ = true;
@@ -426,7 +427,7 @@
     return;
 
   if (auto* client = GetValidationMessageClient())
-    client->HideValidationMessage(ToHTMLElement());
+    client->HideValidationMessage(ValidationAnchor());
 }
 
 bool ListedElement::IsValidationMessageVisible() const {
@@ -434,7 +435,7 @@
     return false;
 
   if (auto* client = GetValidationMessageClient()) {
-    return client->IsValidationMessageVisible(ToHTMLElement());
+    return client->IsValidationMessageVisible(ValidationAnchor());
   }
   return false;
 }
@@ -445,6 +446,20 @@
   return nullptr;
 }
 
+Element& ListedElement::ValidationAnchor() const {
+  return const_cast<HTMLElement&>(ToHTMLElement());
+}
+
+bool ListedElement::ValidationAnchorOrHostIsFocusable() const {
+  const Element& anchor = ValidationAnchor();
+  const HTMLElement& host = ToHTMLElement();
+  if (anchor.IsFocusable())
+    return true;
+  if (&anchor == &host)
+    return false;
+  return host.IsFocusable();
+}
+
 bool ListedElement::checkValidity(List* unhandled_invalid_controls) {
   if (IsNotCandidateOrValid())
     return true;
@@ -460,9 +475,12 @@
 }
 
 void ListedElement::ShowValidationMessage() {
-  HTMLElement& element = ToHTMLElement();
+  Element& element = ValidationAnchor();
   element.scrollIntoViewIfNeeded(false);
-  element.focus();
+  if (element.IsFocusable())
+    element.focus();
+  else
+    ToHTMLElement().focus();
   UpdateVisibleValidationMessage();
 }
 
diff --git a/third_party/blink/renderer/core/html/forms/listed_element.h b/third_party/blink/renderer/core/html/forms/listed_element.h
index c052527..1d3c8f5 100644
--- a/third_party/blink/renderer/core/html/forms/listed_element.h
+++ b/third_party/blink/renderer/core/html/forms/listed_element.h
@@ -118,6 +118,8 @@
                                                 TextDirection& message_dir,
                                                 String& sub_message,
                                                 TextDirection& sub_message_dir);
+  virtual Element& ValidationAnchor() const;
+  bool ValidationAnchorOrHostIsFocusable() const;
 
   // For Element::IsValidElement(), which is for :valid :invalid selectors.
   bool IsValidElement();
diff --git a/third_party/blink/renderer/core/testing/internals.cc b/third_party/blink/renderer/core/testing/internals.cc
index 4bab4aa..b791706 100644
--- a/third_party/blink/renderer/core/testing/internals.cc
+++ b/third_party/blink/renderer/core/testing/internals.cc
@@ -123,6 +123,7 @@
 #include "third_party/blink/renderer/core/page/scrolling/scroll_state.h"
 #include "third_party/blink/renderer/core/page/scrolling/scrolling_coordinator_context.h"
 #include "third_party/blink/renderer/core/page/spatial_navigation_controller.h"
+#include "third_party/blink/renderer/core/page/validation_message_client.h"
 #include "third_party/blink/renderer/core/page/viewport_description.h"
 #include "third_party/blink/renderer/core/paint/compositing/composited_layer_mapping.h"
 #include "third_party/blink/renderer/core/paint/compositing/graphics_layer_tree_as_text.h"
@@ -813,8 +814,10 @@
 
 bool Internals::isValidationMessageVisible(Element* element) {
   DCHECK(element);
-  if (auto* control = ListedElement::From(*element))
-    return control->IsValidationMessageVisible();
+  if (auto* page = element->GetDocument().GetPage()) {
+    return page->GetValidationMessageClient().IsValidationMessageVisible(
+        *element);
+  }
   return false;
 }
 
diff --git a/third_party/blink/web_tests/custom-elements/form-validation-bubble-anchor.html b/third_party/blink/web_tests/custom-elements/form-validation-bubble-anchor.html
new file mode 100644
index 0000000..1f66353
--- /dev/null
+++ b/third_party/blink/web_tests/custom-elements/form-validation-bubble-anchor.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+<style>
+control-element {
+  display: inline-block;
+  width: 10em;
+  height: 2em;
+}
+</style>
+<body>
+<script>
+class ControlElement extends HTMLElement {
+  static formAssociated = true;
+  constructor() {
+    super();
+    this.i = this.attachInternals();
+  }
+}
+customElements.define('control-element', ControlElement);
+
+test(() => {
+  let control = new ControlElement();
+  assert_throws('NotFoundError', () => {
+    control.i.setValidity({patternMismatch: true}, 'p', document.body);
+  }, 'not a descendant');
+  assert_throws('NotFoundError', () => {
+    control.i.setValidity({patternMismatch: true}, 'p', control);
+  }, 'self');
+  control.innerHTML = '<span>';
+  assert_throws('NotFoundError', () => {
+    control.i.setValidity({patternMismatch: true}, 'p', control);
+  }, 'self with a child');
+
+  let shadow = control.attachShadow({mode:'open'});
+  let anchor = document.createElement('div');
+  shadow.appendChild(anchor);
+  control.i.setValidity({badInput: true}, 'b', anchor);
+}, 'setValidity() should throw if the anchor argument is not a shadow-including-descendant of the target');
+
+async_test(t => {
+  assert_own_property(window, 'internals');
+  document.body.insertAdjacentHTML('afterbegin', '<control-element tabindex=0><input><button></button></control-element>');
+  let control = document.body.querySelector('control-element');
+  let innerField = control.querySelector('input');
+  control.i.setValidity({tooLong: true}, 'Too long', innerField);
+  control.i.reportValidity();
+  assert_false(internals.isValidationMessageVisible(control));
+  assert_true(internals.isValidationMessageVisible(innerField));
+  assert_equals(document.activeElement, innerField);
+
+  let innerButton = control.querySelector('button');
+  control.i.setValidity({tooLong: true}, 'Too long', innerButton);
+  // Validation bubble closes if a different anchor is set.
+  assert_false(internals.isValidationMessageVisible(innerField));
+  assert_false(internals.isValidationMessageVisible(innerButton));
+
+  control.i.reportValidity();
+  assert_false(internals.isValidationMessageVisible(innerField));
+  assert_true(internals.isValidationMessageVisible(innerButton));
+  assert_equals(document.activeElement, innerButton);
+
+  innerButton.blur();
+  requestAnimationFrame(t.step_func_done(() => {
+    assert_false(internals.isValidationMessageVisible(innerField));
+    assert_false(internals.isValidationMessageVisible(innerButton));
+  }));
+}, 'Validation bubble is shown on the anchor element, and removing focus ' +
+    'from the anchor closes the validation bubble.');
+</script>
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/custom-elements/form-associated/ElementInternals-validation-expected.txt b/third_party/blink/web_tests/external/wpt/custom-elements/form-associated/ElementInternals-validation-expected.txt
index d0b11b3..e3e31aa 100644
--- a/third_party/blink/web_tests/external/wpt/custom-elements/form-associated/ElementInternals-validation-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/custom-elements/form-associated/ElementInternals-validation-expected.txt
@@ -1,9 +1,7 @@
 This is a testharness.js-based test.
 PASS willValidate
 FAIL validity and setValidity() assert_throws: setValidity() requires the second argument if the first argument contains true function "() => { control.i.setValidity({valueMissing: true}); }" threw object "TypeMismatchError: Failed to execute 'setValidity' on 'ElementInternals': The second argument should not be empty if one or more flags in the first argument are true." ("TypeMismatchError") expected object "TypeError" ("TypeError")
-FAIL "anchor" argument of setValidity() assert_throws: Not a descendant function "() => {
-    control.i.setValidity(flags, m, document.body);
-  }" did not throw
+PASS "anchor" argument of setValidity()
 PASS checkValidity()
 PASS reportValidity()
 PASS Custom control affects validation at the owner form