Make it work better to activate/inactivate checkboxes and radio buttons like the other browsers

First, the test compares `document.querySelector(":active")` and a checkbox or
a radio button.  However, Gecko activates ancestor elements too.  Therefore,
Gecko returns `<html>` element or `null` for the selector.  However, this is
an issue of CSS pseudo class compatibility which is **not** scope of the test.
Therefore, this patch makes it compare `document.querySelector("input:active")`
and a checkbox or a radio button instead.

Next, Gecko does not activate checkboxes and radio buttons when user presses
the space key, but the other browsers do it.  Therefore, this patch makes
`HTMLInputElement::PostHandleEvent` do it and
`EventStateManager::PostHandleEvent` clear it at `keyup` of the space key.

Next, Gecko does not inactive active elements when it gets lost focus.
Therefore, this patch makes `nsFocusManager::NotifyFocusStateChange` do it
like the other browsers.

Finally, with manual testing of draft patches, I found some issues of
inactivating them when they are activated by `<label>`s.  Therefore, I add new
tests in a separate file.  Note that Chrome fails only in the cases testing
`<input type="radio">` with `<label>`.

Differential Revision: https://phabricator.services.mozilla.com/D160494

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1795620
gecko-commit: e64ab51c2c29192b7880d8fd03acf2313dcf0e8a
gecko-reviewers: smaug
diff --git a/html/semantics/forms/the-input-element/checkable-active-onblur-with-click.html b/html/semantics/forms/the-input-element/checkable-active-onblur-with-click.html
new file mode 100644
index 0000000..2d5d008
--- /dev/null
+++ b/html/semantics/forms/the-input-element/checkable-active-onblur-with-click.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<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>
+<style>
+* {
+  font-size: 20px;
+}
+</style>
+</head>
+<body>
+
+<!-- This behavior is not explicitly specified. -->
+
+<input type=checkbox id=cb1 checked> <label for=cb1>ghi</label>
+<input type=radio id=r1 checked> <label for=r1>jkl</label>
+<label id=lc>abc <input type=checkbox id=cb2 checked></label>
+<label id=lr>def <input type=radio id=r2 checked></label>
+
+<script>
+promise_test(async () => {
+  await new Promise(resolve => {
+    addEventListener("load", resolve, { once: true });
+  });
+}, "Wait for load");
+
+const tabKey = "\uE004";
+promise_test(async t => {
+  const checkbox = document.querySelector("input[type=checkbox]");
+  // pointerdown on the checkbox
+  await (new test_driver.Actions()
+    .pointerMove(2, 2, { origin: checkbox })
+    .pointerDown())
+    .send();
+  t.add_cleanup(async () => {
+    // Release the pointer
+    await (new test_driver.Actions().pointerUp()).send();
+  });
+  assert_equals(document.querySelector("input:active"), checkbox,
+    "Checkboxes should be :active while it is pressed");
+
+  // Press tab
+  await (new test_driver.Actions().keyDown(tabKey).keyUp(tabKey)).send();
+  assert_equals(document.querySelector(":active"), null,
+    "Checkboxes should not be :active after tab is used to change focus.");
+}, "Checkboxes should clear :active when the user tabs away from them while pressing it with a pointing device");
+
+promise_test(async t => {
+  const radio = document.querySelector("input[type=radio]");
+  // pointerdown on the radio
+  await (new test_driver.Actions()
+    .pointerMove(2, 2, { origin: radio })
+    .pointerDown())
+    .send();
+  t.add_cleanup(async () => {
+    // Release the pointer
+    await (new test_driver.Actions().pointerUp()).send();
+  });
+  assert_equals(document.querySelector("input:active"), radio,
+    "Radios should be :active while it is pressed");
+
+  // Press tab
+  await (new test_driver.Actions().keyDown(tabKey).keyUp(tabKey)).send();
+  assert_equals(document.querySelector(":active"), null,
+    "Radios should not be :active after tab is used to change focus.");
+}, "Radios should clear :active when the user tabs away from them while pressing it with a pointing device");
+
+promise_test(async t => {
+  const checkbox = document.querySelector("label > input[type=checkbox]");
+  const label = checkbox.parentElement;
+  // pointerdown on the label
+  await (new test_driver.Actions()
+    .pointerMove(2, 2, { origin: label })
+    .pointerDown())
+    .send();
+  t.add_cleanup(async () => {
+    // Release the pointer
+    await (new test_driver.Actions().pointerUp()).send();
+  });
+  assert_equals(document.querySelector("input:active"), checkbox,
+    "Checkboxes should be :active while the label is pressed");
+
+  // Press tab
+  await (new test_driver.Actions().keyDown(tabKey).keyUp(tabKey)).send();
+  assert_equals(document.querySelector(":active"), null,
+    "Checkboxes should not be :active after tab is used to change focus.");
+}, "Checkboxes should clear :active when the user tabs away from them while pressing the parent label with a pointing device");
+
+promise_test(async t => {
+  const radio = document.querySelector("label > input[type=radio]");
+  const label = radio.parentElement;
+  const radioRect = radio.getBoundingClientRect();
+  const labelRect = label.getBoundingClientRect();
+  // pointerdown on the label
+  await (new test_driver.Actions()
+    .pointerMove(2, 2, { origin: label })
+    .pointerDown())
+    .send();
+  t.add_cleanup(async () => {
+    // Release the pointer
+    await (new test_driver.Actions().pointerUp()).send();
+  });
+  assert_equals(document.querySelector("input:active"), radio,
+    "Radios should be :active while the label is pressed");
+
+  // Press tab
+  await (new test_driver.Actions().keyDown(tabKey).keyUp(tabKey)).send();
+  assert_equals(document.querySelector(":active"), null,
+    "Radios should not be :active after tab is used to change focus.");
+}, "Radios should clear :active when the user tabs away from them while pressing the parent label with a pointing device");
+
+promise_test(async t => {
+  const label = document.querySelector("label[for=cb1]");
+  // pointerdown on the label
+  await (new test_driver.Actions()
+    .pointerMove(2, 2, { origin: label })
+    .pointerDown())
+    .send();
+  t.add_cleanup(async () => {
+    // Release the pointer
+    await (new test_driver.Actions().pointerUp()).send();
+  });
+  assert_equals(document.querySelector("input:active"), label.control,
+    "Checkboxes should be :active while the label is pressed");
+
+  // Press tab
+  await (new test_driver.Actions().keyDown(tabKey).keyUp(tabKey)).send();
+  assert_equals(document.querySelector(":active"), null,
+    "Checkboxes should not be :active after tab is used to change focus.");
+}, "Checkboxes should clear :active when the user tabs away from them while pressing the following label with a pointing device");
+
+promise_test(async t => {
+  const label = document.querySelector("label[for=r1]");
+  // pointerdown on the label
+  await (new test_driver.Actions()
+    .pointerMove(2, 2, { origin: label })
+    .pointerDown())
+    .send();
+  t.add_cleanup(async () => {
+    // Release the pointer
+    await (new test_driver.Actions().pointerUp()).send();
+  });
+  assert_equals(document.querySelector("input:active"), label.control,
+    "Radios should be :active while the label is pressed");
+
+  // Press tab
+  await (new test_driver.Actions().keyDown(tabKey).keyUp(tabKey)).send();
+  assert_equals(document.querySelector(":active"), null,
+    "Radios should not be :active after tab is used to change focus.");
+}, "Radios should clear :active when the user tabs away from them while pressing the following label with a pointing device");
+</script>
+</body>
+</html>
diff --git a/html/semantics/forms/the-input-element/checkable-active-onblur.html b/html/semantics/forms/the-input-element/checkable-active-onblur.html
index d409017..cc88996 100644
--- a/html/semantics/forms/the-input-element/checkable-active-onblur.html
+++ b/html/semantics/forms/the-input-element/checkable-active-onblur.html
@@ -14,37 +14,39 @@
 <input type=radio id=radio checked>
 
 <script>
-promise_test(async () => {
+promise_test(async t => {
   checkbox.focus();
 
   // Hold spacebar down
   await (new test_driver.Actions()).keyDown('\uE00D').send();
-  assert_equals(document.querySelector(':active'), checkbox,
+  t.add_cleanup(async () => {
+    // Release spacebar
+    await (new test_driver.Actions()).keyUp('\uE00D').send();
+  });
+  assert_equals(document.querySelector('input:active'), checkbox,
     'Checkboxes should be :active while the spacebar is pressed down.');
 
   // Press tab
   await (new test_driver.Actions()).keyDown('\uE004').keyUp('\uE004').send();
   assert_equals(document.querySelector(':active'), null,
     'Checkboxes should not be :active after tab is used to change focus.');
-
-  // Release spacebar
-  await (new test_driver.Actions()).keyUp('\uE00D').send();
 }, 'Checkboxes should clear :active when the user tabs away from them while holding spacebar.');
 
-promise_test(async () => {
+promise_test(async t => {
   radio.focus();
 
   // Hold spacebar down
   await (new test_driver.Actions()).keyDown('\uE00D').send();
-  assert_equals(document.querySelector(':active'), radio,
+  t.add_cleanup(async () => {
+    // Release spacebar
+    await (new test_driver.Actions()).keyUp('\uE00D').send();
+  });
+  assert_equals(document.querySelector('input:active'), radio,
     'Radios should be :active while the spacebar is pressed down.');
 
   // Press tab
   await (new test_driver.Actions()).keyDown('\uE004').keyUp('\uE004').send();
   assert_equals(document.querySelector(':active'), null,
     'Radios should not be :active after tab is used to change focus.');
-
-  // Release spacebar
-  await (new test_driver.Actions()).keyUp('\uE00D').send();
 }, 'Radios should clear :active when the user tabs away from them while holding spacebar.');
 </script>
diff --git a/html/semantics/forms/the-input-element/checkable-active-space-key-being-disabled.html b/html/semantics/forms/the-input-element/checkable-active-space-key-being-disabled.html
new file mode 100644
index 0000000..5f725b8
--- /dev/null
+++ b/html/semantics/forms/the-input-element/checkable-active-space-key-being-disabled.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Tests active state of checkbox/radio when pressing space key but it's disabled by a keydown event listener</title>
+<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>
+</head>
+<body>
+<input type="checkbox">
+<input type="radio">
+<script>
+const spaceKey = "\uE00D";
+
+function disableTarget(event) {
+  event.target.disabled = true;
+}
+
+// If a `keydown` event listener disables the event target, default event
+// handler in browser shouldn't activate the disabled element.  Otherwise,
+// the browser loses a chance to inactivate the disabled element because
+// it won't get keyup events until it's enabled again.
+
+promise_test(async t => {
+  const checkbox = document.querySelector("input[type=checkbox]");
+  checkbox.focus();
+  checkbox.addEventListener("keydown", disableTarget);
+  await (new test_driver.Actions()).keyDown(spaceKey).send();
+  let released = false;
+  t.add_cleanup(async () => {
+    if (!released) {
+      await (new test_driver.Actions()).keyUp(spaceKey).send();
+    }
+    checkbox.removeEventListener("keydown", disableTarget);
+    checkbox.remove();
+  });
+  test(() => {
+    assert_equals(
+      document.querySelector("input:active"),
+      null,
+      "The checkbox shouldn't be activated"
+    );
+  }, "Space key press shouldn't activate the disabled checkbox");
+
+  await (new test_driver.Actions()).keyUp(spaceKey).send();
+  released = true;
+
+  assert_equals(
+    document.querySelector("input:active"),
+    null,
+    "The disabled checkbox should be inactivated even if activated accidentally"
+  );
+}, "Space key shouldn't active the checkbox when it's disabled by a keydown event listener");
+
+promise_test(async t => {
+  const radio = document.querySelector("input[type=radio]");
+  radio.focus();
+  radio.addEventListener("keydown", disableTarget);
+  await (new test_driver.Actions()).keyDown(spaceKey).send();
+  let released = false;
+  t.add_cleanup(async () => {
+    if (!released) {
+      await (new test_driver.Actions()).keyUp(spaceKey).send();
+    }
+    radio.removeEventListener("keydown", disableTarget);
+    radio.disabled = false;
+  });
+  test(() => {
+    assert_equals(
+      document.querySelector("input:active"),
+      null,
+      "The radio shouldn't be activated"
+    );
+  }, "Space key press shouldn't activate the disabled radio");
+
+  await (new test_driver.Actions()).keyUp(spaceKey).send();
+  released = true;
+
+  assert_equals(
+    document.querySelector("input:active"),
+    null,
+    "The disabled radio should be inactivated even if it's accidentally activated"
+  );
+}, "Space key shouldn't active the radio when it's disabled by a keydown event listener");
+</script>
+</body>
+</html>
diff --git a/html/semantics/forms/the-input-element/checkable-active-space-key-prevented-default.html b/html/semantics/forms/the-input-element/checkable-active-space-key-prevented-default.html
new file mode 100644
index 0000000..877cd70
--- /dev/null
+++ b/html/semantics/forms/the-input-element/checkable-active-space-key-prevented-default.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Tests active state of checkbox/radio when pressing space key but its default prevented</title>
+<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>
+</head>
+<body>
+<input type="checkbox">
+<input type="radio">
+<script>
+const spaceKey = "\uE00D";
+
+function preventDefault(event) {
+  event.preventDefault();
+}
+
+promise_test(async t => {
+  const checkbox = document.querySelector("input[type=checkbox]");
+  checkbox.focus();
+  checkbox.addEventListener("keydown", preventDefault);
+  await (new test_driver.Actions()).keyDown(spaceKey).send();
+  t.add_cleanup(async () => {
+    await (new test_driver.Actions()).keyUp(spaceKey).send();
+    checkbox.removeEventListener("keydown", preventDefault);
+  });
+  assert_equals(
+    document.querySelector("input:active"),
+    null,
+    "The checkbox shouldn't be activated"
+  );
+}, "Space key shouldn't active the checkbox when its default is prevented");
+
+promise_test(async t => {
+  const radio = document.querySelector("input[type=radio]");
+  radio.focus();
+  radio.addEventListener("keydown", preventDefault);
+  await (new test_driver.Actions()).keyDown(spaceKey).send();
+  t.add_cleanup(async () => {
+    await (new test_driver.Actions()).keyUp(spaceKey).send();
+    radio.removeEventListener("keydown", preventDefault);
+  });
+  assert_equals(
+    document.querySelector("input:active"),
+    null,
+    "The radio shouldn't be activated"
+  );
+}, "Space key shouldn't active the radio when its default is prevented");
+</script>
+</body>
+</html>
diff --git a/html/semantics/forms/the-input-element/checkable-active-space-key-untrusted-event.html b/html/semantics/forms/the-input-element/checkable-active-space-key-untrusted-event.html
new file mode 100644
index 0000000..190757d
--- /dev/null
+++ b/html/semantics/forms/the-input-element/checkable-active-space-key-untrusted-event.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Tests active state of checkbox/radio when pressing space key emulated with untrusted key events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<input type="checkbox">
+<input type="radio">
+<script>
+function sendSpaceKeyEvent(eventType, target) {
+  const eventData = { keyCode: 32, which: 32, key: " ", code: "Space"};
+  const spaceKeyEvent = new KeyboardEvent(eventType, eventData);
+  target.dispatchEvent(spaceKeyEvent);
+}
+
+test(t => {
+  const checkbox = document.querySelector("input[type=checkbox]");
+  checkbox.focus();
+  sendSpaceKeyEvent("keydown", checkbox);
+  t.add_cleanup(() => {
+    sendSpaceKeyEvent("keyup", checkbox);
+  });
+  assert_equals(
+    document.querySelector("input:active"),
+    null,
+    "The checkbox shouldn't be activated"
+  );
+}, "Space key shouldn't active the checkbox when space key press is emulated by untrusted events");
+
+test(t => {
+  const radio = document.querySelector("input[type=radio]");
+  radio.focus();
+  sendSpaceKeyEvent("keydown", radio);
+  t.add_cleanup(() => {
+    sendSpaceKeyEvent("keyup", radio);
+  });
+  assert_equals(
+    document.querySelector("input:active"),
+    null,
+    "The radio shouldn't be activated"
+  );
+}, "Space key shouldn't active the radio when space key press is emulated by untrusted events");
+</script>
+</body>
+</html>