Reland "Update popover post-toggle event naming and behavior"

This is a reland of commit 2e5ee120d7fc5df4b6c101e88d03a5f62a73d8b8

It seems that the event coalescing behavior changes cause a segfault
in the event dispatch code. This reland (compare to Patchset 1) just
includes the event renaming, and not the new behavior. I'll land
the new behavior in a separate CL to make it smaller if it gets
reverted again. This part should be safe.

Original change's description:
> Update popover post-toggle event naming and behavior
>
> This CL updates the post-toggle event in the following ways:
>  1. Rename the 'aftertoggle' event to 'toggle'.
>  2. Rename PopoverToggleEvent to ToggleEvent.
>  3. Rename the currentState attribute to oldState.
>  4. Add event coalescing behavior. If two transitions occur before the
>     first 'toggle' event has been fired, cancel the first event and
>     queue a replacement that has oldState === newState.
>
> These changes were driven by the corresponding changes to the spec PR:
>   https://github.com/whatwg/html/pull/8717
>
> Bug: 1307772
> Change-Id: Iabc5a9093d7cef3bbd6e54e488d8e571c51ea568
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4195120
> Auto-Submit: Mason Freed <masonf@chromium.org>
> Commit-Queue: Joey Arhar <jarhar@chromium.org>
> Reviewed-by: Joey Arhar <jarhar@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1098728}

Bug: 1307772
Change-Id: Iede18bbf05516cec4ee881f1afc663c45380c82e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4205710
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Commit-Queue: Joey Arhar <jarhar@chromium.org>
Auto-Submit: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1099397}
diff --git a/html/semantics/popovers/idlharness.tentative.html b/html/semantics/popovers/idlharness.tentative.html
index d1a258b..1cab891 100644
--- a/html/semantics/popovers/idlharness.tentative.html
+++ b/html/semantics/popovers/idlharness.tentative.html
@@ -41,12 +41,12 @@
         'document.getElementById("b3")',
       ],
       BeforeToggleEvent: [
-        'new PopoverToggleEvent("beforetoggle")',
-        'new PopoverToggleEvent("beforetoggle", {currentState: "open"})',
-        'new PopoverToggleEvent("beforetoggle", {currentState: "open",newState: "open"})',
-        'new PopoverToggleEvent("aftertoggle")',
-        'new PopoverToggleEvent("aftertoggle", {currentState: "open"})',
-        'new PopoverToggleEvent("aftertoggle", {currentState: "open",newState: "open"})',
+        'new ToggleEvent("beforetoggle")',
+        'new ToggleEvent("beforetoggle", {oldState: "open"})',
+        'new ToggleEvent("beforetoggle", {oldState: "open",newState: "open"})',
+        'new ToggleEvent("toggle")',
+        'new ToggleEvent("toggle", {oldState: "open"})',
+        'new ToggleEvent("toggle", {oldState: "open",newState: "open"})',
       ],
     });
   }
diff --git a/html/semantics/popovers/popover-events.tentative.html b/html/semantics/popovers/popover-events.tentative.html
index b96a0f5..b299424 100644
--- a/html/semantics/popovers/popover-events.tentative.html
+++ b/html/semantics/popovers/popover-events.tentative.html
@@ -10,6 +10,13 @@
 <div popover>Popover</div>
 
 <script>
+function getPopoverAndSignal(t) {
+  const popover = document.querySelector('[popover]');
+  const controller = new AbortController();
+  const signal = controller.signal;
+  t.add_cleanup(() => controller.abort());
+  return {popover, signal};
+}
 window.onload = () => {
   for(const method of ["listener","attribute"]) {
     promise_test(async t => {
@@ -22,52 +29,48 @@
       function listener(e) {
         if (e.type === "beforetoggle") {
           if (e.newState === "open") {
-            assert_equals(e.currentState,"closed",'The "beforetoggle" event should be fired before the popover is open');
+            ++showCount;
+            assert_equals(e.oldState,"closed",'The "beforetoggle" event should be fired before the popover is open');
             assert_true(e.target.matches(':closed'),'The popover should be in the :closed state when the opening event fires.');
             assert_false(e.target.matches(':open'),'The popover should *not* be in the :open state when the opening event fires.');
-            ++showCount;
           } else {
+            ++hideCount;
             assert_equals(e.newState,"closed",'Popover toggleevent states should be "open" and "closed"');
-            assert_equals(e.currentState,"open",'The "beforetoggle" event should be fired before the popover is closed')
+            assert_equals(e.oldState,"open",'The "beforetoggle" event should be fired before the popover is closed')
             assert_true(e.target.matches(':open'),'The popover should be in the :open state when the hiding event fires.');
             assert_false(e.target.matches(':closed'),'The popover should *not* be in the :closed state when the hiding event fires.');
-            ++hideCount;
           }
         } else {
-          assert_equals(e.type,"aftertoggle",'Popover events should be "beforetoggle" and "aftertoggle"')
+          assert_equals(e.type,"toggle",'Popover events should be "beforetoggle" and "toggle"')
           if (e.newState === "open") {
-            assert_equals(e.currentState,"open",'Aftertoggle should be fired after the popover is open');
+            ++afterShowCount;
             if (document.body.contains(e.target)) {
               assert_true(e.target.matches(':open'),'The popover should be in the :open state when the after opening event fires.');
               assert_false(e.target.matches(':closed'),'The popover should *not* be in the :closed state when the after opening event fires.');
             }
-            ++afterShowCount;
           } else {
+            ++afterHideCount;
             assert_equals(e.newState,"closed",'Popover toggleevent states should be "open" and "closed"');
-            assert_equals(e.currentState,"closed",'Aftertoggle should be fired after the popover is closed');
             assert_true(e.target.matches(':closed'),'The popover should be in the :closed state when the after hiding event fires.');
             assert_false(e.target.matches(':open'),'The popover should *not* be in the :open state when the after hiding event fires.');
-            ++afterHideCount;
           }
-          e.preventDefault(); // "aftertoggle" should not be cancelable.
+          e.preventDefault(); // "toggle" should not be cancelable.
         }
       };
       switch (method) {
         case "listener":
-          const controller = new AbortController();
-          const signal = controller.signal;
-          t.add_cleanup(() => controller.abort());
+          const {signal} = getPopoverAndSignal(t);
           // These events bubble.
           document.addEventListener('beforetoggle', listener, {signal});
-          document.addEventListener('aftertoggle', listener, {signal});
+          document.addEventListener('toggle', listener, {signal});
           break;
         case "attribute":
           assert_false(popover.hasAttribute('onbeforetoggle'));
           t.add_cleanup(() => popover.removeAttribute('onbeforetoggle'));
           popover.onbeforetoggle = listener;
-          assert_false(popover.hasAttribute('onaftertoggle'));
-          t.add_cleanup(() => popover.removeAttribute('onaftertoggle'));
-          popover.onaftertoggle = listener;
+          assert_false(popover.hasAttribute('ontoggle'));
+          t.add_cleanup(() => popover.removeAttribute('ontoggle'));
+          popover.ontoggle = listener;
           break;
         default: assert_unreached();
       }
@@ -82,7 +85,7 @@
       assert_equals(0,afterShowCount);
       assert_equals(0,afterHideCount);
       await waitForRender();
-      assert_equals(1,afterShowCount,'aftertoggle show is fired asynchronously');
+      assert_equals(1,afterShowCount,'toggle show is fired asynchronously');
       assert_equals(0,afterHideCount);
       assert_true(popover.matches(':open'));
       popover.hidePopover();
@@ -93,7 +96,7 @@
       assert_equals(0,afterHideCount);
       await waitForRender();
       assert_equals(1,afterShowCount);
-      assert_equals(1,afterHideCount,'aftertoggle hide is fired asynchronously');
+      assert_equals(1,afterHideCount,'toggle hide is fired asynchronously');
       // No additional events
       await waitForRender();
       await waitForRender();
@@ -106,10 +109,7 @@
   }
 
   promise_test(async t => {
-    const popover = document.querySelector('[popover]');
-    const controller = new AbortController();
-    const signal = controller.signal;
-    t.add_cleanup(() => controller.abort());
+    const {popover,signal} = getPopoverAndSignal(t);
     let cancel = true;
     popover.addEventListener('beforetoggle',(e) => {
       if (e.newState !== "open")
@@ -128,10 +128,7 @@
   }, 'The "beforetoggle" event is cancelable for the "opening" transition');
 
   promise_test(async t => {
-    const popover = document.querySelector('[popover]');
-    const controller = new AbortController();
-    const signal = controller.signal;
-    t.add_cleanup(() => {controller.abort();});
+    const {popover,signal} = getPopoverAndSignal(t);
     popover.addEventListener('beforetoggle',(e) => {
       assert_not_equals(e.newState,"closed",'The "beforetoggle" event was fired for the closing transition');
     }, {signal});
@@ -144,5 +141,74 @@
     await waitForRender(); // Check for async events also
     assert_false(popover.matches(':open'));
   }, 'The "beforetoggle" event is not fired for element removal');
+
+  promise_test(async t => {
+    const {popover,signal} = getPopoverAndSignal(t);
+    let events;
+    function resetEvents() {
+      events = {
+        singleShow: false,
+        singleHide: false,
+        coalescedShow: false,
+        coalescedHide: false,
+      };
+    }
+    function setEvent(type) {
+      assert_equals(events[type],false,'event repeated');
+      events[type] = true;
+    }
+    function assertOnly(type,msg) {
+      Object.keys(events).forEach(val => {
+        assert_equals(events[val],val===type,`${msg} (${val})`);
+      });
+    }
+    popover.addEventListener('toggle',(e) => {
+      switch (e.newState) {
+        case "open":
+          switch (e.oldState) {
+            case "open": setEvent('coalescedShow'); break;
+            case "closed": setEvent('singleShow'); break;
+            default: assert_unreached();
+          }
+          break;
+        case "closed":
+          switch (e.oldState) {
+            case "closed": setEvent('coalescedHide'); break;
+            case "open": setEvent('singleHide'); break;
+            default: assert_unreached();
+          }
+          break;
+        default: assert_unreached();
+      }
+    }, {signal});
+
+    resetEvents();
+    assertOnly('none');
+    assert_false(popover.matches(':open'));
+    popover.showPopover();
+    await waitForRender();
+    assert_true(popover.matches(':open'));
+    assertOnly('singleShow','Single event should have been fired, which is a "show"');
+
+    resetEvents();
+    popover.hidePopover();
+    popover.showPopover(); // Immediate re-show
+    await waitForRender();
+    assert_true(popover.matches(':open'));
+    assertOnly('coalescedShow','Single coalesced event should have been fired, which is a "show"');
+
+    resetEvents();
+    popover.hidePopover();
+    await waitForRender();
+    assertOnly('singleHide','Single event should have been fired, which is a "hide"');
+    assert_false(popover.matches(':open'));
+
+    resetEvents();
+    popover.showPopover();
+    popover.hidePopover(); // Immediate re-hide
+    await waitForRender();
+    assertOnly('coalescedHide','Single coalesced event should have been fired, which is a "hide"');
+    assert_false(popover.matches(':open'));
+  }, 'The "toggle" event is coalesced');
 };
 </script>
diff --git a/html/semantics/popovers/toggleevent-interface.tentative.html b/html/semantics/popovers/toggleevent-interface.tentative.html
index 4d437b0..4570945 100644
--- a/html/semantics/popovers/toggleevent-interface.tentative.html
+++ b/html/semantics/popovers/toggleevent-interface.tentative.html
@@ -7,200 +7,200 @@
 
 <script>
 test(function() {
-  var event = new PopoverToggleEvent("");
-  assert_true(event instanceof window.PopoverToggleEvent);
-}, "the event is an instance of PopoverToggleEvent");
+  var event = new ToggleEvent("");
+  assert_true(event instanceof window.ToggleEvent);
+}, "the event is an instance of ToggleEvent");
 
 test(function() {
-  var event = new PopoverToggleEvent("");
+  var event = new ToggleEvent("");
   assert_true(event instanceof window.Event);
 }, "the event inherts from Event");
 
 test(function() {
   assert_throws_js(TypeError, function() {
-    new PopoverToggleEvent();
+    new ToggleEvent();
   }, 'First argument (type) is required, so was expecting a TypeError.');
 }, 'Missing type argument');
 
 test(function() {
-  var event = new PopoverToggleEvent("test");
+  var event = new ToggleEvent("test");
   assert_equals(event.type, "test");
 }, "type argument is string");
 
 test(function() {
-  var event = new PopoverToggleEvent(null);
+  var event = new ToggleEvent(null);
   assert_equals(event.type, "null");
 }, "type argument is null");
 
 test(function() {
-  var event = new PopoverToggleEvent(undefined);
+  var event = new ToggleEvent(undefined);
   assert_equals(event.type, "undefined");
 }, "event type set to undefined");
 
 test(function() {
-  var event = new PopoverToggleEvent("test");
-  assert_equals(event.currentState, "");
-}, "currentState has default value of empty string");
+  var event = new ToggleEvent("test");
+  assert_equals(event.oldState, "");
+}, "oldState has default value of empty string");
 
 test(function() {
-  var event = new PopoverToggleEvent("test");
-  assert_readonly(event, "currentState", "readonly attribute value");
-}, "currentState is readonly");
+  var event = new ToggleEvent("test");
+  assert_readonly(event, "oldState", "readonly attribute value");
+}, "oldState is readonly");
 
 test(function() {
-  var event = new PopoverToggleEvent("test");
+  var event = new ToggleEvent("test");
   assert_equals(event.newState, "");
 }, "newState has default value of empty string");
 
 test(function() {
-  var event = new PopoverToggleEvent("test");
+  var event = new ToggleEvent("test");
   assert_readonly(event, "newState", "readonly attribute value");
 }, "newState is readonly");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", null);
-  assert_equals(event.currentState, "");
+  var event = new ToggleEvent("test", null);
+  assert_equals(event.oldState, "");
   assert_equals(event.newState, "");
-}, "PopoverToggleEventInit argument is null");
+}, "ToggleEventInit argument is null");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", undefined);
-  assert_equals(event.currentState, "");
+  var event = new ToggleEvent("test", undefined);
+  assert_equals(event.oldState, "");
   assert_equals(event.newState, "");
-}, "PopoverToggleEventInit argument is undefined");
+}, "ToggleEventInit argument is undefined");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {});
-  assert_equals(event.currentState, "");
+  var event = new ToggleEvent("test", {});
+  assert_equals(event.oldState, "");
   assert_equals(event.newState, "");
-}, "PopoverToggleEventInit argument is empty dictionary");
+}, "ToggleEventInit argument is empty dictionary");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {currentState: "sample"});
-  assert_equals(event.currentState, "sample");
-}, "currentState set to 'sample'");
+  var event = new ToggleEvent("test", {oldState: "sample"});
+  assert_equals(event.oldState, "sample");
+}, "oldState set to 'sample'");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {currentState: undefined});
-  assert_equals(event.currentState, "");
-}, "currentState set to undefined");
+  var event = new ToggleEvent("test", {oldState: undefined});
+  assert_equals(event.oldState, "");
+}, "oldState set to undefined");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {currentState: null});
-  assert_equals(event.currentState, "null");
-}, "currentState set to null");
+  var event = new ToggleEvent("test", {oldState: null});
+  assert_equals(event.oldState, "null");
+}, "oldState set to null");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {currentState: false});
-  assert_equals(event.currentState, "false");
-}, "currentState set to false");
+  var event = new ToggleEvent("test", {oldState: false});
+  assert_equals(event.oldState, "false");
+}, "oldState set to false");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {currentState: true});
-  assert_equals(event.currentState, "true");
-}, "currentState set to true");
+  var event = new ToggleEvent("test", {oldState: true});
+  assert_equals(event.oldState, "true");
+}, "oldState set to true");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {currentState: 0.5});
-  assert_equals(event.currentState, "0.5");
-}, "currentState set to a number");
+  var event = new ToggleEvent("test", {oldState: 0.5});
+  assert_equals(event.oldState, "0.5");
+}, "oldState set to a number");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {currentState: []});
-  assert_equals(event.currentState, "");
-}, "currentState set to []");
+  var event = new ToggleEvent("test", {oldState: []});
+  assert_equals(event.oldState, "");
+}, "oldState set to []");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {currentState: [1, 2, 3]});
-  assert_equals(event.currentState, "1,2,3");
-}, "currentState set to [1, 2, 3]");
+  var event = new ToggleEvent("test", {oldState: [1, 2, 3]});
+  assert_equals(event.oldState, "1,2,3");
+}, "oldState set to [1, 2, 3]");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {currentState: {sample: 0.5}});
-  assert_equals(event.currentState, "[object Object]");
-}, "currentState set to an object");
+  var event = new ToggleEvent("test", {oldState: {sample: 0.5}});
+  assert_equals(event.oldState, "[object Object]");
+}, "oldState set to an object");
 
 test(function() {
-  var event = new PopoverToggleEvent("test",
-      {currentState: {valueOf: function () { return 'sample'; }}});
-  assert_equals(event.currentState, "[object Object]");
-}, "currentState set to an object with a valueOf function");
+  var event = new ToggleEvent("test",
+      {oldState: {valueOf: function () { return 'sample'; }}});
+  assert_equals(event.oldState, "[object Object]");
+}, "oldState set to an object with a valueOf function");
 
 test(function() {
-  var eventInit = {currentState: "sample",newState: "sample2"};
-  var event = new PopoverToggleEvent("test", eventInit);
-  assert_equals(event.currentState, "sample");
+  var eventInit = {oldState: "sample",newState: "sample2"};
+  var event = new ToggleEvent("test", eventInit);
+  assert_equals(event.oldState, "sample");
   assert_equals(event.newState, "sample2");
-}, "PopoverToggleEventInit properties set value");
+}, "ToggleEventInit properties set value");
 
 test(function() {
-  var eventInit = {currentState: "open",newState: "closed"};
-  var event = new PopoverToggleEvent("beforetoggle", eventInit);
-  assert_equals(event.currentState, "open");
+  var eventInit = {oldState: "open",newState: "closed"};
+  var event = new ToggleEvent("beforetoggle", eventInit);
+  assert_equals(event.oldState, "open");
   assert_equals(event.newState, "closed");
-}, "PopoverToggleEventInit properties set value 2");
+}, "ToggleEventInit properties set value 2");
 
 test(function() {
-  var eventInit = {currentState: "closed",newState: "open"};
-  var event = new PopoverToggleEvent("aftertoggle", eventInit);
-  assert_equals(event.currentState, "closed");
+  var eventInit = {oldState: "closed",newState: "open"};
+  var event = new ToggleEvent("toggle", eventInit);
+  assert_equals(event.oldState, "closed");
   assert_equals(event.newState, "open");
-}, "PopoverToggleEventInit properties set value 3");
+}, "ToggleEventInit properties set value 3");
 
 test(function() {
-  var eventInit = {currentState: "open",newState: "open"};
-  var event = new PopoverToggleEvent("beforetoggle", eventInit);
-  assert_equals(event.currentState, "open");
+  var eventInit = {oldState: "open",newState: "open"};
+  var event = new ToggleEvent("beforetoggle", eventInit);
+  assert_equals(event.oldState, "open");
   assert_equals(event.newState, "open");
-}, "PopoverToggleEventInit properties set value 4");
+}, "ToggleEventInit properties set value 4");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {newState: "sample"});
+  var event = new ToggleEvent("test", {newState: "sample"});
   assert_equals(event.newState, "sample");
 }, "newState set to 'sample'");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {newState: undefined});
+  var event = new ToggleEvent("test", {newState: undefined});
   assert_equals(event.newState, "");
 }, "newState set to undefined");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {newState: null});
+  var event = new ToggleEvent("test", {newState: null});
   assert_equals(event.newState, "null");
 }, "newState set to null");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {newState: false});
+  var event = new ToggleEvent("test", {newState: false});
   assert_equals(event.newState, "false");
 }, "newState set to false");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {newState: true});
+  var event = new ToggleEvent("test", {newState: true});
   assert_equals(event.newState, "true");
 }, "newState set to true");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {newState: 0.5});
+  var event = new ToggleEvent("test", {newState: 0.5});
   assert_equals(event.newState, "0.5");
 }, "newState set to a number");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {newState: []});
+  var event = new ToggleEvent("test", {newState: []});
   assert_equals(event.newState, "");
 }, "newState set to []");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {newState: [1, 2, 3]});
+  var event = new ToggleEvent("test", {newState: [1, 2, 3]});
   assert_equals(event.newState, "1,2,3");
 }, "newState set to [1, 2, 3]");
 
 test(function() {
-  var event = new PopoverToggleEvent("test", {newState: {sample: 0.5}});
+  var event = new ToggleEvent("test", {newState: {sample: 0.5}});
   assert_equals(event.newState, "[object Object]");
 }, "newState set to an object");
 
 test(function() {
-  var event = new PopoverToggleEvent("test",
+  var event = new ToggleEvent("test",
       {newState: {valueOf: function () { return 'sample'; }}});
   assert_equals(event.newState, "[object Object]");
 }, "newState set to an object with a valueOf function");
diff --git a/interfaces/popover.tentative.idl b/interfaces/popover.tentative.idl
index bf23c76..4e8c85f 100644
--- a/interfaces/popover.tentative.idl
+++ b/interfaces/popover.tentative.idl
@@ -13,13 +13,13 @@
 HTMLInputElement includes PopoverTargetElement;
 HTMLButtonElement includes PopoverTargetElement;
 
-interface PopoverToggleEvent : Event {
-    constructor(DOMString type, optional PopoverToggleEventInit eventInitDict = {});
-    readonly attribute DOMString currentState;
+interface ToggleEvent : Event {
+    constructor(DOMString type, optional ToggleEventInit eventInitDict = {});
+    readonly attribute DOMString oldState;
     readonly attribute DOMString newState;
 };
 
-dictionary PopoverToggleEventInit : EventInit {
-    DOMString currentState = "";
+dictionary ToggleEventInit : EventInit {
+    DOMString oldState = "";
     DOMString newState = "";
 };