ElementReflection: ariaActiveDescendant, ariaErrorMessage, ariaDetails

Spec: https://whatpr.org/html/3917/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:element

This change:
	- updates v8 bindings and IDL
	- Implements (most) of the HTML spec for the three attributes
	  listed above.

This change is part of the experimental AOM project, and allows attributes that
reflect ARIA relationships to return an element reference corresponding to the
ID stored in the content attribute, rather than the string ID itself. These
attributes are exposed on the IDL interface AriaAttributes.
See https://rawgit.com/w3c/aria/master/#AriaAttributes for more information.

Attributes reflecting multiple element references will be implemented using
FrozenArray<Element> in a follow up CL.

BUG=981423

Change-Id: I35164b436c7e2ffd67a80ebd26e4233189e445b3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1687013
Commit-Queue: Meredith Lane <meredithl@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Reviewed-by: Alice Boxhall <aboxhall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#691047}
diff --git a/dom/nodes/aria-element-reflection.tentative.html b/dom/nodes/aria-element-reflection.tentative.html
new file mode 100644
index 0000000..974727f
--- /dev/null
+++ b/dom/nodes/aria-element-reflection.tentative.html
@@ -0,0 +1,266 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>Element Reflection for aria-activedescendant and aria-errormessage</title>
+    <link rel=help href="https://whatpr.org/html/3917/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:element">
+    <link rel="author" title="Meredith Lane" href="meredithl@chromium.org">
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+  </head>
+  <div id="activedescendant" aria-activedescendant="x"></div>
+
+  <div id="parent-listbox" role="listbox" aria-activedescendant="i1">
+    <div role="option" id="i1">Item 1</div>
+    <div role="option" id="i2">Item 2</div>
+  </div>
+
+  <script>
+  test(function(t) {
+    const ancestor = document.getElementById("parent-listbox");
+    assert_equals(activedescendant.ariaActiveDescendantElement, null,
+                  "invalid ID for relationship returns null");
+
+    const descendant1 = document.getElementById("i1");
+    const descendant2 = document.getElementById("i2");
+
+    // Element reference should be set if the content attribute was included.
+    assert_equals(ancestor.getAttribute("aria-activedescendant"), "i1", "check content attribute after parsing.");
+    assert_equals(ancestor.ariaActiveDescendantElement, i1, "check idl attribute after parsing.");
+
+    // If we set the content attribute, the element reference should reflect this.
+    ancestor.setAttribute("aria-activedescendant", "i2");
+    assert_equals(ancestor.ariaActiveDescendantElement, descendant2, "setting the content attribute updates the element reference.");
+
+    // Setting the element reference should be reflected in the content attribute.
+    ancestor.ariaActiveDescendantElement = descendant1;
+    assert_equals(ancestor.ariaActiveDescendantElement, descendant1, "getter should return the right element reference.");
+    assert_equals(ancestor.getAttribute("aria-activedescendant"), "i1", "content attribute should reflect the element reference.");
+
+    // Both content and IDL attribute should be nullable.
+    ancestor.ariaActiveDescendantElement = null;
+    assert_equals(ancestor.ariaActiveDescendantElement, null);
+    assert_false(ancestor.hasAttribute("aria-activedescendant"));
+    assert_equals(ancestor.getAttribute("aria-activedescendant"), null, "nullifying the idl attribute removes the content attribute.");
+
+    // Setting content attribute to non-existent or non compatible element should nullify the IDL attribute.
+    // Reset the element to an existant one.
+    ancestor.setAttribute("aria-activedescendant", "i1");
+    assert_equals(ancestor.ariaActiveDescendantElement, i1, "reset attribute.");
+
+    ancestor.setAttribute("aria-activedescendant", "non-existent-element");
+    assert_equals(ancestor.getAttribute("aria-activedescendant"), "non-existent-element");
+    assert_equals(ancestor.ariaActiveDescendantElement, null,"non-DOM content attribute should null the element reference");
+  }, "aria-activedescendant element reflection");
+  </script>
+
+  <div id="parent-listbox-2" role="listbox" aria-activedescendant="option1">
+    <div role="option" id="option1">Item 1</div>
+    <div role="option" id="option2">Item 2</div>
+  </div>
+
+  <script>
+  test(function(t) {
+    const ancestor = document.getElementById("parent-listbox-2");
+    const option1 = document.getElementById("option1");
+    const option2 = document.getElementById("option2");
+    assert_equals(ancestor.ariaActiveDescendantElement, option1);
+
+    option1.removeAttribute("id");
+    option2.setAttribute("id", "option1");
+    const option2Duplicate = document.getElementById("option1");
+    assert_equals(option2, option2Duplicate);
+
+    assert_equals(ancestor.ariaActiveDescendantElement, option2);
+  }, "If the content attribute is set directly, the IDL attribute getter always returns the first element whose ID matches the content attribute.");
+  </script>
+
+  <div id="blank-id-parent" role="listbox">
+    <div role="option" id="multiple-id"></div>
+    <div role="option" id="multiple-id"></div>
+  </div>
+
+  <script>
+  test(function(t) {
+    const ancestor = document.getElementById("blank-id-parent");
+
+    // Get second child of parent. This violates the setting of a reflected element
+    // as it will not be the first child of the parent with that ID, which should
+    // result in an empty string for the content attribute.
+    ancestor.ariaActiveDescendantElement = ancestor.children[1];
+    assert_true(ancestor.hasAttribute("aria-activedescendant"));
+    assert_equals(ancestor.getAttribute("aria-activedescendant"), "");
+    assert_equals(ancestor.ariaActiveDescendantElement, ancestor.children[1]);
+  }, "Setting the IDL attribute to an element which is not the first element in DOM order with its ID causes the content attribute to be an empty string");
+  </script>
+
+  <div id="outer-container">
+    <p id="light-paragraph">Hello world!</p>
+    <span class="shadow-host">
+    </span>
+  </div>
+
+  <script>
+  test(function(t) {
+    const shadowElement = document.querySelector(".shadow-host");
+    const shadow = shadowElement.attachShadow({mode: "open"});
+    const link = document.createElement("a");
+    shadow.appendChild(link);
+
+    const lightPara = document.getElementById("light-paragraph");
+    assert_equals(lightPara.ariaActiveDescendantElement, null);
+
+    // The given element crosses a shadow dom boundary, so it cannot be
+    // set as an element reference.
+    lightPara.ariaActiveDescendantElement = link;
+    assert_equals(lightPara.ariaActiveDescendantElement, null);
+
+    // The given element crosses a shadow dom boundary (upwards), so
+    // can be used as an element reference, but the content attribute
+    // should reflect the empty string.
+    link.ariaActiveDescendantElement = lightPara;
+    assert_equals(link.ariaActiveDescendantElement, lightPara);
+    assert_equals(link.getAttribute("aria-activedescendant"), "");
+  }, "Setting an element reference that crosses into a shadow tree is disallowed, but setting one that is in a shadow inclusive ancestor is allowed.");
+  </script>
+
+  <input id="startTime" ></input>
+  <span id="errorMessage">Invalid Time</span>
+
+  <script>
+  test(function(t) {
+    const inputElement = document.getElementById("startTime");
+    const errorMessage = document.getElementById("errorMessage");
+
+    inputElement.ariaErrorMessageElement = errorMessage;
+    assert_equals(inputElement.getAttribute("aria-errormessage"), "errorMessage");
+
+    inputElement.ariaErrorMessageElement = null;
+    assert_equals(inputElement.ariaErrorMessageElement, null, "blah");
+    assert_false(inputElement.hasAttribute("aria-errormessage"));
+
+    inputElement.setAttribute("aria-errormessage", "errorMessage");
+    assert_equals(inputElement.ariaErrorMessageElement, errorMessage);
+
+  }, "aria-errormessage");
+
+  </script>
+
+  <label>
+    Password:
+    <input id="password-field" type="password" aria-details="pw">
+  </label>
+
+  <ul>
+    <li id="list-item-1">First description.</li>
+    <li id="list-item-2">Second description.</li>
+  </ul>
+
+  <script>
+
+  test(function(t) {
+    const inputElement = document.getElementById("password-field");
+    const ul1 = document.getElementById("list-item-1");
+    const ul2 = document.getElementById("list-item-2");
+
+    assert_equals(inputElement.ariaDetailsElement, null);
+    inputElement.ariaDetailsElement = ul1;
+    assert_equals(inputElement.getAttribute("aria-details"), "list-item-1");
+
+    inputElement.ariaDetailsElement = ul2;
+    assert_equals(inputElement.getAttribute("aria-details"), "list-item-2");
+  }, "aria-details");
+  </script>
+
+  <div id="old-parent" role="listbox" aria-activedescendant="content-attr-element">
+    <div role="option" id="content-attr-element">Item 1</div>
+    <div role="option" id="idl-attr-element">Item 2</div>
+  </div>
+
+  <script>
+
+  test(function(t) {
+    const deletionParent = document.getElementById("old-parent");
+
+    const descendant1 = document.getElementById("content-attr-element");
+    const descendant2 = document.getElementById("idl-attr-element");
+
+    // Deleting an element set via the content attribute.
+    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "content-attr-element");
+    assert_equals(deletionParent.ariaActiveDescendantElement, descendant1);
+
+    deletionParent.removeChild(descendant1);
+    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "content-attr-element");
+    assert_equals(deletionParent.ariaActiveDescendantElement, null);
+
+    // Deleting an element set via the IDL attribute.
+    deletionParent.ariaActiveDescendantElement = descendant2;
+    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "idl-attr-element");
+
+    deletionParent.removeChild(descendant2);
+    assert_equals(deletionParent.ariaActiveDescendantElement, null);
+
+    // The content attribute will still reflect the id.
+    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "idl-attr-element");
+  }, "Deleting a reflected element should return null for the IDL attribute and cause the content attribute to become stale.");
+  </script>
+
+  <div id="parent" role="listbox" aria-activedescendant="original">
+    <div role="option" id="original">Item 1</div>
+    <div role="option" id="element-with-persistant-id">Item 2</div>
+  </div>
+
+  <script>
+  test(function(t) {
+    const parentNode = document.getElementById("parent");
+    const changingIdElement = document.getElementById("original");
+    const persistantIDElement = document.getElementById("element-with-persistant-id");
+
+    assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement);
+
+    // Modify the id attribute.
+    changingIdElement.setAttribute("id", "new-id");
+
+    // The content attribute still reflects the old id, and we expect the
+    // Element reference to be null as there is no DOM node with id "original"
+    assert_equals(parentNode.getAttribute("aria-activedescendant"), "original");
+    assert_equals(parentNode.ariaActiveDescendantElement, null, "Element set via content attribute with a changed id will return null on getting");
+
+    parentNode.ariaActiveDescendantElement = changingIdElement;
+    assert_equals(parentNode.getAttribute("aria-activedescendant"), "new-id");
+
+    // The explicitly set element takes precendance over the content attribute.
+    // This means that we still return the same element reference, but the
+    // content attribute reflects the old id.
+    changingIdElement.setAttribute("id", "newer-id");
+    assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement, "explicitly set element is still present even after the id has been changed");
+    assert_equals(parentNode.getAttribute("aria-activedescendant"), "new-id", "content attribute reflects the id that was present upon explicitly setting the element reference.");
+  }, "Changing the ID of an element causes the content attribute to become out of sync.");
+  </script>
+
+  <div id="light-parent" role="listbox">
+    <div role="option" id="light-element">Hello world!</div>
+  </div>
+  <div id="shadowHost"></div>
+
+  <script>
+  test(function(t) {
+    const shadowElement = document.getElementById("shadowHost");
+    const shadowHost = shadowElement.attachShadow({mode: "open"});
+    const lightParent = document.getElementById("light-parent");
+    const optionElement = document.getElementById("light-element");
+
+    lightParent.ariaActiveDescendantElement = optionElement;
+    assert_equals(lightParent.ariaActiveDescendantElement, optionElement);
+
+    // Move the referenced element into shadow DOM.
+    shadowHost.appendChild(optionElement);
+    assert_equals(lightParent.ariaActiveDescendantElement, null);
+    assert_equals(lightParent.getAttribute("aria-activedescendant"), "light-element");
+
+    // Move the referenced element back into light DOM.
+    lightParent.appendChild(optionElement);
+    assert_equals(lightParent.ariaActiveDescendantElement, optionElement);
+  }, "Reparenting an element into a descendant shadow scope nullifies the element reference.");
+  </script>
+</html>