[Invokers] Reland: Add HandleInvokeInternal on HTMLDialogElement (#45289)

(This relands
https://chromium-review.googlesource.com/c/chromium/src/+/4987322 after fixing timeout issues in
https://chromium-review.googlesource.com/c/chromium/src/+/5372478)

This adds support for the experimental `invoketarget` behavior for
HTMLDialogElement.

See explainer section here:
https://open-ui.org/components/invokers.explainer/#defaults.

This introduces new behavior for HTMLDialogElements such that:

 - If an `invoketarget` points to a `<dialog>` (and the parent logic
   fell through):
    - If the `invokeaction` is `` (empty - the auto state) try to toggle the dialog as modal.
    - If the `invokeaction` is `close`, try to close the dialog
    - If the `invokeaction` is `showModal`, try to show the dialog as
      modal.

Bug: 1494810
Change-Id: Ic178239b8597df8746d9b4cde8f2efe53290e3ea
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5389304
Commit-Queue: Keith Cirkel <chromium@keithcirkel.co.uk>
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1280317}

Co-authored-by: Keith Cirkel <chromium@keithcirkel.co.uk>
diff --git a/html/semantics/invokers/invoketarget-on-dialog-behavior.tentative.html b/html/semantics/invokers/invoketarget-on-dialog-behavior.tentative.html
new file mode 100644
index 0000000..9f73e09
--- /dev/null
+++ b/html/semantics/invokers/invoketarget-on-dialog-behavior.tentative.html
@@ -0,0 +1,394 @@
+<!doctype html>
+<meta charset="utf-8" />
+<meta name="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" />
+<meta name="timeout" content="long">
+<link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/invoker-utils.js"></script>
+
+<dialog id="invokee">
+  <button id="containedinvoker" invoketarget="invokee"></button>
+</dialog>
+<button id="invokerbutton" invoketarget="invokee"></button>
+
+<script>
+  function resetState() {
+    invokee.close();
+    try { invokee.hidePopover(); } catch {}
+    invokee.removeAttribute("popover");
+    invokerbutton.removeAttribute("invokeaction");
+    containedinvoker.removeAttribute("invokeaction");
+  }
+
+  // opening a dialog
+
+  [null, "", "showmodal", /* test case sensitivity */ "sHoWmOdAl"].forEach(
+    (action) => {
+      ["property", "attribute"].forEach((setType) => {
+        promise_test(
+          async function (t) {
+            t.add_cleanup(resetState);
+            assert_false(invokee.open, "invokee.open");
+            assert_false(invokee.matches(":modal"), "invokee :modal");
+            if (typeof action === "string") {
+              if (setType === "property") {
+                invokerbutton.invokeaction = action;
+              } else {
+                invokerbutton.setAttribute("invokeaction", action);
+              }
+            }
+            await clickOn(invokerbutton);
+            assert_true(invokee.open, "invokee.open");
+            assert_true(invokee.matches(":modal"), "invokee :modal");
+          },
+          `invoking (with invokeaction ${setType} as ${
+            action == null ? "auto" : action || "explicit empty"
+          }) closed dialog opens as modal`,
+        );
+
+        promise_test(
+          async function (t) {
+            t.add_cleanup(resetState);
+            assert_false(invokee.open, "invokee.open");
+            assert_false(invokee.matches(":modal"), "invokee :modal");
+            invokee.addEventListener("invoke", (e) => e.preventDefault(), {
+              once: true,
+            });
+            if (typeof action === "string") {
+              if (setType === "property") {
+                invokerbutton.invokeaction = action;
+              } else {
+                invokerbutton.setAttribute("invokeaction", action);
+              }
+            }
+            await clickOn(invokerbutton);
+            assert_false(invokee.open, "invokee.open");
+            assert_false(invokee.matches(":modal"), "invokee :modal");
+          },
+          `invoking (with invokeaction ${setType} as ${
+            action == null ? "auto" : action || "explicit empty"
+          }) closed dialog with preventDefault is noop`,
+        );
+
+        promise_test(
+          async function (t) {
+            t.add_cleanup(resetState);
+            assert_false(invokee.open, "invokee.open");
+            assert_false(invokee.matches(":modal"), "invokee :modal");
+            invokee.addEventListener(
+              "invoke",
+              (e) => {
+                invokerbutton.setAttribute("invokeaction", "close");
+              },
+              { once: true },
+            );
+            if (typeof action === "string") {
+              if (setType === "property") {
+                invokerbutton.invokeaction = action;
+              } else {
+                invokerbutton.setAttribute("invokeaction", action);
+              }
+            }
+            await clickOn(invokerbutton);
+            assert_true(invokee.open, "invokee.open");
+            assert_true(invokee.matches(":modal"), "invokee :modal");
+          },
+          `invoking (with invokeaction ${setType} as ${
+            action == null ? "auto" : action || "explicit empty"
+          }) while changing action still opens as modal`,
+        );
+      });
+    },
+  );
+
+  // closing an already open dialog
+
+  [null, "", "close", /* test case sensitivity */ "cLoSe"].forEach((action) => {
+    ["property", "attribute"].forEach((setType) => {
+      promise_test(
+        async function (t) {
+          t.add_cleanup(resetState);
+          invokee.show();
+          assert_true(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+          if (typeof action === "string") {
+            if (setType === "property") {
+              containedinvoker.invokeaction = action;
+            } else {
+              containedinvoker.setAttribute("invokeaction", action);
+            }
+          }
+          await clickOn(containedinvoker);
+          assert_false(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+        },
+        `invoking to close (with invokeaction ${setType} as ${
+          action == null ? "auto" : action || "explicit empty"
+        }) open dialog closes`,
+      );
+
+      promise_test(
+        async function (t) {
+          t.add_cleanup(resetState);
+          invokee.show();
+          assert_true(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+          if (typeof action === "string") {
+            if (setType === "property") {
+              containedinvoker.invokeaction = action;
+            } else {
+              containedinvoker.setAttribute("invokeaction", action);
+            }
+          }
+          invokee.addEventListener("invoke", (e) => e.preventDefault(), {
+            once: true,
+          });
+          await clickOn(containedinvoker);
+          assert_true(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+        },
+        `invoking to close (with invokeaction ${setType} as ${
+          action == null ? "auto" : action || "explicit empty"
+        }) open dialog with preventDefault is no-op`,
+      );
+
+      promise_test(
+        async function (t) {
+          t.add_cleanup(resetState);
+          invokee.showModal();
+          assert_true(invokee.open, "invokee.open");
+          assert_true(invokee.matches(":modal"), "invokee :modal");
+          if (typeof action === "string") {
+            if (setType === "property") {
+              containedinvoker.invokeaction = action;
+            } else {
+              containedinvoker.setAttribute("invokeaction", action);
+            }
+          }
+          invokee.addEventListener("invoke", (e) => e.preventDefault(), {
+            once: true,
+          });
+          await clickOn(containedinvoker);
+          assert_true(invokee.open, "invokee.open");
+          assert_true(invokee.matches(":modal"), "invokee :modal");
+        },
+        `invoking to close (with invokeaction ${setType} as ${
+          action == null ? "auto" : action || "explicit empty"
+        }) open modal dialog with preventDefault is no-op`,
+      );
+
+      promise_test(
+        async function (t) {
+          t.add_cleanup(resetState);
+          invokee.show();
+          assert_true(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+          if (typeof action === "string") {
+            if (setType === "property") {
+              containedinvoker.invokeaction = action;
+            } else {
+              containedinvoker.setAttribute("invokeaction", action);
+            }
+          }
+          invokee.addEventListener(
+            "invoke",
+            (e) => {
+              containedinvoker.setAttribute("invokeaction", "show");
+            },
+            { once: true },
+          );
+          await clickOn(containedinvoker);
+          assert_false(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+        },
+        `invoking to close (with invokeaction ${setType} as ${
+          action == null ? "auto" : action || "explicit empty"
+        }) open dialog while changing action still closes`,
+      );
+
+      promise_test(
+        async function (t) {
+          t.add_cleanup(resetState);
+          invokee.showModal();
+          assert_true(invokee.open, "invokee.open");
+          assert_true(invokee.matches(":modal"), "invokee :modal");
+          if (typeof action === "string") {
+            if (setType === "property") {
+              containedinvoker.invokeaction = action;
+            } else {
+              containedinvoker.setAttribute("invokeaction", action);
+            }
+          }
+          invokee.addEventListener(
+            "invoke",
+            (e) => {
+              containedinvoker.setAttribute("invokeaction", "show");
+            },
+            { once: true },
+          );
+          await clickOn(containedinvoker);
+          assert_false(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+        },
+        `invoking to close (with invokeaction ${setType} as ${
+          action == null ? "auto" : action || "explicit empty"
+        }) open modal dialog while changing action still closes`,
+      );
+    });
+  });
+
+  // showmodal explicit behaviours
+
+  promise_test(async function (t) {
+    t.add_cleanup(resetState);
+    containedinvoker.setAttribute("invokeaction", "showModal");
+    invokee.show();
+    assert_true(invokee.open, "invokee.open");
+    assert_false(invokee.matches(":modal"), "invokee :modal");
+    await clickOn(containedinvoker);
+    assert_true(invokee.open, "invokee.open");
+    assert_false(invokee.matches(":modal"), "invokee :modal");
+  }, "invoking (as showmodal) open dialog is noop");
+
+  promise_test(async function (t) {
+    t.add_cleanup(resetState);
+    containedinvoker.setAttribute("invokeaction", "showmodal");
+    invokee.showModal();
+    assert_true(invokee.open, "invokee.open");
+    assert_true(invokee.matches(":modal"), "invokee :modal");
+    invokee.addEventListener(
+      "invoke",
+      (e) => {
+        containedinvoker.setAttribute("invokeaction", "close");
+      },
+      { once: true },
+    );
+    await clickOn(invokerbutton);
+    assert_true(invokee.open, "invokee.open");
+    assert_true(invokee.matches(":modal"), "invokee :modal");
+  }, "invoking (as showmodal) open modal, while changing action still a no-op");
+
+  promise_test(async function (t) {
+    t.add_cleanup(resetState);
+    invokerbutton.setAttribute("invokeaction", "showmodal");
+    assert_false(invokee.open, "invokee.open");
+    assert_false(invokee.matches(":modal"), "invokee :modal");
+    invokee.setAttribute("popover", "auto");
+    await clickOn(invokerbutton);
+    assert_true(invokee.open, "invokee.open");
+    assert_true(invokee.matches(":modal"), "invokee :modal");
+  }, "invoking (as showmodal) closed popover dialog opens as modal");
+
+  // close explicit behaviours
+
+  promise_test(async function (t) {
+    t.add_cleanup(resetState);
+    invokerbutton.setAttribute("invokeaction", "close");
+    assert_false(invokee.open, "invokee.open");
+    assert_false(invokee.matches(":modal"), "invokee :modal");
+    await clickOn(containedinvoker);
+    assert_false(invokee.open, "invokee.open");
+    assert_false(invokee.matches(":modal"), "invokee :modal");
+  }, "invoking (as close) already closed dialog is noop");
+
+  // Open Popovers using Dialog actions
+  ["showmodal", "close", ""].forEach((action) => {
+    ["manual", "auto"].forEach((popoverState) => {
+      promise_test(
+        async function (t) {
+          t.add_cleanup(resetState);
+          invokee.setAttribute("popover", popoverState);
+          invokee.showPopover();
+          containedinvoker.setAttribute("invokeaction", action);
+          assert_true(
+            invokee.matches(":popover-open"),
+            "invokee :popover-open",
+          );
+          assert_false(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+          invokee.addEventListener("invoke", (e) => e.preventDefault(), {
+            once: true,
+          });
+          await clickOn(containedinvoker);
+          assert_true(
+            invokee.matches(":popover-open"),
+            "invokee :popover-open",
+          );
+          assert_false(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+        },
+        `invoking (as ${
+          action || "explicit empty"
+        }) dialog as open popover=${popoverState} is noop`,
+      );
+    });
+  });
+
+  // Elements being disconnected during invoke steps
+  ["showmodal", "close", ""].forEach((action) => {
+    promise_test(
+      async function (t) {
+        t.add_cleanup(() => {
+          document.body.prepend(invokee);
+          resetState();
+        });
+        const invokee = document.querySelector("#invokee");
+        invokerbutton.setAttribute("invokeaction", action);
+        assert_false(invokee.open, "invokee.open");
+        assert_false(invokee.matches(":modal"), "invokee :modal");
+        invokee.addEventListener(
+          "invoke",
+          (e) => {
+            invokee.remove();
+          },
+          {
+            once: true,
+          },
+        );
+        await clickOn(invokerbutton);
+        assert_false(invokee.open, "invokee.open");
+        assert_false(invokee.matches(":modal"), "invokee :modal");
+      },
+      `invoking (as ${
+        action || "explicit empty"
+      }) dialog that is removed is noop`,
+    );
+
+    promise_test(
+      async function (t) {
+        const invokerbutton = document.createElement("button");
+        invokerbutton.invokeTargetElement = invokee;
+        invokerbutton.setAttribute("invokeaction", action);
+        assert_false(invokee.open, "invokee.open");
+        assert_false(invokee.matches(":modal"), "invokee :modal");
+        await clickOn(invokerbutton);
+        assert_false(invokee.open, "invokee.open");
+        assert_false(invokee.matches(":modal"), "invokee :modal");
+      },
+      `invoking (as ${
+        action || "explicit empty"
+      }) dialog from a detached invoker`,
+    );
+
+    promise_test(
+      async function (t) {
+        const invokerbutton = document.createElement("button");
+        const invokee = document.createElement("dialog");
+        invokerbutton.invokeTargetElement = invokee;
+        invokerbutton.setAttribute("invokeaction", action);
+        assert_false(invokee.open, "invokee.open");
+        assert_false(invokee.matches(":modal"), "invokee :modal");
+        await clickOn(invokerbutton);
+        assert_false(invokee.open, "invokee.open");
+        assert_false(invokee.matches(":modal"), "invokee :modal");
+      },
+      `invoking (as ${
+        action || "explicit empty"
+      }) detached dialog from a detached invoker`,
+    );
+  });
+</script>
diff --git a/html/semantics/invokers/invoketarget-on-dialog-invalid-behavior.tentative.html b/html/semantics/invokers/invoketarget-on-dialog-invalid-behavior.tentative.html
new file mode 100644
index 0000000..af84c22
--- /dev/null
+++ b/html/semantics/invokers/invoketarget-on-dialog-invalid-behavior.tentative.html
@@ -0,0 +1,120 @@
+<!doctype html>
+<meta charset="utf-8" />
+<meta name="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" />
+<meta name="timeout" content="long">
+<link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/invoker-utils.js"></script>
+
+<dialog id="invokee">
+  <button id="containedinvoker" invoketarget="invokee"></button>
+</dialog>
+<button id="invokerbutton" invoketarget="invokee"></button>
+
+<script>
+  function resetState() {
+    invokee.close();
+    try { invokee.hidePopover(); } catch {}
+    invokee.removeAttribute("popover");
+    invokerbutton.removeAttribute("invokeaction");
+    containedinvoker.removeAttribute("invokeaction");
+  }
+
+  // invalid
+  [
+    "foo",
+    "foo-bar",
+    "auto",
+    "showpopover",
+    "hidepopover",
+    "togglepopover",
+    "showpicker",
+  ].forEach((action) => {
+    promise_test(async function (t) {
+      t.add_cleanup(resetState);
+      invokerbutton.setAttribute("invokeaction", action);
+      assert_false(invokee.open, "invokee.open");
+      assert_false(invokee.matches(":modal"), "invokee :modal");
+      await clickOn(invokerbutton);
+      assert_false(invokee.open, "invokee.open");
+      assert_false(invokee.matches(":modal"), "invokee :modal");
+    }, `invoking (as ${action}) on dialog does nothing`);
+
+    promise_test(async function (t) {
+      t.add_cleanup(resetState);
+      containedinvoker.setAttribute("invokeaction", action);
+      invokee.show();
+      assert_true(invokee.open, "invokee.open");
+      assert_false(invokee.matches(":modal"), "invokee :modal");
+      await clickOn(containedinvoker);
+      assert_true(invokee.open, "invokee.open");
+      assert_false(invokee.matches(":modal"), "invokee :modal");
+    }, `invoking (as ${action}) on open dialog does nothing`);
+
+    promise_test(async function (t) {
+      t.add_cleanup(resetState);
+      containedinvoker.setAttribute("invokeaction", action);
+      invokee.showModal();
+      assert_true(invokee.open, "invokee.open");
+      assert_true(invokee.matches(":modal"), "invokee :modal");
+      await clickOn(containedinvoker);
+      assert_true(invokee.open, "invokee.open");
+      assert_true(invokee.matches(":modal"), "invokee :modal");
+    }, `invoking (as ${action}) on open modal dialog does nothing`);
+
+    promise_test(async function (t) {
+      t.add_cleanup(resetState);
+      containedinvoker.setAttribute("invokeaction", action);
+      invokee.showModal();
+      assert_true(invokee.open, "invokee.open");
+      assert_true(invokee.matches(":modal"), "invokee :modal");
+      invokee.addEventListener(
+        "invoke",
+        (e) => {
+          containedinvoker.setAttribute("invokeaction", "");
+        },
+        { once: true },
+      );
+      await clickOn(containedinvoker);
+      assert_true(invokee.open, "invokee.open");
+      assert_true(invokee.matches(":modal"), "invokee :modal");
+    }, `invoking (as ${action}) on open modal while changing the attributer does nothing`);
+  });
+
+  // Open Popovers using Dialog actions
+  ["showmodal", "close", ""].forEach((action) => {
+    ["manual", "auto"].forEach((popoverState) => {
+      promise_test(
+        async function (t) {
+          t.add_cleanup(resetState);
+          invokee.setAttribute("popover", popoverState);
+          invokee.showPopover();
+          containedinvoker.setAttribute("invokeaction", action);
+          assert_true(
+            invokee.matches(":popover-open"),
+            "invokee :popover-open",
+          );
+          assert_false(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+          invokee.addEventListener("invoke", (e) => e.preventDefault(), {
+            once: true,
+          });
+          await clickOn(containedinvoker);
+          assert_true(
+            invokee.matches(":popover-open"),
+            "invokee :popover-open",
+          );
+          assert_false(invokee.open, "invokee.open");
+          assert_false(invokee.matches(":modal"), "invokee :modal");
+        },
+        `invoking (as ${
+          action || "explicit empty"
+        }) dialog as open popover=${popoverState} is noop`,
+      );
+    });
+  });
+</script>
diff --git a/html/semantics/invokers/resources/invoker-utils.js b/html/semantics/invokers/resources/invoker-utils.js
index 3179455..8420f24 100644
--- a/html/semantics/invokers/resources/invoker-utils.js
+++ b/html/semantics/invokers/resources/invoker-utils.js
@@ -2,9 +2,13 @@
   return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
 }
 async function clickOn(element) {
-  const actions = new test_driver.Actions();
   await waitForRender();
-  await actions.pointerMove(0, 0, {origin: element})
+  let rect = element.getBoundingClientRect();
+  let actions = new test_driver.Actions();
+  // FIXME: Switch to pointerMove(0, 0, {origin: element}) once
+  // https://github.com/web-platform-tests/wpt/issues/41257 is fixed.
+  await actions
+      .pointerMove(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2), {})
       .pointerDown({button: actions.ButtonType.LEFT})
       .pointerUp({button: actions.ButtonType.LEFT})
       .send();