summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/semantics/popovers
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/html/semantics/popovers')
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/light-dismiss-event-ordering.tentative.html82
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display-ref.tentative.html24
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html50
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html33
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-ref.tentative.html27
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html84
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-idl-property.tentative.html16
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-multicol-display.tentative.html62
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display-ref.html56
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display.tentative.html55
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-nesting.tentative.html55
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display-ref.tentative.html31
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html62
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-animated-display-ref.tentative.html26
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-cleanup.tentative.html98
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-display.tentative.html57
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-finishes-ref.tentative.html16
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-finishes.tentative.html56
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-animated-show-display.tentative.html52
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-animation-corner-cases.tentative.html230
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-appearance-ref.tentative.html18
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-appearance.tentative.html25
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-attribute-basic.tentative.html431
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance-ref.tentative.html45
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance.tentative.html46
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-beforetoggle-opening-event.tentative.html31
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance-ref.tentative.html33
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance.tentative.html26
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-dialog-crash.tentative.html25
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-document-open.tentative.html29
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-events.tentative.html91
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-focus-2.tentative.html129
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-focus-child-dialog.html45
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-focus.tentative.html286
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hidden-display-ref.tentative.html19
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hidden-display.tentative.html38
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none-ref.tentative.html5
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none.tentative.html18
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute.tentative.html215
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-on-scroll.tentative.html65
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.tentative.html493
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-manual-crash.tentative.html31
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-not-keyboard-focusable.tentative.html47
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-display-ref.tentative.html20
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-display.tentative.html26
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-ref.tentative.html22
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display.tentative.html36
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-removal-2.tentative.html28
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-removal.tentative.html27
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-shadow-dom.tentative.html168
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-stacking-context-ref.tentative.html29
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-stacking-context.tentative.html34
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-stacking.tentative.html172
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-top-layer-combinations.tentative.html150
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-top-layer-interactions.tentative.html82
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-types.tentative.html39
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/resources/popover-styles.css17
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/resources/popover-utils.js109
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/toggleevent-interface.tentative.html207
59 files changed, 4529 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/semantics/popovers/light-dismiss-event-ordering.tentative.html b/testing/web-platform/tests/html/semantics/popovers/light-dismiss-event-ordering.tentative.html
new file mode 100644
index 0000000000..4bfcecc4b0
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/light-dismiss-event-ordering.tentative.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://chromium-review.googlesource.com/c/chromium/src/+/4023021">
+<link rel=help href="https://github.com/whatwg/html/pull/8221#discussion_r1041135388">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<button id=target>target</button>
+<div id=popover popover=auto>popover</div>
+
+<script>
+for (const capture of [true, false]) {
+ for (const eventName of ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'click']) {
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ try {
+ popover.hidePopover();
+ } catch {}
+ });
+
+ popover.showPopover();
+ document.addEventListener(eventName, event => {
+ event.preventDefault();
+ }, {capture, once: true});
+ // Click away from the popover to activate light dismiss.
+ await clickOn(target);
+ assert_equals(document.querySelectorAll(':open').length, 0,
+ 'The popover should be closed via light dismiss even when preventDefault is called.');
+
+ popover.showPopover();
+ document.addEventListener(eventName, event => {
+ event.stopPropagation();
+ }, {capture, once: true});
+ // Click away from the popover to activate light dismiss.
+ await clickOn(target);
+ assert_equals(document.querySelectorAll(':open').length, 0,
+ 'The popover should be closed via light dismiss even when stopPropagation is called.');
+
+ }, `Tests the interactions between popover light dismiss and pointer/mouse events. eventName: ${eventName}, capture: ${capture}`);
+ }
+}
+
+promise_test(async t => {
+ t.add_cleanup(() => {
+ try {
+ popover.hidePopover();
+ } catch {}
+ });
+ popover.showPopover();
+
+ const expectedEvents = [
+ 'pointerdown',
+ 'mousedown',
+ 'beforetoggle newState: closed',
+ 'pointerup',
+ 'mouseup',
+ 'click'
+ ];
+ const events = [];
+
+ for (const eventName of ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'click']) {
+ document.addEventListener(eventName, () => events.push(eventName));
+ }
+ popover.addEventListener('beforetoggle', event => {
+ events.push('beforetoggle newState: ' + event.newState);
+ });
+
+ // Click away from the popover to activate light dismiss.
+ await clickOn(target);
+
+ assert_array_equals(events, expectedEvents,
+ 'pointer and popover events should be fired in the correct order.');
+
+ assert_equals(document.querySelectorAll(':open').length, 0,
+ 'The popover should be closed via light dismiss.');
+
+}, 'Tests the order of pointer/mouse events during popover light dismiss.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display-ref.tentative.html
new file mode 100644
index 0000000000..9530e7d3c4
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display-ref.tentative.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<p>There should be a green box attached to the right side of each orange box.</p>
+<div class=ex><div class=anchor></div><div class=popover></div></div>
+<div class=ex><div class=anchor></div><div class=popover></div></div>
+
+<style>
+ .ex {
+ margin: 25px;
+ font-size: 0;
+ }
+ .ex div {
+ display:inline-block;
+ width: 100px;
+ height: 100px;
+ }
+ .anchor {
+ background: orange;
+ }
+ .popover {
+ background: lime;
+ }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html
new file mode 100644
index 0000000000..a10331b2ae
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:xiaochengh@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-anchor-change-display-ref.tentative.html">
+<script src="resources/popover-utils.js"></script>
+
+<p>There should be a green box attached to the right side of each orange box.</p>
+
+<div class=ex>
+ <div class=anchor id=anchor1></div>
+ <div id=popover1 popover=manual defaultopen></div>
+</div>
+
+<div class=ex>
+ <div class=anchor id=will-be-anchor2></div>
+ <div id=popover2 popover=manual anchor=anchor2 defaultopen></div>
+</div>
+
+<script>
+showDefaultopenPopoversOnLoad();
+
+function runTest() {
+ document.body.offsetLeft; // Force layout
+
+ document.getElementById('popover1').setAttribute('anchor', 'anchor1');
+ document.getElementById('will-be-anchor2').setAttribute('id', 'anchor2');
+}
+window.addEventListener('load', runTest);
+</script>
+
+<style>
+ .ex {
+ margin: 25px;
+ }
+ .ex div {
+ width: 100px;
+ height: 100px;
+ }
+ .anchor {
+ background: orange;
+ }
+ [popover] {
+ background: lime;
+ padding:0;
+ border:0;
+ left: anchor(right);
+ top: anchor(top);
+ }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html
new file mode 100644
index 0000000000..a4285607fd
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests that a popover can be anchored to an unrendered element.</title>
+<link rel=author href="mailto:xiaochengh@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id=popover popover anchor=anchor></div>
+<div id=anchor></div>
+
+<style>
+ #anchor {
+ display: none;
+ }
+ [popover] {
+ background: lime;
+ padding: 0;
+ border: 0;
+ width: 100px;
+ height: 100px;
+ top: anchor(top, 100px);
+ left: anchor(left, 100px);
+ }
+</style>
+
+<script>
+test(() => {
+ popover.showPopover();
+ assert_equals(popover.offsetLeft, 100);
+ assert_equals(popover.offsetTop, 100);
+});
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-ref.tentative.html
new file mode 100644
index 0000000000..b9710ee5b5
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-ref.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+
+<p>There should be a green box attached to the right side of each orange box.</p>
+<div class=ex id=ex1><div class=anchor></div><div class=popover></div></div>
+<div class=ex id=ex2><div class=anchor></div><div class=popover></div></div>
+<div class=ex id=ex3><div class=anchor></div><div class=popover></div></div>
+<div class=ex id=ex3><div class=anchor></div><div class=popover></div></div>
+
+<style>
+ .ex {
+ margin: 25px;
+ font-size: 0;
+ }
+ .ex div {
+ display:inline-block;
+ width: 100px;
+ height: 100px;
+ }
+ .anchor {
+ background: orange;
+ }
+ .popover {
+ background: lime;
+ }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html
new file mode 100644
index 0000000000..af9e3329bb
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-anchor-display-ref.tentative.html">
+<link rel=stylesheet href="/fonts/ahem.css">
+<script src="resources/popover-utils.js"></script>
+
+<p>There should be a green box attached to the right side of each orange box.</p>
+
+<!-- Example using the `anchor` implicit reference element -->
+<div class=ex>
+ <div class=anchor id=anchor1></div>
+ <div id=popover1 popover=manual anchor=anchor1 defaultopen></div>
+</div>
+
+<!-- Example with `anchor` attribute but not using it for anchor pos -->
+<div class=ex>
+ <div id=anchor2 class=anchor></div>
+ <div id=popover2 popover=manual anchor defaultopen></div>
+</div>
+
+<!-- Example using `anchor-name` plus inset, and no `anchor` attribute -->
+<div class=ex>
+ <div id=anchor3 class=anchor></div>
+ <div id=popover3 popover=manual defaultopen></div>
+</div>
+
+<!-- Example using implicit anchor reference and inline anchor element -->
+<div class=ex>
+ <span id=anchor4>X</span>
+ <div id=popover4 popover=manual anchor=anchor4 defaultopen></div>
+</div>
+
+
+<script>
+showDefaultopenPopoversOnLoad();
+</script>
+
+<style>
+ .ex {
+ margin: 25px;
+ }
+ .ex div {
+ width: 100px;
+ height: 100px;
+ }
+ .anchor {
+ background: orange;
+ }
+ [popover] {
+ background: lime;
+ padding:0;
+ border:0;
+ }
+ #popover1 {
+ left: anchor(right);
+ top: anchor(top);
+ }
+ #anchor2 {
+ anchor-name: --anchor2;
+ }
+ #popover2 {
+ left: anchor(--anchor2 right);
+ top: anchor(--anchor2 top);
+ }
+ #anchor3 {
+ anchor-name: --anchor3;
+ }
+ #popover3 {
+ inset:auto;
+ left: anchor(--anchor3 right);
+ top: anchor(--anchor3 top);
+ }
+ #anchor4 {
+ font-family: Ahem;
+ font-size: 100px;
+ color: orange;
+ }
+ #popover4 {
+ left: anchor(right);
+ top: anchor(top);
+ }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-idl-property.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-idl-property.tentative.html
new file mode 100644
index 0000000000..5064bb99ca
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-idl-property.tentative.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<button id=b1>This is an anchor button</button>
+<div popover id=p1 anchor=b1>This is a popover</div>
+<button id=b2 popover=p1>This button invokes the popover but isn't an anchor</button>
+
+<script>
+ test(function() {
+ assert_equals(p1.anchor,b1);
+ }, "popover anchor IDL property returns the anchor element");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-multicol-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-multicol-display.tentative.html
new file mode 100644
index 0000000000..fe65ec5ba4
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-multicol-display.tentative.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<title>Tests popovers with implicit anchors in out-of-flow boxes</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#determining">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#propdef-anchor-name">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#anchor-size">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+
+<style>
+.relpos {
+ position: relative;
+}
+.columns {
+ column-count: 2;
+ column-fill: auto;
+ column-gap: 10px;
+ column-width: 100px;
+ width: 210px;
+ height: 50px;
+}
+#anchor1 {
+ position: absolute;
+ width: 10px;
+ height: 30px;
+ background: orange;
+}
+.target {
+ /*
+ * We need a popover to use implicit anchors, and force showing it with CSS
+ * so that it's not in the top layer.
+ */
+ display: block;
+ position: absolute;
+ margin: 0;
+ border: 0;
+ padding: 0;
+ width: anchor-size(width);
+ height: anchor-size(height);
+ background: lime;
+}
+</style>
+<body onload="checkLayout('.target')">
+ <div class="spacer" style="height: 10px"></div>
+ <div class="relpos">
+ <div class="columns">
+ <div class="spacer" style="height: 10px"></div>
+ <div class="relpos">
+ <div class="spacer" style="height: 10px"></div>
+ <div class="relpos">
+ <div class="spacer" style="height: 10px"></div>
+ <div id="anchor1"></div>
+ </div>
+ <div class="target" popover anchor="anchor1"
+ data-expected-height=50></div>
+ </div>
+ </div>
+ </div>
+
+</body>
+
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display-ref.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display-ref.html
new file mode 100644
index 0000000000..17311f218b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display-ref.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:xiaochengh@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+
+<button id=main-menu-button>Show menu</button>
+
+<div id=main-menu>
+ <div>Foo</div>
+ <button id=nested-menu-button>
+ Show nested menu
+ </button>
+ <div>Bar</div>
+</div>
+
+<div id=nested-menu>
+ Baz
+</div>
+
+<style>
+#main-menu-button {
+ position: absolute;
+ top: 200px;
+ left: 100px;
+ width: 100px;
+}
+
+#main-menu {
+ position: absolute;
+ top: 200px;;
+ left: 200px;
+ width: 150px;
+ line-height: 20px;
+}
+
+#nested-menu-button {
+ width: 100%;
+}
+
+#nested-menu {
+ position: absolute;
+ top: 220px;
+ left: 350px;
+}
+
+[popover] {
+ border: 0;
+ margin: 0;
+ padding: 0;
+}
+</style>
+
+<script>
+document.getElementById('main-menu-button').click();
+document.getElementById('nested-menu-button').click();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display.tentative.html
new file mode 100644
index 0000000000..426c0fcb85
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display.tentative.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:xiaochengh@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-anchor-nested-display-ref.html">
+
+<button id=main-menu-button popovertoggletarget=main-menu>Show menu</button>
+
+<div id=main-menu popover anchor=main-menu-button>
+ <div>Foo</div>
+ <button id=nested-menu-button popovertoggletarget=nested-menu>
+ Show nested menu
+ </button>
+ <div>Bar</div>
+</div>
+
+<div id=nested-menu popover anchor=nested-menu-button>
+ Baz
+</div>
+
+<style>
+#main-menu-button {
+ position: absolute;
+ top: 200px;
+ left: 100px;
+ width: 100px;
+}
+
+#main-menu {
+ top: anchor(top);
+ left: anchor(right);
+ width: 150px;
+ line-height: 20px;
+}
+
+#nested-menu-button {
+ width: 100%;
+}
+
+#nested-menu {
+ top: anchor(top);
+ left: anchor(right);
+}
+
+[popover] {
+ border: 0;
+ margin: 0;
+ padding: 0;
+}
+</style>
+
+<script>
+document.getElementById('main-menu-button').click();
+document.getElementById('nested-menu-button').click();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nesting.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nesting.tentative.html
new file mode 100644
index 0000000000..7490d75dc0
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nesting.tentative.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover anchor nesting</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.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/popover-utils.js"></script>
+
+<body>
+
+<!-- This example has the anchor (b1) for one popover (p1)
+ which contains a separate popover (p2) which is anchored
+ by a separate anchor (b2). -->
+<button id=b1 onclick='p1.showPopover()'>Popover 1
+ <div popover id=p2 anchor=b2>
+ <span id=inside2>Inside popover 2</span>
+ </div>
+</button>
+<div popover id=p1 anchor=b1>This is popover 1</div>
+<button id=b2 onclick='p2.showPopover()'>Popover 2</button>
+
+<style>
+ #p1 { top:50px; }
+ #p2 { top:50px; left:250px; }
+ [popover] { border: 5px solid red; }
+</style>
+
+
+<script>
+ const popover1 = document.querySelector('#p1');
+ const button1 = document.querySelector('#b1');
+ const popover2 = document.querySelector('#p2');
+
+ (async function() {
+ setup({ explicit_done: true });
+
+ popover2.showPopover();
+ assert_false(popover1.matches(':open'));
+ assert_true(popover2.matches(':open'));
+ await clickOn(button1);
+ test(t => {
+ // Button1 is the anchor for popover1, and an ancestor of popover2.
+ // Since popover2 is open, but not popover1, button1 should not be
+ // the anchor of any open popover. So popover2 should be closed.
+ assert_false(popover2.matches(':open'));
+ assert_true(popover1.matches(':open'));
+ },'Nested popovers (inside anchor elements) do not affect light dismiss');
+
+ done();
+ })();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display-ref.tentative.html
new file mode 100644
index 0000000000..dbaa30b047
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display-ref.tentative.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<div class=spacer style="height: 200px"></div>
+
+<p>There should be a green box attached to the right side of each orange box.</p>
+<div class=ex id=ex1><div class=anchor></div><div class=popover></div></div>
+
+<div class=spacer style="height: 200vh"></div>
+
+<style>
+ .ex {
+ margin: 25px;
+ font-size: 0;
+ }
+ .ex div {
+ display:inline-block;
+ width: 100px;
+ height: 100px;
+ }
+ .anchor {
+ background: orange;
+ }
+ .popover {
+ background: lime;
+ }
+</style>
+
+<script>
+document.documentElement.scrollTop = 100;
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html
new file mode 100644
index 0000000000..0630477812
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<link rel=author href="mailto:xiaochengh@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-anchor-scroll-display-ref.tentative.html">
+
+<div class=spacer style="height: 200px"></div>
+
+<p>There should be a green box attached to the right side of each orange box.</p>
+
+<!-- Example using the `anchor` implicit reference element -->
+<div class=ex>
+ <div class=anchor id=anchor1></div>
+ <div id=popover1 popover=manual anchor=anchor1></div>
+</div>
+
+<div class=spacer style="height: 200vh"></div>
+
+<style>
+ .ex {
+ margin: 25px;
+ }
+ .ex div {
+ width: 100px;
+ height: 100px;
+ }
+ .anchor {
+ background: orange;
+ }
+ [popover] {
+ background: lime;
+ padding:0;
+ border:0;
+ }
+ #popover1 {
+ left: anchor(right);
+ top: anchor(top);
+ }
+</style>
+
+<script>
+function raf() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+async function runTest() {
+ popover1.showPopover();
+
+ // Render a frame at the intial scroll position.
+ await raf();
+ await raf();
+
+ document.documentElement.scrollTop = 100;
+ document.documentElement.classList.remove('reftest-wait');
+
+ // The popover should still be attached to the anchor.
+}
+runTest();
+</script>
+
+</html>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-animated-display-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-animated-display-ref.tentative.html
new file mode 100644
index 0000000000..477a97c12c
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-animated-display-ref.tentative.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<link rel="stylesheet" href="resources/popover-styles.css">
+
+<div class=topmost></div>
+<div class=fake-popover>This is a popover</div>
+
+
+<style>
+ .fake-popover {
+ width: 100px;
+ height: 100px;
+ margin: 1em;
+ /* The animated property */
+ left: 100px;
+ }
+ .topmost {
+ position:fixed;
+ top:0;
+ left:0;
+ width:1000px;
+ height:1000px;
+ background:green;
+ margin:0;
+ padding:0;
+ }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-cleanup.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-cleanup.tentative.html
new file mode 100644
index 0000000000..9310acc4ff
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-cleanup.tentative.html
@@ -0,0 +1,98 @@
+
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.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/popover-utils.js"></script>
+<script src="/common/gc.js"></script>
+
+<dialog>I am a dialog</dialog>
+
+<style>
+[popover].animation {
+ left: 0px;
+}
+[popover].animation:open {
+ animation: move 1000s;
+}
+@keyframes move {
+ from { left: 0px; }
+ to { left: 200px; }
+}
+
+[popover].transition {
+ opacity: 0;
+ transition: opacity 5s;
+}
+[popover].transition:open {
+ opacity: 1;
+}
+
+[popover] {
+ top: 200px;
+}
+[popover]::backdrop {
+ background-color: rgba(255,0,0,0.2);
+}
+</style>
+
+<script>
+function rAF() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+}
+function addPopover(classname) {
+ const popover = document.createElement('div');
+ popover.popover = 'auto';
+ popover.classList = classname;
+ popover.textContent = 'This is a popover';
+ document.body.appendChild(popover);
+ return popover;
+}
+promise_test(async () => {
+ let popover = addPopover("animation");
+ let dialog = document.querySelector('dialog');
+ popover.showPopover(); // No animations here
+ await rAF();
+ popover.hidePopover(); // Start animations
+ await rAF();
+ popover.remove();
+ await garbageCollect();
+ await rAF();
+ // This test passes if it does not crash.
+},'Ensure no crashes if running animations are immediately cancelled (document removal)');
+
+promise_test(async (t) => {
+ let popover = addPopover("animation");
+ let dialog = document.querySelector('dialog');
+ popover.showPopover(); // No animations here
+ await rAF();
+ popover.hidePopover(); // Start animations
+ await rAF();
+ dialog.showModal(); // Immediately hide popover
+ t.add_cleanup(() => dialog.close());
+ await rAF();
+ popover.remove();
+ await garbageCollect();
+ await rAF();
+ // This test passes if it does not crash.
+},'Ensure no crashes if running animations are immediately cancelled (dialog showModal)');
+
+promise_test(async (t) => {
+ let popover = addPopover("transition");
+ let dialog = document.querySelector('dialog');
+ let button = document.createElement('button');
+ t.add_cleanup(() => {popover.remove();button.remove();});
+ document.body.appendChild(button);
+ button.addEventListener('click',() => dialog.show());
+ popover.showPopover(); // No animations here
+ await rAF();
+ await clickOn(button);
+ await rAF();
+ // This test passes if it does not crash.
+},'Ensure no crashes if running transitions are immediately cancelled (button click)');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-display.tentative.html
new file mode 100644
index 0000000000..5a59a9556d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-display.tentative.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-animated-display-ref.tentative.html">
+
+<div popover>This is a popover</div>
+<div class=topmost></div>
+
+<style>
+ [popover] {
+ width: 100px;
+ height: 100px;
+ margin: 1em;
+ left: 0;
+ transition: left 20s steps(2, jump-end) -10s;
+ }
+ [popover]:open {
+ left: 200px;
+ }
+ .topmost {
+ position:fixed;
+ z-index: 999999;
+ top:0;
+ left:0;
+ width:1000px;
+ height:1000px;
+ background:green;
+ margin:0;
+ padding:0;
+ }
+</style>
+
+<script>
+window.onload = () => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ // This will show the popover, hide the popover, and start the transition.
+ const popover = document.querySelector('[popover]');
+ popover.showPopover();
+ popover.getAnimations()[0].finish();
+ if (getComputedStyle(popover).left != "200px")
+ popover.remove();
+ popover.hidePopover();
+ document.getAnimations()[0].ready.then(() => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ // Take a screenshot now.
+ document.documentElement.classList.remove('reftest-wait');
+ });
+ });
+ });
+ });
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-finishes-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-finishes-ref.tentative.html
new file mode 100644
index 0000000000..d8334f985e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-finishes-ref.tentative.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+
+<div class=topmost></div>
+
+<style>
+ .topmost {
+ position:fixed;
+ top:0;
+ left:0;
+ width:1000px;
+ height:1000px;
+ background:green;
+ margin:0;
+ padding:0;
+ }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-finishes.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-finishes.tentative.html
new file mode 100644
index 0000000000..79e0b4dcbf
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-animated-hide-finishes.tentative.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-animated-hide-finishes-ref.tentative.html">
+
+<div popover>This is a popover</div>
+<div class=topmost></div>
+
+<style>
+ [popover] {
+ width: 100px;
+ height: 100px;
+ margin: 1em;
+ left: 0;
+ /* Immediate transition: */
+ transition: left 1s -1s;
+ }
+ [popover]:open {
+ left: 200px;
+ }
+ [popover]::backdrop {
+ background-color: red;
+ }
+ .topmost {
+ position:fixed;
+ z-index: 999999;
+ top:0;
+ left:0;
+ width:1000px;
+ height:1000px;
+ background:green;
+ margin:0;
+ padding:0;
+ }
+</style>
+
+<script>
+window.onload = () => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ // This will show the popover, hide the popover, and start the hide transition,
+ // which should immediately finish.
+ document.querySelector('[popover]').showPopover();
+ document.querySelector('[popover]').hidePopover();
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ // Take a screenshot now.
+ document.documentElement.classList.remove('reftest-wait');
+ });
+ });
+ });
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-animated-show-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-animated-show-display.tentative.html
new file mode 100644
index 0000000000..f78d8e1236
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-animated-show-display.tentative.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-animated-display-ref.tentative.html">
+
+<div popover>This is a popover</div>
+<div class=topmost></div>
+
+<style>
+ [popover] {
+ width: 100px;
+ height: 100px;
+ margin: 1em;
+ left: 0;
+ transition: left 20s steps(2, jump-end) -10s;
+ }
+ [popover]:open {
+ left: 200px;
+ }
+ .topmost {
+ position:fixed;
+ z-index: 999999;
+ top:0;
+ left:0;
+ width:1000px;
+ height:1000px;
+ background:green;
+ margin:0;
+ padding:0;
+ }
+</style>
+
+<script>
+window.onload = () => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ // This will show the popover, and start the transition.
+ document.querySelector('[popover]').showPopover();
+ document.getAnimations()[0].ready.then(() => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ // Take a screenshot now.
+ document.documentElement.classList.remove('reftest-wait');
+ });
+ });
+ });
+ });
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-animation-corner-cases.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-animation-corner-cases.tentative.html
new file mode 100644
index 0000000000..f41f7a68df
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-animation-corner-cases.tentative.html
@@ -0,0 +1,230 @@
+
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<body>
+<style>
+.animation { opacity: 0; }
+.animation:open { opacity: 1; }
+.animation:not(:open) { animation: fade-out 1000s; }
+@keyframes fade-out {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+
+.animation>div>div { left: 0; }
+.animation:not(:open)>div>div { animation: rotate 1000s; color:red;}
+@keyframes rotate {
+ from { transform: rotate(0); }
+ to { transform: rotate(360deg); }
+}
+
+[popover] { top: 200px; }
+[popover]::backdrop { background-color: rgba(255,0,0,0.2); }
+</style>
+
+<script>
+function createPopover(t,type) {
+ const popover = document.createElement('div');
+ popover.popover = 'auto';
+ popover.classList = type;
+ const div = document.createElement('div');
+ const descendent = div.appendChild(document.createElement('div'));
+ descendent.appendChild(document.createTextNode("Descendent element"));
+ popover.append("This is a pop up",div);
+ document.body.appendChild(popover);
+ t.add_cleanup(() => popover.remove());
+ return {popover, descendent};
+}
+promise_test(async (t) => {
+ const {popover, descendent} = createPopover(t,'animation');
+ assert_false(isElementVisible(popover));
+ assert_equals(descendent.parentElement.parentElement,popover);
+ assert_true(popover.matches(':closed'));
+ assert_false(popover.matches(':open'));
+ popover.showPopover();
+ assert_false(popover.matches(':closed'));
+ assert_true(popover.matches(':open'));
+ assert_true(isElementVisible(popover));
+ assert_equals(popover.getAnimations({subtree: true}).length,0);
+ popover.hidePopover();
+ const animations = popover.getAnimations({subtree: true});
+ assert_equals(animations.length,2,'There should be two animations running');
+ assert_false(popover.matches(':open'),'popover should not match :open as soon as hidden');
+ assert_false(popover.matches(':closed'),'popover should not match :closed until animations complete');
+ assert_true(isElementVisible(popover),'but animations should keep the popover visible');
+ assert_true(isElementVisible(descendent),'The descendent should also be visible');
+ await waitForRender();
+ await waitForRender();
+ assert_equals(popover.getAnimations({subtree: true}).length,2,'The animations should still be running');
+ assert_true(isElementVisible(popover),'Popover should still be visible due to animation');
+ animations.forEach(animation => animation.finish()); // Force the animations to finish
+ await waitForRender(); // Wait one frame
+ assert_true(popover.matches(':closed'),'The pop up should now match :closed');
+ assert_false(popover.matches(':open'),'The pop up still shouldn\'t match :open');
+ assert_false(isElementVisible(popover),'The pop up should now be invisible');
+ assert_false(isElementVisible(descendent),'The descendent should also be invisible');
+ assert_equals(popover.getAnimations({subtree: true}).length,0);
+},'Descendent animations should keep the pop up visible until the animation ends');
+
+promise_test(async (t) => {
+ const {popover, descendent} = createPopover(t,'');
+ assert_equals(popover.classList.length, 0);
+ assert_false(isElementVisible(popover));
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ assert_true(isElementVisible(popover));
+ assert_equals(popover.getAnimations({subtree: true}).length,0);
+ // Start an animation on the popover and its descendent.
+ popover.animate([{opacity: 1},{opacity: 0}],{duration: 1000000,iterations: 1});
+ descendent.animate([{transform: 'rotate(0)'},{transform: 'rotate(360deg)'}],1000000);
+ assert_equals(popover.getAnimations({subtree: true}).length,2);
+ // Then hide the popover.
+ popover.hidePopover();
+ assert_false(popover.matches(':open'),'pop up should not match :open as soon as hidden');
+ assert_true(popover.matches(':closed'),'pop up should match :closed immediately');
+ assert_equals(popover.getAnimations({subtree: true}).length,2,'animations should still be running');
+ await waitForRender();
+ assert_equals(popover.getAnimations({subtree: true}).length,2,'animations should still be running');
+ assert_false(isElementVisible(popover),'Pre-existing animations should not keep the pop up visible');
+},'Pre-existing animations should *not* keep the pop up visible until the animation ends');
+
+promise_test(async (t) => {
+ const {popover, descendent} = createPopover(t,'');
+ popover.showPopover();
+ assert_true(isElementVisible(popover));
+ assert_equals(popover.getAnimations({subtree: true}).length,0);
+ let animation;
+ popover.addEventListener('beforetoggle', (e) => {
+ if (e.newState !== "closed")
+ return;
+ animation = popover.animate([{opacity: 1},{opacity: 0}],1000000);
+ });
+ assert_equals(popover.getAnimations({subtree: true}).length,0,'There should be no animations yet');
+ popover.hidePopover();
+ assert_equals(popover.getAnimations({subtree: true}).length,1,'the hide animation should now be running');
+ assert_true(!!animation);
+ assert_true(isElementVisible(popover),'The animation should keep the popover visible');
+ animation.finish();
+ await waitForRender();
+ assert_false(isElementVisible(popover),'Once the animation ends, the popover is hidden');
+},'It should be possible to use the "beforetoggle" event handler to animate the hide');
+
+
+promise_test(async (t) => {
+ const {popover, descendent} = createPopover(t,'');
+ const dialog = document.body.appendChild(document.createElement('dialog'));
+ t.add_cleanup(() => dialog.remove());
+ popover.showPopover();
+ assert_true(isElementVisible(popover));
+ assert_equals(popover.getAnimations({subtree: true}).length,0);
+ let animation;
+ popover.addEventListener('beforetoggle', (e) => {
+ if (e.newState !== "closed")
+ return;
+ animation = popover.animate([{opacity: 1},{opacity: 0}],1000000);
+ });
+ assert_equals(popover.getAnimations({subtree: true}).length,0,'There should be no animations yet');
+ dialog.showModal(); // Force hide the popover
+ await waitForRender();
+ assert_equals(popover.getAnimations({subtree: true}).length,1,'the hide animation should now be running');
+ assert_true(isElementVisible(popover),'And the animation should keep the popover visible');
+ animation.finish();
+ await waitForRender();
+ assert_false(isElementVisible(popover),'Once the animation ends, the popover is hidden');
+},'It should be possible to use the "beforetoggle" event handler to animate the hide, even when the hide is due to dialog.showModal');
+
+promise_test(async (t) => {
+ const {popover, descendent} = createPopover(t,'');
+ popover.showPopover();
+ assert_true(isElementVisible(popover));
+ popover.addEventListener('beforetoggle', (e) => e.preventDefault());
+ popover.hidePopover();
+ await waitForRender();
+ assert_false(isElementVisible(popover),'Even if hide event is cancelled, the popover still closes');
+},'toggle event cannot be cancelled');
+
+promise_test(async (t) => {
+ const {popover, descendent} = createPopover(t,'animation');
+ assert_false(isElementVisible(popover));
+ popover.showPopover();
+ assert_false(popover.matches(':closed'));
+ assert_true(popover.matches(':open'));
+ assert_true(isElementVisible(popover));
+ assert_equals(popover.getAnimations({subtree: true}).length,0);
+ popover.popover = 'manual';
+ const animations = popover.getAnimations({subtree: true});
+ assert_equals(animations.length,2,'There should be two animations running');
+ assert_false(popover.matches(':open'),'popover should not match :open as soon as hidden');
+ assert_false(popover.matches(':closed'),'popover should not match :closed until animations complete');
+ assert_true(isElementVisible(popover),'but animations should keep the popover visible');
+ animations.forEach(animation => animation.finish()); // Force the animations to finish
+ await waitForRender(); // Wait one frame
+ assert_true(popover.matches(':closed'),'The pop up should now match :closed');
+ assert_false(popover.matches(':open'),'The pop up still shouldn\'t match :open');
+ assert_false(isElementVisible(popover),'The pop up should now be invisible');
+},'Closing animations are triggered by changing the popover type');
+
+promise_test(async (t) => {
+ const {popover, descendent} = createPopover(t,'');
+ popover.showPopover();
+ assert_true(isElementVisible(popover));
+ assert_equals(popover.getAnimations({subtree: true}).length,0);
+ popover.addEventListener('beforetoggle', (e) => {
+ if (e.newState !== "closed")
+ return;
+ popover.animate([{opacity: 1},{opacity: 0}],1000000);
+ });
+ assert_equals(popover.getAnimations({subtree: true}).length,0,'There should be no animations yet');
+ popover.hidePopover();
+ await waitForRender();
+ assert_equals(popover.getAnimations({subtree: true}).length,1,'the hide animation should now be running');
+ assert_true(isElementVisible(popover),'The popover should still be visible because the animation hasn\'t ended.');
+ const animation = popover.getAnimations({subtree: true})[0];
+
+ animation.dispatchEvent(new Event('finish'));
+ await waitForRender();
+ assert_true(isElementVisible(popover),'Synthetic finish events should not stop the animation, so the popover should still be visible.');
+ assert_equals(popover.getAnimations({subtree: true}).length,1,'the hide animation should still be running');
+
+ animation.dispatchEvent(new Event('cancel'));
+ await waitForRender();
+ assert_true(isElementVisible(popover),'Synthetic cancel events should not stop the animation, so the popover should still be visible.');
+ assert_equals(popover.getAnimations({subtree: true}).length,1,'the hide animation should still be running');
+},'animation finish/cancel events must be trusted in order to finish closing the popover.');
+
+promise_test(async (t) => {
+ const {popover, descendent} = createPopover(t,'');
+ popover.showPopover();
+ popover.addEventListener('beforetoggle', (e) => {
+ if (e.newState !== "closed")
+ return;
+ popover.animate([{opacity: 1},{opacity: 0}],1000000);
+ });
+ popover.hidePopover();
+ await waitForRender();
+ assert_true(isElementVisible(popover),'The popover should still be visible because the animation hasn\'t ended.');
+ assert_equals(popover.getAnimations({subtree: true}).length,1,'There should be one animation running');
+ const animation = popover.getAnimations({subtree: true})[0];
+ let new_animation;
+ const listener = () => {new_animation = popover.animate([{opacity: 1},{opacity: 0}],1000000)};
+ animation.addEventListener('finish',listener,{capture:true});
+ animation.addEventListener('cancel',listener,{capture:true});
+ popover.addEventListener('animationfinish',listener,{capture:true});
+ popover.addEventListener('animationcancel',listener,{capture:true});
+ assert_true(isElementVisible(popover),'The popover should still be visible.');
+ assert_equals(new_animation, undefined,'New animation should not be started yet.');
+ animation.finish();
+ await waitForRender(); // Wait one frame
+ assert_true(popover.matches(':closed'),'The pop up should now match :closed');
+ assert_false(popover.matches(':open'),'The pop up still shouldn\'t match :open');
+ assert_false(isElementVisible(popover),'The pop up should now be invisible');
+ assert_not_equals(new_animation, animation);
+ assert_equals(popover.getAnimations({subtree: true})[0],new_animation,'The new animation should now be running');
+},'Capturing event listeners can\'t affect popover animations.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-appearance-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-appearance-ref.tentative.html
new file mode 100644
index 0000000000..7ceca94559
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-appearance-ref.tentative.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover element appearance</title>
+<link rel="stylesheet" href="resources/popover-styles.css">
+
+<style>
+.fake-popover {top: 100px; bottom: auto;}
+#blank {left: -300px;}
+#auto {left: -100px;}
+#manual {left: 100px;}
+#invalid {left: 300px;}
+</style>
+
+<p>There should be four popovers with similar appearance.</p>
+<div class="fake-popover" id=blank>Blank</div>
+<div class="fake-popover" id=auto>Auto</div>
+<div class="fake-popover" id=manual>Manual</div>
+<div class="fake-popover" id=invalid>Invalid</div>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-appearance.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-appearance.tentative.html
new file mode 100644
index 0000000000..5c2c8d1b11
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-appearance.tentative.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover element appearance</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel="match" href="popover-appearance-ref.tentative.html">
+
+<style>
+[popover] {top: 100px; bottom: auto;}
+[popover=""] {left: -300px}
+[popover=auto] {left: -100px; }
+[popover=manual] {left: 100px; }
+[popover=invalid] {left: 300px; }
+</style>
+
+<p>There should be four popovers with similar appearance.</p>
+<div popover>Blank
+ <div popover=auto>Auto</div>
+</div>
+<div popover=manual>Manual</div>
+<!-- This ensures unsupported popover values are treated as popover=manual -->
+<div popover=invalid>Invalid</div>
+<script>
+ document.querySelectorAll('[popover]').forEach(p => p.showPopover());
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-attribute-basic.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-attribute-basic.tentative.html
new file mode 100644
index 0000000000..d16e34f896
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-attribute-basic.tentative.html
@@ -0,0 +1,431 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<meta name="timeout" content="long">
+<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/popover-utils.js"></script>
+<script src="../../resources/common.js"></script>
+
+<div id=popovers>
+ <div popover id=boolean>Pop up</div>
+ <div popover="">Pop up</div>
+ <div popover=auto>Pop up</div>
+ <div popover=manual>Pop up</div>
+ <article popover>Different element type</article>
+ <header popover>Different element type</header>
+ <nav popover>Different element type</nav>
+ <input type=text popover value="Different element type">
+ <dialog popover>Dialog with popover attribute</dialog>
+ <dialog popover="manual">Dialog with popover=manual</dialog>
+ <div popover=true>Invalid popover value - defaults to popover=manual</div>
+ <div popover=popover>Invalid popover value - defaults to popover=manual</div>
+ <div popover=invalid>Invalid popover value - defaults to popover=manual</div>
+</div>
+
+<div id=nonpopovers>
+ <div>Not a popover</div>
+ <dialog open>Dialog without popover attribute</dialog>
+</div>
+
+<div popover class=animated>Animated popover</div>
+<div id=outside></div>
+<style>
+[popover].animated {
+ opacity: 0;
+ transition: opacity 10s;
+}
+[popover].animated:open {
+ opacity: 1;
+}
+[popover] {
+ inset:auto;
+ top:0;
+ left:0;
+}
+#outside {
+ position:fixed;
+ top:200px;
+ left:200px;
+ height:10px;
+ width:10px;
+}
+</style>
+
+<script>
+setup({ explicit_done: true });
+window.onload = () => {
+ const outsideElement = document.getElementById('outside');
+ function assertPopoverVisibility(popover, isPopover, expectedVisibility, message) {
+ const isVisible = isElementVisible(popover);
+ assert_equals(isVisible, expectedVisibility,`${message}: Expected this element to be ${expectedVisibility ? "visible" : "not visible"}`);
+ // Check other things related to being visible or not:
+ if (isVisible) {
+ assert_not_equals(window.getComputedStyle(popover).display,'none');
+ assert_equals(popover.matches(':open'),isPopover,`${message}: Visible popovers should match :open`);
+ assert_false(popover.matches(':closed'),`${message}: Visible popovers and *all* non-popovers should *not* match :closed`);
+ } else {
+ assert_equals(window.getComputedStyle(popover).display,'none',`${message}: Non-showing popovers should have display:none`);
+ assert_false(popover.matches(':open'),`${message}: Non-showing popovers should *not* match :open`);
+ assert_true(popover.matches(':closed'),`${message}: Non-showing popovers should match :closed`);
+ }
+ }
+ function assertIsFunctionalPopover(popover) {
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'A popover should start out hidden');
+ popover.showPopover();
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After showPopover(), a popover should be visible');
+ assert_throws_dom("InvalidStateError",() => popover.showPopover(),'Calling showPopover on a showing popover should throw InvalidStateError');
+ popover.hidePopover();
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After hidePopover(), a popover should be hidden');
+ assert_throws_dom("InvalidStateError",() => popover.hidePopover(),'Calling hidePopover on a hidden popover should throw InvalidStateError');
+ popover.togglePopover();
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover() on hidden popover, it should be visible');
+ popover.togglePopover();
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover() on visible popover, it should be hidden');
+ popover.togglePopover(/*force=*/true);
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover(true) on hidden popover, it should be visible');
+ popover.togglePopover(/*force=*/true);
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover(true) on visible popover, it should be visible');
+ popover.togglePopover(/*force=*/false);
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover(false) on visible popover, it should be hidden');
+ popover.togglePopover(/*force=*/false);
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover(false) on hidden popover, it should be hidden');
+ const parent = popover.parentElement;
+ popover.remove();
+ assert_throws_dom("InvalidStateError",() => popover.showPopover(),'Calling showPopover on a disconnected popover should throw InvalidStateError');
+ assert_throws_dom("InvalidStateError",() => popover.hidePopover(),'Calling hidePopover on a disconnected popover should throw InvalidStateError');
+ assert_throws_dom("InvalidStateError",() => popover.togglePopover(),'Calling hidePopover on a disconnected popover should throw InvalidStateError');
+ parent.appendChild(popover);
+ }
+ function assertNotAPopover(nonPopover) {
+ // If the non-popover element nonetheless has a 'popover' attribute, it should
+ // be invisible. Otherwise, it should be visible.
+ const expectVisible = !nonPopover.hasAttribute('popover');
+ assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'A non-popover should start out visible');
+ assert_throws_dom("NotSupportedError",() => nonPopover.showPopover(),'Calling showPopover on a non-popover should throw NotSupportedError');
+ assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling showPopover on a non-popover should leave it visible');
+ assert_throws_dom("NotSupportedError",() => nonPopover.hidePopover(),'Calling hidePopover on a non-popover should throw NotSupportedError');
+ assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling hidePopover on a non-popover should leave it visible');
+ assert_throws_dom("NotSupportedError",() => nonPopover.togglePopover(),'Calling togglePopover on a non-popover should throw NotSupportedError');
+ assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling togglePopover on a non-popover should leave it visible');
+ }
+
+ // Start with the provided examples:
+ Array.from(document.getElementById('popovers').children).forEach(popover => {
+ test((t) => {
+ assertIsFunctionalPopover(popover);
+ }, `The element ${popover.outerHTML} should behave as a popover.`);
+ });
+ Array.from(document.getElementById('nonpopovers').children).forEach(nonPopover => {
+ test((t) => {
+ assertNotAPopover(nonPopover);
+ }, `The element ${nonPopover.outerHTML} should *not* behave as a popover.`);
+ });
+
+ // Then loop through all HTML5 elements that render a box by default:
+ let elementsThatDontRender = ['audio','base','br','datalist','dialog','embed','head','link','meta','noscript','param','rp','script','slot','style','template','title','wbr'];
+ const elements = HTML5_ELEMENTS.filter(el => !elementsThatDontRender.includes(el));
+ elements.forEach(tag => {
+ test((t) => {
+ const element = document.createElement(tag);
+ element.setAttribute('popover','auto');
+ document.body.appendChild(element);
+ t.add_cleanup(() => element.remove());
+ assertIsFunctionalPopover(element);
+ }, `A <${tag} popover> element should behave as a popover.`);
+ test((t) => {
+ const element = document.createElement(tag);
+ document.body.appendChild(element);
+ t.add_cleanup(() => element.remove());
+ assertNotAPopover(element);
+ }, `A <${tag}> element should *not* behave as a popover.`);
+ });
+
+ function createPopover(t) {
+ const popover = document.createElement('div');
+ document.body.appendChild(popover);
+ t.add_cleanup(() => popover.remove());
+ popover.setAttribute('popover','auto');
+ return popover;
+ }
+
+ test((t) => {
+ // You can set the `popover` attribute to anything.
+ // Setting the `popover` IDL to a string sets the content attribute to exactly that, always.
+ // Getting the `popover` IDL value only retrieves valid values.
+ const popover = createPopover(t);
+ assert_equals(popover.popover,'auto');
+ popover.setAttribute('popover','auto');
+ assert_equals(popover.popover,'auto');
+ popover.setAttribute('popover','AuTo');
+ assert_equals(popover.popover,'auto','Case is normalized in IDL');
+ assert_equals(popover.getAttribute('popover'),'AuTo','Case is *not* normalized/changed in the content attribute');
+ popover.popover='aUtO';
+ assert_equals(popover.popover,'auto','Case is normalized in IDL');
+ assert_equals(popover.getAttribute('popover'),'aUtO','Value set from IDL is propagated exactly to the content attribute');
+ popover.setAttribute('popover','invalid');
+ assert_equals(popover.popover,'manual','Invalid values should reflect as "manual"');
+ popover.removeAttribute('popover');
+ assert_equals(popover.popover,null,'No value should reflect as null');
+ popover.popover='auto';
+ assert_equals(popover.getAttribute('popover'),'auto');
+ popover.popover='';
+ assert_equals(popover.getAttribute('popover'),'');
+ assert_equals(popover.popover,'auto');
+ popover.popover='AuTo';
+ assert_equals(popover.getAttribute('popover'),'AuTo');
+ assert_equals(popover.popover,'auto');
+ popover.popover='invalid';
+ assert_equals(popover.getAttribute('popover'),'invalid','IDL setter allows any value');
+ assert_equals(popover.popover,'manual','but IDL getter reflects "manual"');
+ popover.popover='';
+ assert_equals(popover.getAttribute('popover'),'','IDL setter propagates exactly');
+ assert_equals(popover.popover,'auto','Empty should map to auto in IDL');
+ popover.popover='auto';
+ popover.popover=null;
+ assert_equals(popover.getAttribute('popover'),null,'Setting null for the IDL property should remove the content attribute');
+ assert_equals(popover.popover,null,'Null returns null');
+ popover.popover='auto';
+ popover.popover=undefined;
+ assert_equals(popover.getAttribute('popover'),null,'Setting undefined for the IDL property should remove the content attribute');
+ assert_equals(popover.popover,null,'undefined returns null');
+ },'IDL attribute reflection');
+
+ test((t) => {
+ const popover = createPopover(t);
+ assertIsFunctionalPopover(popover);
+ popover.removeAttribute('popover');
+ assertNotAPopover(popover);
+ popover.setAttribute('popover','AuTo');
+ assertIsFunctionalPopover(popover);
+ popover.removeAttribute('popover');
+ popover.setAttribute('PoPoVeR','AuTo');
+ assertIsFunctionalPopover(popover);
+ // Via IDL also
+ popover.popover = 'auto';
+ assertIsFunctionalPopover(popover);
+ popover.popover = 'aUtO';
+ assertIsFunctionalPopover(popover);
+ popover.popover = 'invalid'; // treated as "manual"
+ assertIsFunctionalPopover(popover);
+ },'Popover attribute value should be case insensitive');
+
+ test((t) => {
+ const popover = createPopover(t);
+ assertIsFunctionalPopover(popover);
+ popover.setAttribute('popover','manual'); // Change popover type
+ assertIsFunctionalPopover(popover);
+ popover.setAttribute('popover','invalid'); // Change popover type to something invalid
+ assertIsFunctionalPopover(popover);
+ popover.popover = 'manual'; // Change popover type via IDL
+ assertIsFunctionalPopover(popover);
+ popover.popover = 'invalid'; // Make invalid via IDL (treated as "manual")
+ assertIsFunctionalPopover(popover);
+ },'Changing attribute values for popover should work');
+
+ test((t) => {
+ const popover = createPopover(t);
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ popover.setAttribute('popover','manual'); // Change popover type
+ assert_false(popover.matches(':open'));
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ popover.setAttribute('popover','invalid');
+ assert_true(popover.matches(':open'),'From "manual" to "invalid" (which is interpreted as "manual") should not close the popover');
+ popover.setAttribute('popover','auto');
+ assert_false(popover.matches(':open'),'From "invalid" ("manual") to "auto" should hide the popover');
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ popover.setAttribute('popover','invalid');
+ assert_false(popover.matches(':open'),'From "auto" to "invalid" (which is interpreted as "manual") should close the popover');
+ },'Changing attribute values should close open popovers');
+
+ function modalPseudoSupported() {
+ try {
+ document.createElement('dialog').matches(':modal');
+ return true; // No exception means :modal is supported.
+ } catch(e) {
+ return false;
+ }
+ }
+
+ const validTypes = ["auto","manual"];
+ validTypes.forEach(type => {
+ test((t) => {
+ const popover = createPopover(t);
+ popover.setAttribute('popover',type);
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ popover.remove();
+ assert_false(popover.matches(':open'));
+ document.body.appendChild(popover);
+ assert_false(popover.matches(':open'));
+ },`Removing a visible popover=${type} element from the document should close the popover`);
+
+ if (modalPseudoSupported()) {
+ test((t) => {
+ const popover = createPopover(t);
+ popover.setAttribute('popover',type);
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ assert_false(popover.matches(':modal'));
+ popover.hidePopover();
+ },`A showing popover=${type} does not match :modal`);
+ }
+ });
+
+ test((t) => {
+ const other_popover = createPopover(t);
+ other_popover.setAttribute('popover','auto');
+ other_popover.showPopover();
+ const popover = createPopover(t);
+ popover.setAttribute('popover','auto');
+ other_popover.addEventListener('beforetoggle', (e) => {
+ if (e.newState !== "closed")
+ return;
+ popover.setAttribute('popover','manual');
+ },{once: true});
+ assert_true(other_popover.matches(':open'));
+ assert_false(popover.matches(':open'));
+ popover.showPopover();
+ assert_false(other_popover.matches(':open'),'unrelated popover is hidden');
+ assert_false(popover.matches(':open'),'popover is not shown if its type changed during show');
+ },`Changing the popover type in a "beforetoggle" event handler should not cause problems (during showPopover())`);
+
+ test((t) => {
+ const popover = createPopover(t);
+ popover.setAttribute('popover','auto');
+ const other_popover = createPopover(t);
+ other_popover.setAttribute('popover','auto');
+ popover.appendChild(other_popover);
+ popover.showPopover();
+ other_popover.showPopover();
+ let nested_popover_hidden=false;
+ other_popover.addEventListener('beforetoggle', (e) => {
+ if (e.newState !== "closed")
+ return;
+ nested_popover_hidden = true;
+ popover.setAttribute('popover','manual');
+ },{once: true});
+ popover.addEventListener('beforetoggle', (e) => {
+ if (e.newState !== "closed")
+ return;
+ assert_true(nested_popover_hidden,'The nested popover should be hidden first');
+ },{once: true});
+ assert_true(popover.matches(':open'));
+ assert_true(other_popover.matches(':open'));
+ popover.hidePopover();
+ assert_false(other_popover.matches(':open'),'unrelated popover is hidden');
+ assert_false(popover.matches(':open'),'popover is still hidden if its type changed during hide event');
+ assert_throws_dom("InvalidStateError",() => other_popover.hidePopover(),'Nested popover should already be hidden');
+ },`Changing the popover type in a "beforetoggle" event handler should not cause problems (during hidePopover())`);
+
+ function interpretedType(typeString,method) {
+ if (validTypes.includes(typeString))
+ return typeString;
+ if (typeString === undefined)
+ return "invalid-value-undefined";
+ if (method === "idl" && typeString === null)
+ return "invalid-value-idl-null";
+ return "manual"; // Invalid types default to "manual"
+ }
+ function setPopoverValue(popover,type,method) {
+ switch (method) {
+ case "attr":
+ if (type === undefined) {
+ popover.removeAttribute('popover');
+ } else {
+ popover.setAttribute('popover',type);
+ }
+ break;
+ case "idl":
+ popover.popover = type;
+ break;
+ default:
+ assert_notreached();
+ }
+ }
+ ["attr","idl"].forEach(method => {
+ validTypes.forEach(type => {
+ [...validTypes,"invalid",null,undefined].forEach(newType => {
+ [...validTypes,"invalid",null,undefined].forEach(inEventType => {
+ promise_test(async (t) => {
+ const popover = createPopover(t);
+ setPopoverValue(popover,type,method);
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ let gotEvent = false;
+ popover.addEventListener('beforetoggle', (e) => {
+ if (e.newState !== "closed")
+ return;
+ gotEvent = true;
+ setPopoverValue(popover,inEventType,method);
+ },{once:true});
+ setPopoverValue(popover,newType,method);
+ if (type===interpretedType(newType,method)) {
+ // Keeping the type the same should not hide it or fire events.
+ assert_true(popover.matches(':open'),'popover should remain open when not changing the type');
+ assert_false(gotEvent);
+ popover.hidePopover(); // Cleanup
+ } else {
+ // Changing the type at all should hide the popover. The hide event
+ // handler should run, set a new type, and that type should end up
+ // as the final result.
+ assert_false(popover.matches(':open'));
+ if (inEventType === undefined || (method ==="idl" && inEventType === null)) {
+ assert_throws_dom("NotSupportedError",() => popover.showPopover(),'We should have removed the popover attribute, so showPopover should throw');
+ } else {
+ // Make sure the attribute is correct.
+ assert_equals(popover.getAttribute('popover'),String(inEventType),'Content attribute');
+ assert_equals(popover.popover, interpretedType(inEventType,method),'IDL attribute');
+ // Make sure the type is really correct, via behavior.
+ popover.showPopover(); // Show it
+ assert_true(popover.matches(':open'),'Popover should function');
+ await clickOn(outsideElement); // Try to light dismiss
+ switch (interpretedType(inEventType,method)) {
+ case 'manual':
+ assert_true(popover.matches(':open'),'A popover=manual should not light-dismiss');
+ popover.hidePopover();
+ break;
+ case 'auto':
+ assert_false(popover.matches(':open'),'A popover=auto should light-dismiss');
+ break;
+ }
+ }
+ }
+ },`Changing a popover from ${type} to ${newType} (via ${method}), and then ${inEventType} during 'beforetoggle' works`);
+ });
+ });
+ });
+ });
+
+ promise_test(async () => {
+ const popover = document.querySelector('[popover].animated');
+ assert_true(!!popover);
+ assert_false(isElementVisible(popover));
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ assert_false(popover.matches(':closed'));
+ assert_true(getComputedStyle(popover).opacity < 0.1,'Animations should start on show');
+ assert_throws_dom("InvalidStateError",() => popover.showPopover(),'Calling showPopover on a popover that is in the process of animating show should throw InvalidStateError');
+ await finishAnimations(popover);
+ assert_true(getComputedStyle(popover).opacity > 0.9);
+ assert_true(isElementVisible(popover));
+ popover.hidePopover();
+ assert_false(popover.matches(':open'));
+ assert_false(popover.matches(':closed'),'Not :closed until animations finish');
+ assert_true(getComputedStyle(popover).opacity > 0.9,'Animations should start on hide');
+ assert_throws_dom("InvalidStateError",() => popover.hidePopover(),'Calling hidePopover on a popover that is in the process of animating hide should throw InvalidStateError');
+ assert_throws_dom("InvalidStateError",() => popover.showPopover(),'Calling showPopover on a popover that is in the process of animating hide should throw InvalidStateError');
+ await finishAnimations(popover);
+ assert_true(popover.matches(':closed'),':closed should match once animations finish');
+ },'Exceptions are thrown even when show/hide are animated');
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance-ref.tentative.html
new file mode 100644
index 0000000000..bf2b16c3f5
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance-ref.tentative.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover ::backdrop pseudo element appearance</title>
+<link rel="stylesheet" href="resources/popover-styles.css">
+
+<style>
+#bottom { top: 70px; left: 70px; }
+#middle { top: 120px; left: 120px; }
+#top { top: 170px; left: 170px; }
+.fake-popover-backdrop {
+ height: 200px;
+ width: 200px;
+}
+#bottom-backdrop {
+ top: 50px;
+ left: 50px;
+ background-color: rgb(0, 50, 0);
+}
+#middle-backdrop {
+ top: 100px;
+ left: 100px;
+ background-color: rgb(0, 130, 0);
+}
+#top-backdrop {
+ top: 150px;
+ left: 150px;
+ background-color: rgb(0, 210, 0);
+}
+.fake-popover {
+ margin:0;
+}
+</style>
+<p>Test for [popover]::backdrop presence and stacking order. The test passes
+ if there are 3 stacked boxes, with the brightest green on top.</p>
+<div popover id=bottom>Bottom
+ <div popover id=middle>Middle
+ <div popover=manual id=top>Top</div>
+ </div>
+</div>
+<div id="bottom-backdrop" class="fake-popover-backdrop"></div>
+<div id="bottom" class="fake-popover">Bottom</div>
+<div id="middle-backdrop" class="fake-popover-backdrop"></div>
+<div id="middle" class="fake-popover">Middle</div>
+<div id="top-backdrop" class="fake-popover-backdrop"></div>
+<div id="top" class="fake-popover">Top</div>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance.tentative.html
new file mode 100644
index 0000000000..24e42989ca
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance.tentative.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover ::backdrop pseudo element appearance</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel="match" href="popover-backdrop-appearance-ref.tentative.html">
+
+<style>
+#bottom { top: 70px; left: 70px; }
+#middle { top: 120px; left: 120px; }
+#top { top: 170px; left: 170px; }
+::backdrop { height: 200px; width: 200px; }
+#bottom::backdrop {
+ top: 50px;
+ left: 50px;
+ background-color: rgb(0, 50, 0);
+ z-index: 100; /* z-index has no effect. */
+}
+#middle::backdrop {
+ top: 100px;
+ left: 100px;
+ background-color: rgb(0, 130, 0);
+ z-index: -100; /* z-index has no effect. */
+}
+#top::backdrop {
+ top: 150px;
+ left: 150px;
+ background-color: rgb(0, 210, 0);
+ z-index: 0; /* z-index has no effect. */
+}
+[popover] {
+ margin:0;
+}
+</style>
+<p>Test for [popover]::backdrop presence and stacking order. The test passes
+ if there are 3 stacked boxes, with the brightest green on top.</p>
+<div popover id=bottom>Bottom
+ <div popover id=middle>Middle
+ <div popover=manual id=top>Top</div>
+ </div>
+</div>
+<script>
+document.getElementById('bottom').showPopover();
+document.getElementById('middle').showPopover();
+document.getElementById('top').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-beforetoggle-opening-event.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-beforetoggle-opening-event.tentative.html
new file mode 100644
index 0000000000..b2c3fa5d68
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-beforetoggle-opening-event.tentative.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover show event</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div popover></div>
+
+<script>
+test(() => {
+ let frameCount = 0;
+ requestAnimationFrame(() => {++frameCount;});
+ const popover = document.querySelector('[popover]');
+ const testText = 'Show Event Occurred';
+ popover.addEventListener('beforetoggle',(e) => {
+ if (e.newState !== "open")
+ return;
+ popover.textContent = testText;
+ })
+ popover.offsetHeight;
+ assert_equals(popover.textContent,"");
+ assert_equals(frameCount,0);
+ popover.showPopover();
+ popover.offsetHeight;
+ assert_equals(popover.textContent,testText);
+ assert_equals(frameCount,0,'nothing should be rendered before the popover is updated');
+ popover.hidePopover(); // Cleanup
+},'Ensure the `beforetoggle` event can be used to populate content before the popover renders');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance-ref.tentative.html
new file mode 100644
index 0000000000..12efbb6b1e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance-ref.tentative.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Dialog-Popover appearance</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+
+<p>Both dialogs should have the same shades of background.</p>
+<p>The popover should have a completely-transparent ::backdrop.</p>
+<dialog popover id=d1>This is a modal dialog</dialog>
+<dialog popover id=d2>This is a dialog popover</dialog>
+
+<style>
+ dialog {
+ left: 50px;
+ right: auto;
+ bottom: auto;
+ }
+ #d1 {top:100px;}
+ #d2 {top:150px;}
+ /* Force backdrop to spec: */
+ #d1::backdrop {
+ /* https://html.spec.whatwg.org/multipage/rendering.html#flow-content-3 */
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+ #d2::backdrop {
+ /* When shown as a popover, backdrop must be transparent */
+ background-color: transparent;
+ }
+</style>
+
+<script>
+ document.getElementById('d1').showModal();
+ document.getElementById('d2').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance.tentative.html
new file mode 100644
index 0000000000..9707ac0e93
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance.tentative.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Dialog-Popover appearance</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel="match" href="popover-dialog-appearance-ref.tentative.html">
+
+<p>Both dialogs should have the same shades of background.</p>
+<p>The popover should have a completely-transparent ::backdrop.</p>
+<dialog popover id=d1>This is a modal dialog</dialog>
+<dialog popover id=d2>This is a dialog popover</dialog>
+
+<style>
+ dialog {
+ left: 50px;
+ right: auto;
+ bottom: auto;
+ }
+ #d1 {top:100px;}
+ #d2 {top:150px;}
+</style>
+
+<script>
+ document.getElementById('d1').showModal();
+ document.getElementById('d2').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-dialog-crash.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-crash.tentative.html
new file mode 100644
index 0000000000..76f51b8a5e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-crash.tentative.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8" />
+<title>Dialog-Popover crash</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<p>This test passes if it does not crash.</p>
+<dialog popover>This is a modal dialog</dialog>
+<div popover>This is a popover</div>
+
+<script>
+ const dialog = document.querySelector('dialog[popover]');
+ const popover = document.querySelector('div[popover]');
+ dialog.showModal();
+ popover.showPopover();
+ clickOn(dialog)
+ .then(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-document-open.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-document-open.tentative.html
new file mode 100644
index 0000000000..429fd89d3b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-document-open.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div popover id=popover1>Popover</div>
+
+<script>
+ window.onload = () => {
+ test((t) => {
+ const popover1 = document.querySelector('#popover1');
+ popover1.showPopover();
+ assert_true(popover1.matches(':open'));
+ assert_true(!document.querySelector('#popover2'));
+ document.open();
+ document.write('<!DOCTYPE html><div popover id=popover2>Popover</div>');
+ document.close();
+ assert_true(!document.querySelector('#popover1'),'popover1 should be removed from the document');
+ assert_true(!!document.querySelector('#popover2'),'popover2 should be in the document');
+ assert_false(popover1.matches(':open'),'popover1 should have been hidden when it was removed from the document');
+ assert_false(popover1.matches(':open'),'popover2 shouldn\'t be showing yet');
+ popover2.showPopover();
+ assert_true(popover2.matches(':open'),'popover2 should be able to be shown');
+ popover2.hidePopover();
+ },'document.open should not break popovers');
+ };
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-events.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-events.tentative.html
new file mode 100644
index 0000000000..7d63ce74b7
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-events.tentative.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover events</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<div popover>Popover</div>
+
+<script>
+window.onload = () => {
+ for(const method of ["listener","attribute"]) {
+ promise_test(async t => {
+ const popover = document.querySelector('[popover]');
+ assert_false(popover.matches(':open'));
+ let showCount = 0;
+ let hideCount = 0;
+ function listener(e) {
+ if (e.newState === "open") {
+ assert_equals(e.currentState,"closed",'Popover toggleevent states should be "open" and "closed"')
+ 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 {
+ assert_equals(e.currentState,"open",'Popover toggleevent states should be "open" and "closed"')
+ assert_equals(e.newState,"closed",'Popover toggleevent states should be "open" and "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;
+ }
+ };
+ switch (method) {
+ case "listener":
+ const controller = new AbortController();
+ const signal = controller.signal;
+ t.add_cleanup(() => controller.abort());
+ // The 'beforetoggle' event bubbles.
+ document.addEventListener('beforetoggle', listener, {signal});
+ break;
+ case "attribute":
+ assert_false(popover.hasAttribute('onbeforetoggle'));
+ t.add_cleanup(() => popover.removeAttribute('onbeforetoggle'));
+ popover.onbeforetoggle = listener;
+ break;
+ default: assert_unreached();
+ }
+ assert_equals(0,showCount);
+ assert_equals(0,hideCount);
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ assert_equals(1,showCount);
+ assert_equals(0,hideCount);
+ await waitForRender();
+ assert_true(popover.matches(':open'));
+ popover.hidePopover();
+ assert_false(popover.matches(':open'));
+ assert_equals(1,showCount);
+ assert_equals(1,hideCount);
+ await waitForRender();
+ // No additional events after animation frame
+ assert_false(popover.matches(':open'));
+ assert_equals(1,showCount);
+ assert_equals(1,hideCount);
+ }, `Toggle event (${method}) get properly dispatched for popovers`);
+ }
+
+ promise_test(async t => {
+ const popover = document.querySelector('[popover]');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ t.add_cleanup(() => controller.abort());
+ let cancel = true;
+ popover.addEventListener('beforetoggle',(e) => {
+ if (e.newState !== "open")
+ return;
+ if (cancel)
+ e.preventDefault();
+ }, {signal});
+ assert_false(popover.matches(':open'));
+ popover.showPopover();
+ assert_false(popover.matches(':open'),'The "beforetoggle" event should be cancelable for the "opening" transition');
+ cancel = false;
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ popover.hidePopover();
+ assert_false(popover.matches(':open'));
+ }, 'Toggle event is cancelable for the "opening" transition');
+};
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.tentative.html
new file mode 100644
index 0000000000..569b633886
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.tentative.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover focus behaviors</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.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/popover-utils.js"></script>
+
+<div id=fixup>
+ <button id=button1>Button1</button>
+ <div popover id=popover1 style="top:100px">
+ <button id=inside_popover1>Inside1</button>
+ <button id=invoker2 popovertoggletarget=popover2>Nested Invoker 2</button>
+ <button id=inside_popover2>Inside2</button>
+ </div>
+ <button id=button2>Button2</button>
+ <button popovertoggletarget=popover1 id=invoker1>Invoker1</button>
+ <button id=button3>Button3</button>
+ <div popover id=popover2 style="top:200px">
+ <button id=inside_popover3>Inside3</button>
+ <button id=invoker3 popovertoggletarget=popover3>Nested Invoker 3</button>
+ </div>
+ <div popover id=popover3 style="top:300px">
+ Non-focusable popover
+ </div>
+ <button id=button4>Button4</button>
+</div>
+<style>
+ #fixup [popover] {
+ bottom:auto;
+ }
+</style>
+<script>
+async function verifyFocusOrder(order) {
+ order[0].focus();
+ for(let i=0;i<order.length;++i) {
+ const control = order[i];
+ assert_equals(document.activeElement,control,`Step ${i+1}`);
+ await sendTab();
+ }
+ // Shift-tab not supported, crbug.com/893480.
+ // for(let i=order.length-1;i>=0;--i) {
+ // const control = order[i];
+ // await sendShiftTab();
+ // assert_equals(document.activeElement,control,`Step ${i+1} (backwards)`);
+ // }
+}
+promise_test(async t => {
+ button1.focus();
+ assert_equals(document.activeElement,button1);
+ await sendTab();
+ assert_equals(document.activeElement,button2,'Hidden popover should be skipped');
+ // Shift-tab not supported, crbug.com/893480.
+ // await sendShiftTab();
+ // assert_equals(document.activeElement,button1,'Hidden popover should be skipped backwards');
+ //await sendTab();
+ await sendTab();
+ assert_equals(document.activeElement,invoker1);
+ await sendEnter(); // Activate the invoker
+ assert_true(popover1.matches(':open'), 'popover1 should be invoked by invoker1');
+ assert_equals(document.activeElement,invoker1,'Focus should not move when popover is shown');
+ await sendTab();
+ assert_equals(document.activeElement,inside_popover1,'Focus should move from invoker into the open popover');
+ await sendTab();
+ assert_equals(document.activeElement,invoker2,'Focus should move within popover');
+ await verifyFocusOrder([button1, button2, invoker1, inside_popover1, invoker2, inside_popover2, button3, button4]);
+ invoker2.focus();
+ await sendEnter(); // Activate the nested invoker
+ assert_true(popover2.matches(':open'), 'popover2 should be invoked by nested invoker');
+ assert_equals(document.activeElement,invoker2,'Focus should stay on the invoker');
+ await sendTab();
+ assert_equals(document.activeElement,inside_popover3,'Focus should move into nested popover');
+ await sendTab();
+ assert_equals(document.activeElement,invoker3);
+ await sendEnter(); // Activate the (empty) nested invoker
+ assert_true(popover3.matches(':open'), 'popover3 should be invoked by nested invoker');
+ assert_equals(document.activeElement,invoker3,'Focus should stay on the invoker');
+ await sendTab();
+ assert_equals(document.activeElement,inside_popover2,'Focus should skip popover without focusable content, going back to higher scope');
+ await sendTab();
+ assert_equals(document.activeElement,button3,'Focus should exit popovers');
+ await sendTab();
+ assert_equals(document.activeElement,button4,'Focus should skip popovers');
+ button1.focus();
+ await verifyFocusOrder([button1, button2, invoker1, inside_popover1, invoker2, inside_popover3, invoker3, inside_popover2, button3, button4]);
+}, "Popover focus navigation");
+</script>
+
+<button id=circular0 popovertoggletarget=popover4>Invoker</button>
+<div id=popover4 popover>
+ <button id=circular1 autofocus popoverhidetarget=popover4></button>
+ <button id=circular2 popovershowtarget=popover4></button>
+ <button id=circular3 popovertoggletarget=popover4></button>
+</div>
+<button id=circular4>after</button>
+<script>
+promise_test(async t => {
+ circular0.focus();
+ await sendEnter(); // Activate the invoker
+ await verifyFocusOrder([circular0, circular1, circular2, circular3, circular4]);
+ popover4.hidePopover();
+}, "Circular reference tab navigation");
+</script>
+
+<div id=deleted>
+ <button popovershowtarget=deleted1>Show popover</button>
+ <div popover id=deleted1>
+ <button popoverhidetarget=deleted1 autofocus>Hide popover</button>
+ </div>
+</div>
+<script>
+promise_test(async t => {
+ const invoker = document.querySelector('#deleted>button');
+ const popover = document.querySelector('#deleted>[popover]');
+ const hideButton = popover.querySelector('[popoverhidetarget]');
+ invoker.focus(); // Make sure button is focused.
+ assert_equals(document.activeElement,invoker);
+ await sendEnter(); // Activate the invoker
+ assert_true(popover.matches(':open'), 'popover should be invoked by invoker');
+ assert_equals(document.activeElement,hideButton,'Hide button should be focused due to autofocus attribute');
+ await sendEnter(); // Activate the hide invoker
+ assert_false(popover.matches(':open'), 'popover should be hidden by invoker');
+ assert_equals(document.activeElement,invoker,'Focus should be returned to the invoker');
+}, "Popover focus returns when popover is hidden by invoker");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-focus-child-dialog.html b/testing/web-platform/tests/html/semantics/popovers/popover-focus-child-dialog.html
new file mode 100644
index 0000000000..c07d313c9e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-focus-child-dialog.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://chromium-review.googlesource.com/c/chromium/src/+/4021969">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id=popover1 popover>
+ <dialog id=childdialog autofocus>
+ <button autofocus>hello world</button>
+ </dialog>
+</div>
+
+<div id=popover2 popover=manual>
+ <div id=childpopover popover=manual autofocus>
+ <button autofocus>hello world</button>
+ </div>
+</div>
+
+<script>
+test(t => {
+ t.add_cleanup(() => childdialog.close());
+ t.add_cleanup(() => popover1.hidePopover());
+
+ childdialog.showModal();
+ document.activeElement.blur();
+ popover1.showPopover();
+
+ assert_true(popover1.matches(':open'), 'The popover should be open.');
+ assert_true(childdialog.hasAttribute('open'), 'The dialog should be open.');
+ assert_equals(document.activeElement, document.body, 'Nothing should have gotten focused.');
+}, 'Popovers should not initially focus child dialog elements.');
+
+test(t => {
+ t.add_cleanup(() => childpopover.hidePopover());
+ t.add_cleanup(() => popover2.hidePopover());
+
+ childpopover.showPopover();
+ document.activeElement.blur();
+ popover2.showPopover();
+
+ assert_true(popover2.matches(':open'), 'The parent popover should be open.');
+ assert_true(childpopover.matches(':open'), 'The child popover should be open.');
+ assert_equals(document.activeElement, document.body, 'Nothing should have gotten focused.');
+}, 'Popovers should not initially focus child popover elements.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-focus.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-focus.tentative.html
new file mode 100644
index 0000000000..b1e59a1397
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-focus.tentative.html
@@ -0,0 +1,286 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover focus behaviors</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<meta name="timeout" content="long">
+<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/popover-utils.js"></script>
+
+<div popover data-test='default behavior - popover is not focused' data-no-focus>
+ <p>This is a popover</p>
+ <button>first button</button>
+</div>
+
+<div popover data-test='autofocus popover' autofocus tabindex=-1 class=should-be-focused>
+ <p>This is a popover</p>
+</div>
+
+<div popover data-test='autofocus empty popover' autofocus tabindex=-1 class=should-be-focused></div>
+
+<div popover data-test='autofocus popover with button' autofocus tabindex=-1 class=should-be-focused>
+ <p>This is a popover</p>
+ <button>button</button>
+</div>
+
+<div popover data-test='autofocus child'>
+ <p>This is a popover</p>
+ <button autofocus class=should-be-focused>autofocus button</button>
+</div>
+
+<div popover data-test='autofocus on tabindex=0 element'>
+ <p autofocus tabindex=0 class=should-be-focused>This is a popover with autofocus on a tabindex=0 element</p>
+ <button>button</button>
+</div>
+
+<div popover data-test='autofocus multiple children'>
+ <p>This is a popover</p>
+ <button autofocus class=should-be-focused>autofocus button</button>
+ <button autofocus>second autofocus button</button>
+</div>
+
+<div popover autofocus tabindex=-1 data-test='autofocus popover and multiple autofocus children' class=should-be-focused>
+ <p>This is a popover</p>
+ <button autofocus>autofocus button</button>
+ <button autofocus>second autofocus button</button>
+</div>
+
+<style>
+ [popover] {
+ border: 2px solid black;
+ top:150px;
+ left:150px;
+ opacity: 0;
+ }
+ [popover]:not(:open) {
+ /* Add a *hide* transition to all popovers, to make sure animations don't
+ affect focus management */
+ transition: opacity 10s;
+ }
+ [popover]:open {
+ opacity: 1;
+ }
+ :focus-within { border: 5px dashed red; }
+ :focus { border: 5px solid lime; }
+</style>
+
+<script>
+ function addInvoker(t, popover) {
+ const button = document.createElement('button');
+ button.innerText = 'Click me';
+ const popoverId = 'popover-id';
+ assert_equals(document.querySelectorAll('#' + popoverId).length, 0);
+ document.body.appendChild(button);
+ t.add_cleanup(function() {
+ popover.removeAttribute('id');
+ button.remove();
+ });
+ popover.id = popoverId;
+ button.setAttribute('popovertoggletarget', popoverId);
+ return button;
+ }
+ function addPriorFocus(t) {
+ const priorFocus = document.createElement('button');
+ priorFocus.id = 'priorFocus';
+ document.body.appendChild(priorFocus);
+ t.add_cleanup(() => priorFocus.remove());
+ return priorFocus;
+ }
+ async function finishAnimationsAndVerifyHide(popover) {
+ await finishAnimations(popover);
+ assert_false(isElementVisible(popover),'After animations are finished, the popover should be hidden');
+ }
+ function activateAndVerify(popover) {
+ const testName = popover.getAttribute('data-test');
+ promise_test(async t => {
+ const priorFocus = addPriorFocus(t);
+ let expectedFocusedElement = popover.matches('.should-be-focused') ? popover : popover.querySelector('.should-be-focused');
+ const changesFocus = !popover.hasAttribute('data-no-focus');
+ if (!changesFocus) {
+ expectedFocusedElement = priorFocus;
+ }
+ assert_true(!!expectedFocusedElement);
+ assert_false(popover.matches(':open'));
+
+ // Directly show and hide the popover:
+ priorFocus.focus();
+ assert_equals(document.activeElement, priorFocus);
+ popover.showPopover();
+ assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
+ popover.hidePopover();
+ assert_equals(document.activeElement, priorFocus, 'prior element should get focus on hide, or if focus didn\'t shift on show, focus should stay where it was');
+ await finishAnimationsAndVerifyHide(popover);
+
+ // Hit Escape:
+ priorFocus.focus();
+ assert_equals(document.activeElement, priorFocus);
+ popover.showPopover();
+ assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
+ await sendEscape();
+ assert_equals(document.activeElement, priorFocus, 'prior element should get focus after Escape');
+ await finishAnimationsAndVerifyHide(popover);
+
+ // Move focus into the popover, then hit Escape:
+ let containedButton = popover.querySelector('button');
+ if (containedButton) {
+ priorFocus.focus();
+ assert_equals(document.activeElement, priorFocus);
+ popover.showPopover();
+ containedButton.focus();
+ assert_equals(document.activeElement, containedButton);
+ await sendEscape();
+ assert_equals(document.activeElement, priorFocus, 'prior element should get focus after Escape');
+ await finishAnimationsAndVerifyHide(popover);
+ }
+
+ // Change the popover type:
+ priorFocus.focus();
+ popover.showPopover();
+ assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
+ assert_equals(popover.popover, 'auto', 'All popovers in this test should start as popover=auto');
+ popover.popover = 'hint';
+ assert_false(popover.matches(':open'), 'Changing the popover type should hide the popover');
+ assert_equals(document.activeElement, priorFocus, 'prior element should get focus when the type is changed');
+ await finishAnimationsAndVerifyHide(popover);
+ popover.popover = 'auto';
+
+ // Remove from the document:
+ priorFocus.focus();
+ popover.showPopover();
+ assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
+ popover.remove();
+ assert_false(isElementVisible(popover), 'Removing the popover should hide it immediately');
+ if (!popover.hasAttribute('data-no-focus')) {
+ assert_not_equals(document.activeElement, priorFocus, 'prior element should *not* get focus when the popover is removed from the document');
+ }
+ document.body.appendChild(popover);
+
+ // Show a modal dialog:
+ priorFocus.focus();
+ popover.showPopover();
+ assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
+ const dialog = document.body.appendChild(document.createElement('dialog'));
+ dialog.showModal();
+ assert_false(popover.matches(':open'), 'Opening a modal dialog should hide the popover');
+ assert_not_equals(document.activeElement, priorFocus, 'prior element should *not* get focus when a modal dialog is shown');
+ await finishAnimationsAndVerifyHide(popover);
+ dialog.close();
+ dialog.remove();
+
+ // Use an activating element:
+ const button = addInvoker(t, popover);
+ priorFocus.focus();
+ button.click();
+ assert_true(popover.matches(':open'));
+ assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by button.click()`);
+
+ // Make sure Escape works in the invoker case:
+ await sendEscape();
+ assert_equals(document.activeElement, priorFocus, 'prior element should get focus after Escape (via invoker)');
+ await finishAnimationsAndVerifyHide(popover);
+
+ // Make sure we can directly focus the (already open) popover:
+ priorFocus.focus();
+ button.click();
+ assert_true(popover.matches(':open'));
+ assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by button.click()`);
+ popover.focus();
+ assert_equals(document.activeElement, popover.hasAttribute('tabindex') ? popover : expectedFocusedElement, `${testName} directly focus with popover.focus()`);
+ button.click(); // Button is set to toggle the popover
+ assert_false(popover.matches(':open'));
+ assert_equals(document.activeElement, priorFocus, 'prior element should get focus on button-toggled hide');
+ await finishAnimationsAndVerifyHide(popover);
+ }, "Popover focus test: " + testName);
+
+ promise_test(async t => {
+ const priorFocus = addPriorFocus(t);
+ assert_false(popover.matches(':open'), 'popover should start out hidden');
+ let button = addInvoker(t, popover);
+ assert_equals(button.getAttribute('popovertoggletarget'), popover.id, 'This test assumes the button uses `popovertoggletarget`.');
+ assert_not_equals(button, priorFocus, 'Stranger things have happened');
+ assert_false(popover.contains(button), 'Start with a non-contained button');
+ priorFocus.focus();
+ assert_equals(document.activeElement, priorFocus);
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ await clickOn(button); // This will *not* light dismiss, but will "toggle" the popover.
+ assert_false(popover.matches(':open'));
+ assert_equals(document.activeElement, priorFocus, 'focus should return to the prior focus');
+ await finishAnimationsAndVerifyHide(popover);
+
+ // Same thing, but the button is contained within the popover
+ button.removeAttribute('popovertoggletarget');
+ button.setAttribute('popoverhidetarget', popover.id);
+ popover.appendChild(button);
+ t.add_cleanup(() => button.remove());
+ priorFocus.focus();
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ const changesFocus = !popover.hasAttribute('data-no-focus');
+ if (changesFocus) {
+ assert_not_equals(document.activeElement, priorFocus, 'focus should shift for this element');
+ }
+ await clickOn(button);
+ assert_false(popover.matches(':open'), 'clicking button should hide the popover');
+ assert_equals(document.activeElement, priorFocus, 'Contained button should return focus to the previously focused element');
+ await finishAnimationsAndVerifyHide(popover);
+
+ // Same thing, but the button is unrelated (no popovertoggletarget)
+ button = document.createElement('button');
+ document.body.appendChild(button);
+ priorFocus.focus();
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ await clickOn(button); // This will light dismiss the popover, focus the prior focus, then focus this button.
+ assert_false(popover.matches(':open'), 'clicking button should hide the popover (via light dismiss)');
+ assert_equals(document.activeElement, button, 'Focus should go to unrelated button on light dismiss');
+ await finishAnimationsAndVerifyHide(popover);
+ }, "Popover button click focus test: " + testName);
+
+ promise_test(async t => {
+ if (popover.hasAttribute('data-no-focus')) {
+ // This test only applies if the popover changes focus
+ return;
+ }
+ const priorFocus = addPriorFocus(t);
+ assert_false(popover.matches(':open'), 'popover should start out hidden');
+
+ // Move the prior focus out of the document
+ priorFocus.focus();
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ const newFocus = document.activeElement;
+ assert_not_equals(newFocus, priorFocus, 'focus should shift for this element');
+ priorFocus.remove();
+ assert_equals(document.activeElement, newFocus, 'focus should not change when prior focus is removed');
+ popover.hidePopover();
+ assert_not_equals(document.activeElement, priorFocus, 'focused element has been removed');
+ await finishAnimationsAndVerifyHide(popover);
+ document.body.appendChild(priorFocus); // Put it back
+
+ // Move the prior focus inside the (already open) popover
+ priorFocus.focus();
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ assert_false(popover.contains(priorFocus), 'Start with a non-contained prior focus');
+ popover.appendChild(priorFocus); // Move inside the popover
+ assert_true(popover.contains(priorFocus));
+ assert_true(popover.matches(':open'), 'popover should stay open');
+ popover.hidePopover();
+ await waitForRender();
+ assert_true(isElementVisible(popover),'Animations should keep the popover visible');
+ assert_not_equals(getComputedStyle(popover).display,'none','Animations should keep the popover visible');
+ assert_equals(document.activeElement, priorFocus, 'focused element gets focused');
+ await finishAnimationsAndVerifyHide(popover);
+ assert_equals(getComputedStyle(popover).display,'none','Animations have ended, popover should be hidden');
+ assert_not_equals(document.activeElement, priorFocus, 'focused element is display:none inside the popover');
+ document.body.appendChild(priorFocus); // Put it back
+ }, "Popover corner cases test: " + testName);
+ }
+
+ document.querySelectorAll('body > [popover]').forEach(popover => activateAndVerify(popover));
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display-ref.tentative.html
new file mode 100644
index 0000000000..2dc0d558b6
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display-ref.tentative.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel="stylesheet" href="resources/popover-styles.css">
+
+
+<div class=fake-popover>This content should be visible and green</div>
+<div class=fake-popover style="top:100px;">This content should be visible and green</div>
+<div class=fake-popover style="top:200px;">This content should be visible and green</div>
+
+<style>
+ .fake-popover {
+ top: 0;
+ margin:10px;
+ width: 300px;
+ height: 50px;
+ background: green;
+ }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display.tentative.html
new file mode 100644
index 0000000000..b77566fdfc
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display.tentative.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-hidden-display-ref.tentative.html">
+<meta name=fuzzy content="0-1;0-15">
+
+<div class=nottoplayer popover >This content should be visible and green</div>
+<div class=nottoplayer popover=invalid style="top:100px;">This content should be visible and green</div>
+<div class=toplayer popover style="top:200px;">This content should be visible and green</div>
+
+<style>
+ [popover] {
+ display: block; /* This should make the popover visible */
+ top: 0;
+ margin:10px;
+ width: 300px;
+ height: 50px;
+ }
+ [popover].nottoplayer {
+ background: green;
+ }
+ [popover].toplayer {
+ background: red;
+ }
+ [popover].toplayer:open {
+ background: green;
+ }
+ [popover].nottoplayer:open {
+ background: red;
+ }
+</style>
+<script>
+ const toplayer = document.querySelectorAll('[popover].toplayer');
+ if (toplayer.length !== 1)
+ document.write('FAIL');
+ toplayer[0].showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none-ref.tentative.html
new file mode 100644
index 0000000000..3d58e4ca09
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none-ref.tentative.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+
+No popover should be displayed here.<p>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none.tentative.html
new file mode 100644
index 0000000000..b36f1bbffd
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none.tentative.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-inside-display-none-ref.tentative.html">
+
+No popover should be displayed here.<p>
+
+<div style="display:none">
+ <div popover>This content should be hidden</div>
+</div>
+
+<script>
+ const popover = document.querySelector('[popover]');
+ popover.showPopover();
+ if (!popover.matches(':open'))
+ document.body.appendChild(document.createTextNode('FAIL'));
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute.tentative.html
new file mode 100644
index 0000000000..5ce315ef1d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute.tentative.html
@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover invoking attribute</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<meta name="timeout" content="long">
+<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>
+
+<body>
+<script>
+const buttonLogic = (t,s,h) => {
+ // This mimics the expected logic for button invokers:
+ let expectedBehavior = t ? "toggle" : (s ? "show" : (h ? "hide" : "none"));
+ let expectedId = t || s || h || 1;
+ if (!t && s && h) {
+ // Special case - only use toggle if the show/hide idrefs match.
+ expectedBehavior = (s === h) ? "toggle" : "show";
+ }
+ return {expectedBehavior, expectedId};
+}
+const noActivationLogic = (t,s,h) => {
+ // This does not activate any popovers.
+ return {expectedBehavior: "none", expectedId: 1};
+}
+function makeElementWithType(element,type) {
+ return (test) => {
+ const el = Object.assign(document.createElement(element),{type});
+ document.body.appendChild(el);
+ test.add_cleanup(() => el.remove());
+ return el;
+ };
+}
+const supportedButtonTypes = ['button','reset','submit',''].map(type => {
+ return {
+ name: `<button type="${type}">`,
+ makeElement: makeElementWithType('button',type),
+ invokeFn: el => {el.focus(); el.click()},
+ getExpectedLogic: buttonLogic,
+ supported: true,
+ };
+});
+const supportedInputButtonTypes = ['button','reset','submit','image'].map(type => {
+ return {
+ name: `<input type="${type}">`,
+ makeElement: makeElementWithType('input',type),
+ invokeFn: el => {el.focus(); el.click()},
+ getExpectedLogic: buttonLogic,
+ supported: true,
+ };
+});
+const unsupportedTypes = ['text','email','password','search','tel','url','checkbox','radio','range','file','color','date','datetime-local','month','time','week','number'].map(type => {
+ return {
+ name: `<input type="${type}">`,
+ makeElement: makeElementWithType('input',type),
+ invokeFn: (el) => {el.focus();},
+ getExpectedLogic: noActivationLogic, // None of these support popover invocation
+ supported: false,
+ };
+});
+const invokers = [
+ ...supportedButtonTypes,
+ ...supportedInputButtonTypes,
+ ...unsupportedTypes,
+];
+window.addEventListener('load', () => {
+ ["auto","manual"].forEach(type => {
+ invokers.forEach(testcase => {
+ let t_set = [1], s_set = [1], h_set = [1];
+ if (testcase.supported) {
+ t_set = s_set = h_set = [0,1,2]; // Test all permutations
+ }
+ t_set.forEach(t => {
+ s_set.forEach(s => {
+ h_set.forEach(h => {
+ [false,true].forEach(use_idl => {
+ promise_test(async test => {
+ const popover1 = Object.assign(document.createElement('div'),{popover: type, id: 'popover-1'});
+ const popover2 = Object.assign(document.createElement('div'),{popover: type, id: 'popover-2'});
+ assert_equals(popover1.popover,type);
+ assert_equals(popover2.popover,type);
+ assert_not_equals(popover1.id,popover2.id);
+ const invoker = testcase.makeElement(test);
+ if (use_idl) {
+ invoker.popoverToggleTarget = t===1 ? popover1.id : (t===2 ? popover2.id : null);
+ invoker.popoverShowTarget = s===1 ? popover1.id : (s===2 ? popover2.id : null);
+ invoker.popoverHideTarget = h===1 ? popover1.id : (h===2 ? popover2.id : null);
+ } else {
+ if (t) invoker.setAttribute('popovertoggletarget',t===1 ? popover1.id : popover2.id);
+ if (s) invoker.setAttribute('popovershowtarget',s===1 ? popover1.id : popover2.id);
+ if (h) invoker.setAttribute('popoverhidetarget',h===1 ? popover1.id : popover2.id);
+ }
+ assert_true(!document.getElementById(popover1.id));
+ assert_true(!document.getElementById(popover2.id));
+ document.body.appendChild(popover1);
+ document.body.appendChild(popover2);
+ test.add_cleanup(() => {
+ popover1.remove();
+ popover2.remove();
+ });
+ const {expectedBehavior, expectedId} = testcase.getExpectedLogic(t,s,h);
+ const otherId = expectedId !== 1 ? 1 : 2;
+ function assertPopoverShowing(num,state,message) {
+ assert_true(num>0,`Invalid expectedId ${num}`);
+ assert_equals((num===1 ? popover1 : popover2).matches(':open'),state,message || "");
+ }
+ assertPopoverShowing(expectedId,false);
+ assertPopoverShowing(otherId,false);
+ await testcase.invokeFn(invoker);
+ assert_equals(document.activeElement,invoker,'Focus should end up on the invoker');
+ assertPopoverShowing(otherId,false,'The other popover should never change');
+ switch (expectedBehavior) {
+ case "toggle":
+ case "show":
+ assertPopoverShowing(expectedId,true,'Toggle or show should show the popover');
+ (expectedId===1 ? popover1 : popover2).hidePopover(); // Hide the popover
+ break;
+ case "hide":
+ case "none":
+ assertPopoverShowing(expectedId,false,'Hide or none should leave the popover hidden');
+ break;
+ default:
+ assert_unreached();
+ }
+ if (expectedBehavior === "none") {
+ // If no behavior is expected, then there is nothing left to test. Even re-focusing
+ // a control that has no expected behavior may hide an open popover via light dismiss.
+ return;
+ }
+ (expectedId===1 ? popover1 : popover2).showPopover(); // Show the popover directly
+ assert_equals(document.activeElement,invoker,'The popover should not shift focus');
+ assertPopoverShowing(expectedId,true);
+ assertPopoverShowing(otherId,false);
+ await testcase.invokeFn(invoker);
+ assertPopoverShowing(otherId,false,'The other popover should never change');
+ switch (expectedBehavior) {
+ case "toggle":
+ case "hide":
+ assertPopoverShowing(expectedId,false,'Toggle or hide should hide the popover');
+ break;
+ case "show":
+ assertPopoverShowing(expectedId,true,'Show should leave the popover showing');
+ break;
+ default:
+ assert_unreached();
+ }
+ },`Test ${testcase.name}, t=${t}, s=${s}, h=${h}, ${use_idl ? "IDL" : "Content Attr"}, with popover=${type}`);
+ });
+ });
+ });
+ });
+ });
+ });
+});
+</script>
+
+
+
+<button popovertoggletarget=p1>Toggle Popover 1</button>
+<div popover id=p1 style="border: 5px solid red;top: 100px;left: 100px;">This is popover #1</div>
+
+<script>
+function clickOn(element) {
+ const actions = new test_driver.Actions();
+ return actions.pointerMove(0, 0, {origin: element})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .send();
+}
+
+const popover = document.querySelector('[popover]');
+const button = document.querySelector('button');
+let showCount = 0;
+let hideCount = 0;
+popover.addEventListener('beforetoggle',(e) => {
+ if (e.newState === "open")
+ ++showCount;
+ else
+ ++hideCount;
+ });
+
+async function assertState(expectOpen,expectShow,expectHide) {
+ assert_equals(popover.matches(':open'),expectOpen,'Popover open state is incorrect');
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ assert_equals(showCount,expectShow,'Show count is incorrect');
+ assert_equals(hideCount,expectHide,'Hide count is incorrect');
+}
+
+window.addEventListener('load', () => {
+ promise_test(async () => {
+ showCount = hideCount = 0;
+ await assertState(false,0,0);
+ await clickOn(button);
+ await assertState(true,1,0);
+ popover.hidePopover();
+ await assertState(false,1,1);
+ button.click();
+ await assertState(true,2,1);
+ popover.hidePopover();
+ await assertState(false,2,2);
+ }, "Clicking a popovertoggletarget button opens a closed popover (also check event counts)");
+
+ promise_test(async () => {
+ showCount = hideCount = 0;
+ await assertState(false,0,0);
+ await clickOn(button);
+ await assertState(true,1,0);
+ await clickOn(button);
+ await assertState(false,1,1);
+ }, "Clicking a popovertoggletarget button closes an open popover (also check event counts)");
+});
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-on-scroll.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-on-scroll.tentative.html
new file mode 100644
index 0000000000..73b3a2d619
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-on-scroll.tentative.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html lang="en">
+<meta charset="utf-8" />
+<title>Popover should *not* light dismiss on scroll</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=help href="https://github.com/openui/open-ui/issues/240">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id=scroller>
+ Scroll me<br><br>
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
+ ut labore et dolore magna aliqua. Enim ut sem viverra aliquet eget sit amet tellus. Massa
+ sed elementum tempus egestas sed sed risus pretium. Felis bibendum ut tristique et egestas
+ quis. Tortor dignissim convallis aenean et. Eu mi bibendum neque egestas congue quisque
+</div>
+
+<div popover id=popover1>
+ This is popover 1
+ <div popover id=popover2 anchor=anchor>
+ This is popover 2
+ </div>
+</div>
+<button onclick='popover1.showPopover();popover2.showPopover();'>Open popovers</button>
+
+<style>
+ #popover1 { top:50px; left: 50px; }
+ #popover2 { top:150px; left: 50px; }
+ #scroller {
+ height: 150px;
+ width: 150px;
+ overflow-y: scroll;
+ border: 1px solid black;
+ }
+</style>
+
+<script>
+ const popovers = document.querySelectorAll('[popover]');
+ function assertAll(showing) {
+ for(let popover of popovers) {
+ assert_equals(popover.matches(':open'),showing);
+ }
+ }
+ async_test(t => {
+ for(let popover of popovers) {
+ popover.addEventListener('beforetoggle',e => {
+ if (e.newState !== "closed")
+ return;
+ assert_unreached('Scrolling should not light-dismiss a popover');
+ });
+ }
+ assertAll(/*showing*/false);
+ popovers[0].showPopover();
+ popovers[1].showPopover();
+ assertAll(/*showing*/true);
+ scroller.scrollTo(0, 100);
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ assertAll(/*showing*/true);
+ t.done();
+ });
+ });
+ },'Scrolling should not light-dismiss popovers');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.tentative.html
new file mode 100644
index 0000000000..3c48bd9274
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.tentative.html
@@ -0,0 +1,493 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover light dismiss behavior</title>
+<meta name="timeout" content="long">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.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/popover-utils.js"></script>
+
+<button id=b1t popovertoggletarget='p1'>Popover 1</button>
+<button id=b1s popovershowtarget='p1'>Popover 1</button>
+<button id=p1anchor>Popover1 anchor (no action)</button>
+<span id=outside>Outside all popovers</span>
+<div popover id=p1 anchor=p1anchor>
+ <span id=inside1>Inside popover 1</span>
+ <button id=b2 popovershowtarget='p2'>Popover 2</button>
+ <span id=inside1after>Inside popover 1 after button</span>
+</div>
+<div popover id=p2 anchor=b2>
+ <span id=inside2>Inside popover 2</span>
+</div>
+<button id=after_p1>Next control after popover1</button>
+<style>
+ #p1 {top: 50px;}
+ #p2 {top: 120px;}
+ [popover] {bottom:auto;}
+ [popover]::backdrop {
+ /* This should *not* affect anything: */
+ pointer-events: auto;
+ }
+</style>
+<script>
+ const popover1 = document.querySelector('#p1');
+ const button1toggle = document.querySelector('#b1t');
+ const button1show = document.querySelector('#b1s');
+ const popover1anchor = document.querySelector('#p1anchor');
+ const inside1After = document.querySelector('#inside1after');
+ const button2 = document.querySelector('#b2');
+ const popover2 = document.querySelector('#p2');
+ const outside = document.querySelector('#outside');
+ const inside1 = document.querySelector('#inside1');
+ const inside2 = document.querySelector('#inside2');
+ const afterp1 = document.querySelector('#after_p1');
+
+ let popover1HideCount = 0;
+ popover1.addEventListener('beforetoggle',(e) => {
+ if (e.newState !== "closed")
+ return;
+ ++popover1HideCount;
+ e.preventDefault(); // 'beforetoggle' should not be cancellable.
+ });
+ let popover2HideCount = 0;
+ popover2.addEventListener('beforetoggle',(e) => {
+ if (e.newState !== "closed")
+ return;
+ ++popover2HideCount;
+ e.preventDefault(); // 'beforetoggle' should not be cancellable.
+ });
+ promise_test(async () => {
+ assert_false(popover1.matches(':open'));
+ popover1.showPopover();
+ assert_true(popover1.matches(':open'));
+ let p1HideCount = popover1HideCount;
+ await clickOn(outside);
+ assert_false(popover1.matches(':open'));
+ assert_equals(popover1HideCount,p1HideCount+1);
+ },'Clicking outside a popover will dismiss the popover');
+
+ promise_test(async (t) => {
+ const controller = new AbortController();
+ t.add_cleanup(() => controller.abort());
+ function addListener(eventName) {
+ document.addEventListener(eventName,(e) => e.preventDefault(),{signal:controller.signal,capture: true});
+ }
+ addListener('pointerdown');
+ addListener('pointerup');
+ addListener('mousedown');
+ addListener('mouseup');
+ assert_false(popover1.matches(':open'));
+ popover1.showPopover();
+ assert_true(popover1.matches(':open'));
+ let p1HideCount = popover1HideCount;
+ await clickOn(outside);
+ assert_false(popover1.matches(':open'),'preventDefault should not prevent light dismiss');
+ assert_equals(popover1HideCount,p1HideCount+1);
+ },'Canceling pointer events should not keep clicks from light dismissing popovers');
+
+ promise_test(async () => {
+ assert_false(popover1.matches(':open'));
+ popover1.showPopover();
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ await clickOn(inside1);
+ assert_true(popover1.matches(':open'));
+ assert_equals(popover1HideCount,p1HideCount);
+ popover1.hidePopover();
+ },'Clicking inside a popover does not close that popover');
+
+ promise_test(async () => {
+ assert_false(popover1.matches(':open'));
+ popover1.showPopover();
+ await waitForRender();
+ assert_true(popover1.matches(':open'));
+ const actions = new test_driver.Actions();
+ await actions.pointerMove(0, 0, {origin: outside})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .send();
+ await waitForRender();
+ assert_true(popover1.matches(':open'),'pointerdown (outside the popover) should not hide the popover');
+ await actions.pointerUp({button: actions.ButtonType.LEFT})
+ .send();
+ await waitForRender();
+ assert_false(popover1.matches(':open'),'pointerup (outside the popover) should trigger light dismiss');
+ },'Popovers close on pointerup, not pointerdown');
+
+ promise_test(async () => {
+ assert_false(popover1.matches(':open'));
+ popover1.showPopover();
+ assert_true(popover1.matches(':open'));
+ async function testOne(eventName) {
+ document.body.dispatchEvent(new PointerEvent(eventName));
+ document.body.dispatchEvent(new MouseEvent(eventName));
+ document.body.dispatchEvent(new ProgressEvent(eventName));
+ await waitForRender();
+ assert_true(popover1.matches(':open'),`A synthetic "${eventName}" event should not hide the popover`);
+ }
+ await testOne('pointerup');
+ await testOne('pointerdown');
+ await testOne('mouseup');
+ await testOne('mousedown');
+ popover1.hidePopover();
+ },'Synthetic events can\'t close popovers');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ await clickOn(inside1After);
+ assert_true(popover1.matches(':open'));
+ await sendTab();
+ assert_equals(document.activeElement,afterp1,'Focus should move to a button outside the popover');
+ assert_true(popover1.matches(':open'));
+ popover1.hidePopover();
+ },'Moving focus outside the popover should not dismiss the popover');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ popover2.showPopover();
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ let p2HideCount = popover2HideCount;
+ await clickOn(inside2);
+ assert_true(popover1.matches(':open'),'popover1 should be open');
+ assert_true(popover2.matches(':open'),'popover2 should be open');
+ assert_equals(popover1HideCount,p1HideCount,'popover1');
+ assert_equals(popover2HideCount,p2HideCount,'popover2');
+ popover1.hidePopover();
+ assert_false(popover1.matches(':open'));
+ assert_false(popover2.matches(':open'));
+ },'Clicking inside a child popover shouldn\'t close either popover');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ popover2.showPopover();
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ p2HideCount = popover2HideCount;
+ await clickOn(inside1);
+ assert_true(popover1.matches(':open'));
+ assert_equals(popover1HideCount,p1HideCount);
+ assert_false(popover2.matches(':open'));
+ assert_equals(popover2HideCount,p2HideCount+1);
+ popover1.hidePopover();
+ },'Clicking inside a parent popover should close child popover');
+
+ promise_test(async () => {
+ await clickOn(button1show);
+ assert_true(popover1.matches(':open'));
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ await clickOn(button1show);
+ assert_true(popover1.matches(':open'),'popover1 should stay open');
+ assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown');
+ popover1.hidePopover(); // Cleanup
+ assert_false(popover1.matches(':open'));
+ },'Clicking on invoking element, after using it for activation, shouldn\'t close its popover');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ assert_true(popover1.matches(':open'));
+ assert_false(popover2.matches(':open'));
+ await clickOn(button2);
+ assert_true(popover2.matches(':open'),'button2 should activate popover2');
+ p2HideCount = popover2HideCount;
+ await clickOn(button2);
+ assert_true(popover2.matches(':open'),'popover2 should stay open');
+ assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown');
+ popover1.hidePopover(); // Cleanup
+ assert_false(popover1.matches(':open'));
+ assert_false(popover2.matches(':open'));
+ },'Clicking on invoking element, after using it for activation, shouldn\'t close its popover (nested case)');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ popover2.showPopover();
+ assert_true(popover1.matches(':open'));
+ assert_true(popover2.matches(':open'));
+ p2HideCount = popover2HideCount;
+ await clickOn(button2);
+ assert_true(popover2.matches(':open'),'popover2 should stay open');
+ assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown');
+ popover1.hidePopover(); // Cleanup
+ assert_false(popover1.matches(':open'));
+ assert_false(popover2.matches(':open'));
+ },'Clicking on invoking element, after using it for activation, shouldn\'t close its popover (nested case, not used for invocation)');
+
+ promise_test(async () => {
+ popover1.showPopover(); // Directly show the popover
+ assert_true(popover1.matches(':open'));
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ await clickOn(button1show);
+ assert_true(popover1.matches(':open'),'popover1 should stay open');
+ assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown');
+ popover1.hidePopover(); // Cleanup
+ assert_false(popover1.matches(':open'));
+ },'Clicking on invoking element, even if it wasn\'t used for activation, shouldn\'t close its popover');
+
+ promise_test(async () => {
+ popover1.showPopover(); // Directly show the popover
+ assert_true(popover1.matches(':open'));
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ await clickOn(button1toggle);
+ assert_false(popover1.matches(':open'),'popover1 should be hidden by popovertoggletarget');
+ assert_equals(popover1HideCount,p1HideCount+1,'popover1 should get hidden only once by popovertoggletarget');
+ },'Clicking on popovertoggletarget element, even if it wasn\'t used for activation, should hide it exactly once');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ assert_true(popover1.matches(':open'));
+ await waitForRender();
+ await clickOn(popover1anchor);
+ assert_false(popover1.matches(':open'),'popover1 should close');
+ },'Clicking on anchor element (that isn\'t an invoking element) shouldn\'t prevent its popover from being closed');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ popover2.showPopover(); // Popover1 is an ancestral element for popover2.
+ assert_true(popover1.matches(':open'));
+ assert_true(popover2.matches(':open'));
+ const drag_actions = new test_driver.Actions();
+ // Drag *from* popover2 *to* popover1 (its ancestor).
+ await drag_actions.pointerMove(0,0,{origin: popover2})
+ .pointerDown({button: drag_actions.ButtonType.LEFT})
+ .pointerMove(0,0,{origin: popover1})
+ .pointerUp({button: drag_actions.ButtonType.LEFT})
+ .send();
+ assert_true(popover1.matches(':open'),'popover1 should be open');
+ assert_true(popover2.matches(':open'),'popover1 should be open');
+ popover1.hidePopover();
+ assert_false(popover2.matches(':open'));
+ },'Dragging from an open popover outside an open popover should leave the popover open');
+</script>
+
+<button id=b3 popovertoggletarget=p3>Popover 3 - button 3
+ <div popover id=p4>Inside popover 4</div>
+</button>
+<div popover id=p3>Inside popover 3</div>
+<div popover id=p5>Inside popover 5
+ <button popovertoggletarget=p3>Popover 3 - button 4 - unused</button>
+</div>
+<style>
+ #p3 {top:100px;}
+ #p4 {top:200px;}
+ #p5 {top:200px;}
+</style>
+<script>
+ const popover3 = document.querySelector('#p3');
+ const popover4 = document.querySelector('#p4');
+ const popover5 = document.querySelector('#p5');
+ const button3 = document.querySelector('#b3');
+ promise_test(async () => {
+ await clickOn(button3);
+ assert_true(popover3.matches(':open'),'invoking element should open popover');
+ popover4.showPopover();
+ assert_true(popover4.matches(':open'));
+ assert_false(popover3.matches(':open'),'popover3 is unrelated to popover4');
+ popover4.hidePopover(); // Cleanup
+ assert_false(popover4.matches(':open'));
+ },'A popover inside an invoking element doesn\'t participate in that invoker\'s ancestor chain');
+
+ promise_test(async () => {
+ popover5.showPopover();
+ assert_true(popover5.matches(':open'));
+ assert_false(popover3.matches(':open'));
+ popover3.showPopover();
+ assert_true(popover3.matches(':open'));
+ assert_true(popover5.matches(':open'));
+ popover5.hidePopover();
+ assert_false(popover3.matches(':open'));
+ assert_false(popover5.matches(':open'));
+ },'An invoking element that was not used to invoke the popover can still be part of the ancestor chain');
+</script>
+
+<div popover id=p6>Inside popover 6
+ <div style="height:2000px;background:lightgreen"></div>
+ Bottom of popover6
+</div>
+<button popovertoggletarget=p6>Popover 6</button>
+<style>
+ #p6 {
+ width: 300px;
+ height: 300px;
+ overflow-y: scroll;
+ }
+</style>
+<script>
+ const popover6 = document.querySelector('#p6');
+ promise_test(async () => {
+ popover6.showPopover();
+ assert_equals(popover6.scrollTop,0,'popover6 should start non-scrolled');
+ await new test_driver.Actions()
+ .scroll(0, 0, 0, 50, {origin: popover6})
+ .send();
+ assert_true(popover6.matches(':open'),'popover6 should stay open');
+ assert_equals(popover6.scrollTop,50,'popover6 should be scrolled');
+ popover6.hidePopover();
+ },'Scrolling within a popover should not close the popover');
+</script>
+
+<my-element id="myElement">
+ <template shadowroot="open">
+ <button id=b7 onclick='showPopover7()'>Popover7</button>
+ <div popover id=p7 anchor=b7 style="top: 100px;">
+ <p>Popover content.</p>
+ <input id="inside7" type="text" placeholder="some text">
+ </div>
+ </template>
+</my-element>
+<script>
+ const button7 = document.querySelector('#myElement').shadowRoot.querySelector('#b7');
+ const popover7 = document.querySelector('#myElement').shadowRoot.querySelector('#p7');
+ const inside7 = document.querySelector('#myElement').shadowRoot.querySelector('#inside7');
+ function showPopover7() {
+ popover7.showPopover();
+ }
+ promise_test(async () => {
+ button7.click();
+ assert_true(popover7.matches(':open'),'invoking element should open popover');
+ inside7.click();
+ assert_true(popover7.matches(':open'));
+ popover7.hidePopover();
+ },'Clicking inside a shadow DOM popover does not close that popover');
+
+ promise_test(async () => {
+ button7.click();
+ inside7.click();
+ assert_true(popover7.matches(':open'));
+ await clickOn(outside);
+ assert_false(popover7.matches(':open'));
+ },'Clicking outside a shadow DOM popover should close that popover');
+</script>
+
+<div popover id=p8 anchor=p8anchor>
+ <button>Button</button>
+ <span id=inside8after>Inside popover 8 after button</span>
+</div>
+<button id=p8anchor>Popover8 anchor (no action)</button>
+<script>
+ promise_test(async () => {
+ const popover8 = document.querySelector('#p8');
+ const inside8After = document.querySelector('#inside8after');
+ const popover8Anchor = document.querySelector('#p8anchor');
+ assert_false(popover8.matches(':open'));
+ popover8.showPopover();
+ await clickOn(inside8After);
+ assert_true(popover8.matches(':open'));
+ await sendTab();
+ assert_equals(document.activeElement,popover8Anchor,'Focus should move to the anchor element');
+ assert_true(popover8.matches(':open'),'popover should stay open');
+ popover8.hidePopover();
+ },'Moving focus back to the anchor element should not dismiss the popover');
+</script>
+
+<!-- Convoluted ancestor relationship -->
+<div popover id=convoluted_p1>Popover 1
+ <div id=convoluted_anchor>Anchor
+ <button popovertoggletarget=convoluted_p2>Open Popover 2</button>
+ <div popover id=convoluted_p4><p>Popover 4</p></div>
+ </div>
+</div>
+<div popover id=convoluted_p2 anchor=convoluted_p2>Popover 2 (self-anchor-linked)
+ <button popovertoggletarget=convoluted_p3>Open Popover 3</button>
+ <button popovershowtarget=convoluted_p2>Self-linked invoker</button>
+</div>
+<div popover id=convoluted_p3 anchor=convoluted_anchor>Popover 3
+ <button popovertoggletarget=convoluted_p4>Open Popover 4</button>
+</div>
+<button onclick="convoluted_p1.showPopover()">Open convoluted popover</button>
+<style>
+ #convoluted_p1 {top:50px;}
+ #convoluted_p2 {top:150px;}
+ #convoluted_p3 {top:250px;}
+ #convoluted_p4 {top:350px;}
+</style>
+<script>
+const convPopover1 = document.querySelector('#convoluted_p1');
+const convPopover2 = document.querySelector('#convoluted_p2');
+const convPopover3 = document.querySelector('#convoluted_p3');
+const convPopover4 = document.querySelector('#convoluted_p4');
+promise_test(async () => {
+ convPopover1.showPopover(); // Programmatically open p1
+ assert_true(convPopover1.matches(':open'));
+ convPopover1.querySelector('button').click(); // Click to invoke p2
+ assert_true(convPopover1.matches(':open'));
+ assert_true(convPopover2.matches(':open'));
+ convPopover2.querySelector('button').click(); // Click to invoke p3
+ assert_true(convPopover1.matches(':open'));
+ assert_true(convPopover2.matches(':open'));
+ assert_true(convPopover3.matches(':open'));
+ convPopover3.querySelector('button').click(); // Click to invoke p4
+ assert_true(convPopover1.matches(':open'));
+ assert_true(convPopover2.matches(':open'));
+ assert_true(convPopover3.matches(':open'));
+ assert_true(convPopover4.matches(':open'));
+ convPopover4.firstElementChild.click(); // Click within p4
+ assert_true(convPopover1.matches(':open'));
+ assert_true(convPopover2.matches(':open'));
+ assert_true(convPopover3.matches(':open'));
+ assert_true(convPopover4.matches(':open'));
+ convPopover1.hidePopover();
+ assert_false(convPopover1.matches(':open'));
+ assert_false(convPopover2.matches(':open'));
+ assert_false(convPopover3.matches(':open'));
+ assert_false(convPopover4.matches(':open'));
+},'Ensure circular/convoluted ancestral relationships are functional');
+
+promise_test(async () => {
+ convPopover1.showPopover(); // Programmatically open p1
+ convPopover1.querySelector('button').click(); // Click to invoke p2
+ assert_true(convPopover1.matches(':open'));
+ assert_true(convPopover2.matches(':open'));
+ assert_false(convPopover3.matches(':open'));
+ assert_false(convPopover4.matches(':open'));
+ convPopover4.showPopover(); // Programmatically open p4
+ assert_true(convPopover1.matches(':open'),'popover1 stays open because it is a DOM ancestor of popover4');
+ assert_false(convPopover2.matches(':open'),'popover2 closes because it isn\'t connected to popover4 via active invokers');
+ assert_true(convPopover4.matches(':open'));
+ convPopover4.firstElementChild.click(); // Click within p4
+ assert_true(convPopover1.matches(':open'),'nothing changes');
+ assert_false(convPopover2.matches(':open'));
+ assert_true(convPopover4.matches(':open'));
+ convPopover1.hidePopover();
+ assert_false(convPopover1.matches(':open'));
+ assert_false(convPopover2.matches(':open'));
+ assert_false(convPopover3.matches(':open'));
+ assert_false(convPopover4.matches(':open'));
+},'Ensure circular/convoluted ancestral relationships are functional, with a direct showPopover()');
+</script>
+
+<div popover id=p13>Popover 1
+ <div popover id=p14>Popover 2
+ <div popover id=p15>Popover 3</div>
+ </div>
+</div>
+<style>
+ #p13 {top: 100px;}
+ #p14 {top: 200px;}
+ #p15 {top: 300px;}
+</style>
+<script>
+promise_test(async () => {
+ const p13 = document.querySelector('#p13');
+ const p14 = document.querySelector('#p14');
+ const p15 = document.querySelector('#p15');
+ p13.showPopover();
+ p14.showPopover();
+ p15.showPopover();
+ p15.addEventListener('beforetoggle', (e) => {
+ if (e.newState !== "closed")
+ return;
+ p14.hidePopover();
+ },{once:true});
+ assert_true(p13.matches(':open') && p14.matches(':open') && p15.matches(':open'),'all three should be open');
+ p14.hidePopover();
+ assert_true(p13.matches(':open'),'p13 should still be open');
+ assert_false(p14.matches(':open'));
+ assert_false(p15.matches(':open'));
+},'Hide the target popover during "hide all popovers until"');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-manual-crash.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-manual-crash.tentative.html
new file mode 100644
index 0000000000..d721f7c731
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-manual-crash.tentative.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8" />
+<title>Popover=manual crash test</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<style>
+[popover] {top: 100px; bottom: auto;}
+[popover=""] {left: -200px}
+[popover=auto] {left: 0; }
+[popover=manual] {left: 200px; }
+</style>
+
+<p>This test passes if it does not crash.</p>
+<div popover>Auto1
+ <div popover=auto>Auto2</div>
+</div>
+<div popover=manual>Manual</div>
+<script>
+ document.querySelectorAll('[popover]').forEach(p => p.showPopover());
+ const manual = document.querySelector('[popover=manual]');
+ clickOn(manual)
+ .then(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-not-keyboard-focusable.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-not-keyboard-focusable.tentative.html
new file mode 100644
index 0000000000..815ae04ebb
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-not-keyboard-focusable.tentative.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover keyboard focus behaviors</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.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>
+
+<button id=firstfocus>Button 1</button>
+<div popover>
+ <p>This is a popover without a focusable element</p>
+</div>
+<button id=secondfocus>Button 2</button>
+
+<script>
+promise_test(async () => {
+ const b1 = document.getElementById('firstfocus');
+ const b2 = document.getElementById('secondfocus');
+ const popover = document.querySelector('[popover]');
+ b1.focus();
+ assert_equals(document.activeElement,b1);
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ assert_equals(document.activeElement,b1);
+ // Tab once
+ await new test_driver.send_keys(document.body,'\uE004'); // Tab
+ assert_equals(document.activeElement, b2, 'Keyboard focus should skip the open popover');
+ assert_true(popover.matches(':open'),'changing focus should not close the popover');
+ popover.hidePopover();
+
+ // Add a focusable button to the popover and make sure we can focus that
+ const button = document.createElement('button');
+ popover.appendChild(button);
+ b1.focus();
+ popover.showPopover();
+ assert_equals(document.activeElement,b1);
+ // Tab once
+ await new test_driver.send_keys(document.body,'\uE004'); // Tab
+ assert_equals(document.activeElement, button, 'Keyboard focus should go to the contained button');
+ assert_true(popover.matches(':open'),'changing focus to the popover should leave it showing');
+ popover.hidePopover();
+ assert_false(popover.matches(':open'));
+}, "Popover should not be keyboard focusable");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-open-display-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-open-display-ref.tentative.html
new file mode 100644
index 0000000000..144b81e645
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-display-ref.tentative.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel="stylesheet" href="resources/popover-styles.css">
+
+<div class=topmost></div>
+<div class=fake-popover>This is a popover</div>
+
+<style>
+ .topmost {
+ position:fixed;
+ top:0;
+ left:0;
+ width:1000px;
+ height:1000px;
+ background:green;
+ margin:0;
+ padding:0;
+ }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-open-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-open-display.tentative.html
new file mode 100644
index 0000000000..56e63d0f37
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-display.tentative.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-open-display-ref.tentative.html">
+
+<div popover>This is a popover</div>
+<div class=topmost></div>
+
+<style>
+ .topmost {
+ position:fixed;
+ z-index: 999999;
+ top:0;
+ left:0;
+ width:1000px;
+ height:1000px;
+ background:green;
+ margin:0;
+ padding:0;
+ }
+</style>
+
+<script>
+ document.querySelector('[popover]').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-ref.tentative.html
new file mode 100644
index 0000000000..0d14050e85
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-ref.tentative.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+
+<div popover id=p1>This is popover 1<div id=anchor2></div></div>
+<div popover id=p2 anchor=anchor2>This is popover 2<div id=anchor3></div></div>
+<div popover id=p3 anchor=anchor3>This is popover 3</div>
+
+<style>
+ #p2 {
+ top: 100px;
+ }
+ #p3 {
+ top:200px;
+ }
+</style>
+
+<script>
+ document.querySelector('#p1').showPopover();
+ document.querySelector('#p2').showPopover();
+ document.querySelector('#p3').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display.tentative.html
new file mode 100644
index 0000000000..cae628a13f
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display.tentative.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-open-overflow-display-ref.tentative.html">
+
+<div id=container>
+ <div popover id=p1>This is popover 1<div id=anchor2></div></div>
+ <div popover id=p2 anchor=anchor2>This is popover 2<div id=anchor3></div></div>
+ <div popover id=p3 anchor=anchor3>This is popover 3</div>
+</div>
+
+<style>
+ #container {
+ overflow:hidden;
+ position: absolute;
+ top: 100px;
+ left: 50px;
+ width: 30px;
+ height: 30px;
+ }
+ #p2 {
+ position: absolute;
+ top: 100px;
+ }
+ #p3 {
+ position: relative;
+ top:200px;
+ }
+</style>
+
+<script>
+ document.querySelector('#p1').showPopover();
+ document.querySelector('#p2').showPopover();
+ document.querySelector('#p3').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-removal-2.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-removal-2.tentative.html
new file mode 100644
index 0000000000..b7b185d58d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-removal-2.tentative.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover document removal behavior</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id=frame1 srcdoc="<div popover id=popover>Popover</div>"></iframe>
+<iframe id=frame2></iframe>
+
+<script>
+ window.onload = () => {
+ test(t => {
+ const frame1Doc = document.getElementById('frame1').contentDocument;
+ const frame2Doc = document.getElementById('frame2').contentDocument;
+ const popover = frame1Doc.querySelector('[popover]');
+ assert_true(!!popover);
+ assert_false(popover.matches(':open'));
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ frame2Doc.body.appendChild(popover);
+ assert_false(popover.matches(':open'));
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ }, 'Moving popover between documents shouldn\'t cause issues');
+ };
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-removal.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-removal.tentative.html
new file mode 100644
index 0000000000..aeed3b678d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-removal.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover document removal behavior</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div popover id=popover>Popover</div>
+
+<script>
+promise_test(async t => {
+ function loadCompleted() {
+ return new Promise(resolve => {
+ window.addEventListener('load', resolve);
+ });
+ }
+ const popover = document.querySelector('[popover]');
+ assert_false(popover.matches(':open'));
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ popover.remove(); // Shouldn't cause any issues
+ document.body.click(); // Shouldn't cause light dismiss problems
+ await loadCompleted(); // The document should finish loading
+}, 'Removal from the document shouldn\'t cause issues');
+
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-shadow-dom.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-shadow-dom.tentative.html
new file mode 100644
index 0000000000..72bbe1e893
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-shadow-dom.tentative.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<script>
+ function ensureShadowDom(host) {
+ host.querySelectorAll('my-element').forEach(host => {
+ if (host.shadowRoot)
+ return; // Declarative Shadow DOM is enabled
+ const template = host.firstElementChild;
+ assert_true(template instanceof HTMLTemplateElement);
+ const shadow = host.attachShadow({mode: 'open'});
+ shadow.appendChild(template.content);
+ template.remove();
+ })
+ }
+ function findPopovers(root) {
+ let popovers = [];
+ if (!root)
+ return popovers;
+ if (root instanceof Element && root.matches('[popover]'))
+ popovers.push(root);
+ popovers.push(...findPopovers(root.shadowRoot));
+ root.childNodes.forEach(child => {
+ popovers.push(...findPopovers(child));
+ })
+ return popovers;
+ }
+ function getPopoverReferences(testId) {
+ const testRoot = document.querySelector(`#${testId}`);
+ assert_true(!!testRoot);
+ ensureShadowDom(testRoot);
+ return findPopovers(testRoot);
+ }
+ function showTestPopover(testId,popoverNum) {
+ getPopoverReferences(testId)[popoverNum].showPopover();
+ }
+</script>
+
+<div id=test1>
+ <button onclick='showTestPopover("test1",0)'>Test1 Popover</button>
+ <my-element>
+ <template shadowroot=open>
+ <div popover>
+ <p>This should show, even though it is inside shadow DOM.</p>
+ </div>
+ </template>
+ </my-element>
+</div>
+
+<script>
+ test(function() {
+ const popover = getPopoverReferences('test1')[0];
+ popover.showPopover();
+ assert_true(popover.matches(':open'));
+ assert_true(isElementVisible(popover));
+ popover.hidePopover(); // Cleanup
+ }, "Popovers located inside shadow DOM can still be shown");
+</script>
+
+
+<div id=test2>
+ <button id=t2b1 onclick='showTestPopover("test2",0)'>Test 2 Popover 1</button>
+ <div popover anchor=t2b1 style="top: 200px;">
+ <p>Popover 1</p>
+ <button id=t2b2 onclick='showTestPopover("test2",1)'>Test 2 Popover 2</button>
+ </div>
+ <my-element>
+ <template shadowroot=open>
+ <div popover anchor=t2b2 style="top: 400px;">
+ <p>Hiding this popover will hide *all* open popovers,</p>
+ <p>because t2b2 doesn't exist in this context.</p>
+ </div>
+ </template>
+ </my-element>
+</div>
+
+<script>
+ test(function() {
+ const [popover1,popover2] = getPopoverReferences('test2');
+ popover1.showPopover();
+ assert_true(popover1.matches(':open'));
+ assert_true(isElementVisible(popover1));
+ popover2.showPopover();
+ assert_false(popover1.matches(':open'), 'popover1 open'); // P1 was closed by P2
+ assert_false(isElementVisible(popover1), 'popover1 visible');
+ assert_true(popover2.matches(':open'), 'popover2 open'); // P2 is open
+ assert_true(isElementVisible(popover2), 'popover2 visible');
+ popover2.hidePopover(); // Cleanup
+ }, "anchor references do not cross shadow boundaries");
+</script>
+
+
+<div id=test3>
+ <my-element>
+ <template shadowroot=open>
+ <button id=t3b1 onclick='showTestPopover("test3",0)'>Test 3 Popover 1</button>
+ <div popover anchor=t3b1>
+ <p>This popover will stay open when popover2 shows.</p>
+ <slot></slot>
+ </div>
+ </template>
+ <button id=t3b2 onclick='showTestPopover("test3",1)'>Test 3 Popover 2</button>
+ </my-element>
+ <div popover anchor=t3b2>Popover 2</div>
+</div>
+
+<script>
+ promise_test(async function() {
+ const [popover1,popover2] = getPopoverReferences('test3');
+ popover1.showPopover();
+ assert_true(popover1.matches(':open'));
+ assert_true(isElementVisible(popover1));
+ // Showing popover2 should not close popover1, since it is a flat
+ // tree ancestor of popover2's anchor button.
+ popover2.showPopover();
+ assert_true(popover2.matches(':open'));
+ assert_true(isElementVisible(popover2));
+ assert_true(popover1.matches(':open'));
+ assert_true(isElementVisible(popover1));
+ popover1.hidePopover();
+ await waitForRender();
+ assert_false(popover1.matches(':open'));
+ assert_false(isElementVisible(popover1));
+ assert_false(popover2.matches(':open'));
+ assert_false(isElementVisible(popover2));
+ }, "anchor references use the flat tree not the DOM tree");
+</script>
+
+
+<div id=test4>
+ <button id=t4b1 onclick='showTestPopover("test4",0)'>Test 4 Popover 1</button>
+ <div popover anchor=t4b1>
+ <p>This should not get hidden when popover2 opens.</p>
+ <my-element>
+ <template shadowroot=open>
+ <button id=t4b2 onclick='showTestPopover("test4",1)'>Test 4 Popover 2</button>
+ <div popover anchor=t4b2>
+ <p>This should not hide popover1.</p>
+ </div>
+ </template>
+ </my-element>
+ </div>
+</div>
+
+<script>
+ promise_test(async function() {
+ const [popover1,popover2] = getPopoverReferences('test4');
+ popover1.showPopover();
+ popover2.showPopover();
+ // Both 1 and 2 should be open at this point.
+ assert_true(popover1.matches(':open'), 'popover1 not open');
+ assert_true(isElementVisible(popover1));
+ assert_true(popover2.matches(':open'), 'popover2 not open');
+ assert_true(isElementVisible(popover2));
+ // This should hide both of them.
+ popover1.hidePopover();
+ await waitForRender();
+ assert_false(popover1.matches(':open'));
+ assert_false(isElementVisible(popover1));
+ assert_false(popover2.matches(':open'));
+ assert_false(isElementVisible(popover2));
+ }, "The popover stack is preserved across shadow-inclusive ancestors");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context-ref.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context-ref.tentative.html
new file mode 100644
index 0000000000..4d4ca6973f
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context-ref.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="stylesheet" href="resources/popover-styles.css">
+
+<div class="fake-popover">
+ Inside popover
+ <div class=z style="z-index: 2; background:lightgreen">z-index 2
+ <div class=z style="z-index: 3; background:lightblue; left: 20px;">z-index 3</div>
+ <div class=z style="z-index: 1; background:pink; top:-20px; left: 10px;">z-index 1</div>
+ </div>
+ <div class=z style="background:green; top:-100px; left: 250px; width: 100px;">Outside</div>
+ Bottom of popover
+</div>
+
+<style>
+ .fake-popover {
+ width: 200px;
+ height: 230px;
+ border: 1px solid red;
+ top:50px;
+ left:50px;
+ }
+ .z {
+ position: relative;
+ border: 1px solid black;
+ padding: 1em;
+ }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context.tentative.html
new file mode 100644
index 0000000000..b5d0d651d3
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context.tentative.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<link rel=match href="popover-stacking-context-ref.tentative.html">
+
+<div popover>
+ Inside popover
+ <div class=z style="z-index: 2; background:lightgreen">z-index 2
+ <div class=z style="z-index: 3; background:lightblue; left: 20px;">z-index 3</div>
+ <div class=z style="z-index: 1; background:pink; top:-20px; left: 10px;">z-index 1</div>
+ </div>
+ <div class=z style="background:green; top:-100px; left: 250px; width: 100px;">Outside</div>
+ Bottom of popover
+</div>
+
+<style>
+ [popover] {
+ width: 200px;
+ height: 230px;
+ border: 1px solid red;
+ top:50px;
+ left:50px;
+ }
+ .z {
+ position: relative;
+ border: 1px solid black;
+ padding: 1em;
+ }
+</style>
+
+<script>
+ document.querySelector('[popover]').showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-stacking.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-stacking.tentative.html
new file mode 100644
index 0000000000..dc07c1c208
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-stacking.tentative.html
@@ -0,0 +1,172 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- Enumerate all the ways of creating an ancestor popover relationship -->
+
+<div class="example">
+ <p>Direct DOM children</p>
+ <div popover class=ancestor><p>Ancestor popover</p>
+ <div popover class=child><p>Child popover</p></div>
+ </div>
+</div>
+
+<div class="example">
+ <p>Grandchildren</p>
+ <div popover class=ancestor><p>Ancestor popover</p>
+ <div>
+ <div>
+ <div popover class=child><p>Child popover</p></div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="example">
+ <p>popovertoggletarget attribute relationship</p>
+ <div popover class=ancestor><p>Ancestor popover</p>
+ <button popovertoggletarget=trigger1 class=clickme>Button</button>
+ </div>
+ <div id=trigger1 popover class=child><p>Child popover</p></div>
+</div>
+
+<div class="example">
+ <p>nested popovertoggletarget attribute relationship</p>
+ <div popover class=ancestor><p>Ancestor popover</p>
+ <div>
+ <div>
+ <button popovertoggletarget=trigger2 class=clickme>Button</button>
+ </div>
+ </div>
+ </div>
+ <div id=trigger2 popover class=child><p>Child popover</p></div>
+</div>
+
+<div class="example">
+ <p>anchor attribute relationship</p>
+ <div id=anchor1 popover class=ancestor><p>Ancestor popover</p></div>
+ <div anchor=anchor1 popover class=child><p>Child popover</p></div>
+</div>
+
+<div class="example">
+ <p>indirect anchor attribute relationship</p>
+ <div popover class=ancestor>
+ <p>Ancestor popover</p>
+ <div>
+ <div>
+ <span id=anchor2>Anchor</span>
+ </div>
+ </div>
+ </div>
+ <div anchor=anchor2 popover class=child><p>Child popover</p></div>
+</div>
+
+<!-- Other examples -->
+
+<div popover id=p1 anchor=b1><p>This is popover #1</p>
+ <button id=b2 onclick='p2.showPopover()'>Popover 2</button>
+ <button id=b4 onclick='p4.showPopover()'>Popover 4</button>
+</div>
+<div popover id=p2 anchor=b2><p>This is popover #2</p>
+ <button id=b3 onclick='p3.showPopover()'>Popover 3</button>
+</div>
+<div popover id=p3 anchor=b3><p>This is popover #3</p></div>
+<div popover id=p4 anchor=b4><p>This is popover #4</p></div>
+<button id=b1 onclick='p1.showPopover()'>Popover 1</button>
+
+<dialog id=d1>This is a dialog<button onclick='this.parentElement.close()'>Close</button></dialog>
+<button id=b5 onclick='d1.showPopover()'>Dialog</button>
+
+<script>
+ // Test basic ancestor relationships
+ for(let example of document.querySelectorAll('.example')) {
+ const descr = example.querySelector('p').textContent;
+ const ancestor = example.querySelector('[popover].ancestor');
+ const child = example.querySelector('[popover].child');
+ const clickToActivate = example.querySelector('.clickme');
+ test(function() {
+ assert_true(!!descr && !!ancestor && !!child);
+ assert_false(ancestor.matches(':open'));
+ assert_false(child.matches(':open'));
+ ancestor.showPopover();
+ if (clickToActivate)
+ clickToActivate.click();
+ else
+ child.showPopover();
+ assert_true(child.matches(':open'));
+ assert_true(ancestor.matches(':open'));
+ ancestor.hidePopover();
+ assert_false(ancestor.matches(':open'));
+ assert_false(child.matches(':open'));
+ },descr);
+ }
+
+ const popovers = [p1, p2, p3, p4];
+
+ function assertState(...states) {
+ assert_equals(popovers.length,states.length);
+ for(let i=0;i<popovers.length;++i) {
+ assert_equals(popovers[i].matches(':open'),states[i],`Popover #${i+1} incorrect state`);
+ }
+ }
+ test(function() {
+ assertState(false,false,false,false);
+ p1.showPopover();
+ assertState(true,false,false,false);
+ p2.showPopover();
+ assertState(true,true,false,false);
+ p3.showPopover();
+ assertState(true,true,true,false);
+ // P4 is a sibling of P2, so showing it should
+ // close P2 and P3.
+ p4.showPopover();
+ assertState(true,false,false,true);
+ // P2 should close P4 now.
+ p2.showPopover();
+ assertState(true,true,false,false);
+ // Hiding P1 should hide all.
+ p1.hidePopover();
+ assertState(false,false,false,false);
+ }, "more complex nesting, all using anchor ancestry")
+
+ test(function() {
+ function openManyPopovers() {
+ p1.showPopover();
+ p2.showPopover();
+ p3.showPopover();
+ assertState(true,true,true,false);
+ }
+ openManyPopovers();
+ d1.show(); // Dialog.show() should hide all popovers.
+ assertState(false,false,false,false);
+ d1.close();
+ openManyPopovers();
+ d1.showModal(); // Dialog.showModal() should also hide all popovers.
+ assertState(false,false,false,false);
+ d1.close();
+ }, "popovers should be closed by dialogs")
+
+ test(function() {
+ // Note: d1 is a <dialog> element, not a popover.
+ assert_false(d1.open);
+ d1.show();
+ assert_true(d1.open);
+ p1.showPopover();
+ assertState(true,false,false,false);
+ assert_true(d1.open);
+ p1.hidePopover();
+ assert_true(d1.open);
+ d1.close();
+ assert_false(d1.open);
+ }, "dialogs should not be closed by popovers")
+</script>
+
+<style>
+ #p1 { top:350px; }
+ #p2 { top:350px; left:200px; }
+ #p3 { top:500px; }
+ #p4 { top:500px;left:200px; }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-combinations.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-combinations.tentative.html
new file mode 100644
index 0000000000..0e04f30481
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-combinations.tentative.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popover combined with dialog/fullscreen behavior</title>
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.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/popover-utils.js"></script>
+
+<button id=visible>Visible button</button>
+<div id=examples>
+ <dialog popover>Popover Dialog</dialog>
+ <dialog popover open style="top:50px;">Open Non-modal Popover Dialog</dialog>
+ <div popover class=fullscreen>Fullscreen Popover</div>
+ <dialog popover class=fullscreen>Fullscreen Popover Dialog</dialog>
+ <dialog popover open class=fullscreen style="top:200px;">Fullscreen Open Non-modal Popover Dialog</dialog>
+</div>
+
+<style>
+ [popover] {
+ inset:auto;
+ top:0;
+ left:0;
+ }
+ [popover].fullscreen.visible {
+ display:block;
+ }
+</style>
+
+<script>
+const isDialog = (ex) => ex instanceof HTMLDialogElement;
+const isFullscreen = (ex) => ex.classList.contains('fullscreen');
+function ensureIsOpenPopover(ex,message) {
+ // Because :open will eventually support <dialog>, this does extra work to
+ // verify we're dealing with an :open Popover. Note that this will also throw
+ // if this is an element with the `popover` attribute that has been made
+ // visible via an explicit `display:block` style rule.
+ message = message || 'Error';
+ assert_true(ex.matches(':open'),`${message}: Popover doesn\'t match :open`);
+ assert_false(ex.matches(':closed'),`${message}: Popover matches :closed`);
+ ex.hidePopover(); // Shouldn't throw if this is a showing popover
+ ex.showPopover(); // Show it again to avoid state change
+ assert_true(ex.matches(':open') && !ex.matches(':closed'),`${message}: Sanity`);
+}
+window.onload = () => requestAnimationFrame(() => requestAnimationFrame(() => {
+ const examples = Array.from(document.querySelectorAll('#examples>*'));
+ examples.forEach(ex => {
+ promise_test(async (t) => {
+ t.add_cleanup(() => ex.remove());
+ // Test initial conditions
+ if (ex.hasAttribute('open')) {
+ assert_true(isDialog(ex));
+ assert_true(isElementVisible(ex),'Open dialog should be visible by default');
+ assert_throws_dom("InvalidStateError",() => ex.showPopover(),'Calling showPopover on an already-showing element should throw InvalidStateError');
+ ex.removeAttribute('open');
+ assert_false(isElementVisible(ex),'Removing the open attribute should hide the dialog');
+ } else {
+ ex.showPopover(); // Should not throw
+ ensureIsOpenPopover(ex,'showPopover should work');
+ ex.hidePopover(); // Should not throw
+ assert_true(ex.matches(':closed'),'hidePopover should work');
+ }
+ assert_false(isElementVisible(ex));
+
+ // Start with popover, try the other API
+ ex.showPopover();
+ ensureIsOpenPopover(ex);
+ let tested_something=false;
+ if (isDialog(ex)) {
+ tested_something=true;
+ assert_throws_dom("InvalidStateError",() => ex.showModal(),'Calling showModal() on an already-showing Popover should throw InvalidStateError');
+ assert_throws_dom("InvalidStateError",() => ex.show(),'Calling show() on an already-showing Popover should throw InvalidStateError');
+ }
+ if (isFullscreen(ex)) {
+ tested_something=true;
+ let requestSucceeded = false;
+ await blessTopLayer(ex);
+ await ex.requestFullscreen()
+ .then(() => {requestSucceeded = true;}) // We should not hit this.
+ .catch((exception) => {
+ // This exception is expected.
+ assert_equals(exception.name,'TypeError',`Invalid exception from requestFullscreen() (${exception.message})`);
+ });
+ assert_false(requestSucceeded,'requestFullscreen() should not succeed when the element is an already-showing Popover');
+ }
+ assert_true(tested_something);
+ ensureIsOpenPopover(ex);
+ ex.hidePopover();
+
+ // Start with the other API, then try popover
+ if (isDialog(ex)) {
+ ex.show();
+ assert_true(ex.hasAttribute('open'));
+ assert_throws_dom("InvalidStateError",() => ex.showPopover(),'Calling showPopover() on an already-showing non-modal dialog should throw InvalidStateError');
+ ex.close();
+ assert_false(ex.hasAttribute('open'));
+ ex.showModal();
+ assert_true(ex.hasAttribute('open'));
+ assert_throws_dom("InvalidStateError",() => ex.showPopover(),'Calling showPopover() on an already-showing modal dialog should throw InvalidStateError');
+ ex.close();
+ assert_false(ex.hasAttribute('open'));
+ } else if (isFullscreen(ex)) {
+ let requestSucceeded = false;
+ await blessTopLayer(visible);
+ await ex.requestFullscreen()
+ .then(() => {
+ assert_throws_dom("InvalidStateError",() => ex.showPopover(),'Calling showPopover() on an already-fullscreen element should throw InvalidStateError');
+ });
+ await document.exitFullscreen()
+ .then(() => assert_true(true));
+ }
+
+ // Finally, try invoking these combined popovers via a declarative invoker
+ const button = document.createElement('button');
+ t.add_cleanup(() => button.remove());
+ document.body.appendChild(button);
+ assert_equals(ex.id,'');
+ ex.id = 'popover-id';
+ button.popoverToggleTarget = ex.id;
+ assert_true(ex.matches(':closed'));
+ await clickOn(button);
+ ensureIsOpenPopover(ex,'Invoking element should be able to invoke all popovers');
+ ex.hidePopover();
+ if (isDialog(ex)) {
+ ex.showModal();
+ assert_true(ex.hasAttribute('open'));
+ } else if (isFullscreen(ex)) {
+ // Popover fullscreen isn't visible by default, so explicitly add
+ // display:block, so that calls to "clickOn" can succeed.
+ ex.classList.add('visible');
+ await blessTopLayer(visible);
+ await ex.requestFullscreen();
+ } else {
+ assert_unreached('Not a dialog or fullscreen');
+ }
+ ex.appendChild(button); // Add button to the element, so it's visible to click
+ await clickOn(button);
+ assert_true(ex.matches(':closed'),'The invoker click should have failed on the already-open dialog/fullscreen');
+ if (isDialog(ex)) {
+ ex.close();
+ } else {
+ await document.exitFullscreen()
+ }
+ }, `Popover combination: ${ex.textContent}`);
+ });
+}));
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-interactions.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-interactions.tentative.html
new file mode 100644
index 0000000000..50a21be7f7
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-interactions.tentative.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Interactions between top layer element types</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<body>
+<script>
+const types = Object.freeze({
+ popover: Symbol("Popover API"),
+ modalDialog: Symbol("Modal Dialog"),
+ fullscreen: Symbol("Fullscreen Element"),
+});
+const examples = [
+ {
+ type: types.popover,
+ closes: [types.popover],
+ createElement: () => Object.assign(document.createElement('div'), {popover: 'auto'}),
+ trigger: function() {this.element.showPopover()},
+ close: function() {this.element.hidePopover()},
+ isTopLayer: function() {return this.element.matches(':open')},
+ },
+ {
+ type: types.modalDialog,
+ closes: [types.popover],
+ createElement: () => document.createElement('dialog'),
+ trigger: function() {this.element.showModal();this.showing=true;},
+ close: function() {this.element.close();this.showing=false;},
+ isTopLayer: function() {return !!(this.element.isConnected && this.showing);},
+ },
+ {
+ type: types.fullscreen,
+ closes: [types.popover, types.fullscreen],
+ createElement: () => document.createElement('div'),
+ trigger: async function(visibleElement) {assert_false(this.isTopLayer());await blessTopLayer(visibleElement);await this.element.requestFullscreen();},
+ close: function() {assert_equals(this.element,document.fullscreenElement); document.exitFullscreen();},
+ isTopLayer: function() {return this.element.matches(':fullscreen');},
+ },
+];
+
+function createElement(ex) {
+ assert_true(!ex.element);
+ const element = ex.element = ex.createElement();
+ assert_true(!!element);
+ element.appendChild(document.createTextNode(`This is a ${ex.type.description}`));
+ document.body.appendChild(element);
+ assert_false(ex.isTopLayer(),'Element should start out not in the top layer');
+ return element;
+}
+function doneWithExample(ex) {
+ assert_true(!!ex.element);
+ if (ex.isTopLayer())
+ ex.close();
+ ex.element.remove();
+ ex.element = null;
+}
+// Test interactions between top layer elements
+for(let i=0;i<examples.length;++i) {
+ for(let j=0;j<examples.length;++j) {
+ const example1 = Object.assign([],examples[i]);
+ const example2 = Object.assign([],examples[j]);
+ const shouldClose = example2.closes.includes(example1.type);
+ const desc = `A ${example2.type.description} should ${shouldClose ? "" : "*not*"} close a ${example1.type.description}`;
+ promise_test(async t => {
+ const element1 = createElement(example1);
+ const element2 = createElement(example2);
+ t.add_cleanup(() => {doneWithExample(example1);doneWithExample(example2);});
+ await example1.trigger(document.body); // Open the 1st top layer element
+ assert_true(example1.isTopLayer()); // Make sure it is top layer
+ await example2.trigger(element1); // Open the 2nd top layer element
+ assert_true(example2.isTopLayer()); // Make sure it is top layer
+ assert_equals(shouldClose,!example1.isTopLayer(),desc);
+ },desc);
+ }
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-types.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-types.tentative.html
new file mode 100644
index 0000000000..615c5a818c
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-types.tentative.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div>
+ <div popover>Popover</div>
+ <div popover=manual>Async</div>
+ <div popover=manual>Async</div>
+ <script>
+ {
+ const auto = document.currentScript.parentElement.querySelector('[popover=""]');
+ const manual = document.currentScript.parentElement.querySelectorAll('[popover=manual]')[0];
+ const manual2 = document.currentScript.parentElement.querySelectorAll('[popover=manual]')[1];
+ function assert_state_1(autoOpen,manualOpen,manual2Open) {
+ assert_equals(auto.matches(':open'),autoOpen,'auto open state is incorrect');
+ assert_equals(manual.matches(':open'),manualOpen,'manual open state is incorrect');
+ assert_equals(manual2.matches(':open'),manual2Open,'manual2 open state is incorrect');
+ }
+ test(() => {
+ assert_state_1(false,false,false);
+ auto.showPopover();
+ assert_state_1(true,false,false);
+ manual.showPopover();
+ assert_state_1(true,true,false);
+ manual2.showPopover();
+ assert_state_1(true,true,true);
+ auto.hidePopover();
+ assert_state_1(false,true,true);
+ manual.hidePopover();
+ assert_state_1(false,false,true);
+ manual2.hidePopover();
+ assert_state_1(false,false,false);
+ },'manuals do not close popovers');
+ }
+ </script>
+</div>
diff --git a/testing/web-platform/tests/html/semantics/popovers/resources/popover-styles.css b/testing/web-platform/tests/html/semantics/popovers/resources/popover-styles.css
new file mode 100644
index 0000000000..df683c3c64
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/resources/popover-styles.css
@@ -0,0 +1,17 @@
+.fake-popover {
+ position: fixed;
+ inset: 0;
+ width: fit-content;
+ height: fit-content;
+ margin: auto;
+ border: solid;
+ padding: 0.25em;
+ overflow: auto;
+ color: CanvasText;
+ background-color: Canvas;
+}
+.fake-popover-backdrop {
+ position: fixed;
+ inset:0;
+ pointer-events: none !important;
+}
diff --git a/testing/web-platform/tests/html/semantics/popovers/resources/popover-utils.js b/testing/web-platform/tests/html/semantics/popovers/resources/popover-utils.js
new file mode 100644
index 0000000000..0df24ccd4f
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/resources/popover-utils.js
@@ -0,0 +1,109 @@
+function waitForRender() {
+ 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})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .send();
+ await waitForRender();
+}
+async function sendTab() {
+ await waitForRender();
+ const kTab = '\uE004';
+ await new test_driver.send_keys(document.body,kTab);
+ await waitForRender();
+}
+// Waiting for crbug.com/893480:
+// async function sendShiftTab() {
+// await waitForRender();
+// const kShift = '\uE008';
+// const kTab = '\uE004';
+// await new test_driver.Actions()
+// .keyDown(kShift)
+// .keyDown(kTab)
+// .keyUp(kTab)
+// .keyUp(kShift)
+// .send();
+// await waitForRender();
+// }
+async function sendEscape() {
+ await waitForRender();
+ await new test_driver.send_keys(document.body,'\uE00C'); // Escape
+ await waitForRender();
+}
+async function sendEnter() {
+ await waitForRender();
+ await new test_driver.send_keys(document.body,'\uE007'); // Enter
+ await waitForRender();
+}
+function isElementVisible(el) {
+ return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
+}
+async function finishAnimations(popover) {
+ popover.getAnimations({subtree: true}).forEach(animation => animation.finish());
+ await waitForRender();
+}
+let mouseOverStarted;
+function mouseOver(element) {
+ mouseOverStarted = performance.now();
+ return (new test_driver.Actions())
+ .pointerMove(0, 0, {origin: element})
+ .send();
+}
+function msSinceMouseOver() {
+ return performance.now() - mouseOverStarted;
+}
+async function waitForHoverTime(hoverWaitTimeMs) {
+ await new Promise(resolve => step_timeout(resolve,hoverWaitTimeMs));
+ await waitForRender();
+};
+async function blessTopLayer(visibleElement) {
+ // The normal "bless" function doesn't work well when there are top layer
+ // elements blocking clicks. Additionally, since the normal test_driver.bless
+ // function just adds a button to the main document and clicks it, we can't
+ // call that in the presence of open popovers, since that click will close them.
+ const button = document.createElement('button');
+ button.innerHTML = "Click me to activate";
+ visibleElement.appendChild(button);
+ let wait_click = new Promise(resolve => button.addEventListener("click", resolve, {once: true}));
+ await test_driver.click(button);
+ await wait_click;
+ button.remove();
+}
+// This is a "polyfill" of sorts for the `defaultopen` attribute.
+// It can be called before window.load is complete, and it will
+// show defaultopen popovers according to the rules previously part
+// of the popover API: any popover=manual popover can be shown this
+// way, and only the first popover=auto popover.
+function showDefaultopenPopoversOnLoad() {
+ function show() {
+ const popovers = Array.from(document.querySelectorAll('[popover][defaultopen]'));
+ popovers.forEach((p) => {
+ // The showPopover calls below aren't guarded by a check on the popover
+ // open/closed status. If they throw exceptions, this function was
+ // probably called at a bad time. However, a check is made for open
+ // <dialog open> elements.
+ if (p instanceof HTMLDialogElement && p.hasAttribute('open'))
+ return;
+ switch (p.popover) {
+ case 'auto':
+ if (!document.querySelector('[popover]:open'))
+ p.showPopover();
+ return;
+ case 'manual':
+ p.showPopover();
+ return;
+ default:
+ assert_unreached(`Unknown popover type ${p.popover}`);
+ }
+ });
+ }
+ if (document.readyState === 'complete') {
+ show();
+ } else {
+ window.addEventListener('load',show,{once:true});
+ }
+}
diff --git a/testing/web-platform/tests/html/semantics/popovers/toggleevent-interface.tentative.html b/testing/web-platform/tests/html/semantics/popovers/toggleevent-interface.tentative.html
new file mode 100644
index 0000000000..8ee63c4071
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/toggleevent-interface.tentative.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popup.research.explainer">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+test(function() {
+ var event = new BeforeToggleEvent("");
+ assert_true(event instanceof window.BeforeToggleEvent);
+}, "the event is an instance of BeforeToggleEvent");
+
+test(function() {
+ var event = new BeforeToggleEvent("");
+ assert_true(event instanceof window.Event);
+}, "the event inherts from Event");
+
+test(function() {
+ assert_throws_js(TypeError, function() {
+ new BeforeToggleEvent();
+ }, 'First argument (type) is required, so was expecting a TypeError.');
+}, 'Missing type argument');
+
+test(function() {
+ var event = new BeforeToggleEvent("test");
+ assert_equals(event.type, "test");
+}, "type argument is string");
+
+test(function() {
+ var event = new BeforeToggleEvent(null);
+ assert_equals(event.type, "null");
+}, "type argument is null");
+
+test(function() {
+ var event = new BeforeToggleEvent(undefined);
+ assert_equals(event.type, "undefined");
+}, "event type set to undefined");
+
+test(function() {
+ var event = new BeforeToggleEvent("test");
+ assert_equals(event.currentState, "");
+}, "currentState has default value of empty string");
+
+test(function() {
+ var event = new BeforeToggleEvent("test");
+ assert_readonly(event, "currentState", "readonly attribute value");
+}, "currentState is readonly");
+
+test(function() {
+ var event = new BeforeToggleEvent("test");
+ assert_equals(event.newState, "");
+}, "newState has default value of empty string");
+
+test(function() {
+ var event = new BeforeToggleEvent("test");
+ assert_readonly(event, "newState", "readonly attribute value");
+}, "newState is readonly");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", null);
+ assert_equals(event.currentState, "");
+ assert_equals(event.newState, "");
+}, "BeforeToggleEventInit argument is null");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", undefined);
+ assert_equals(event.currentState, "");
+ assert_equals(event.newState, "");
+}, "BeforeToggleEventInit argument is undefined");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {});
+ assert_equals(event.currentState, "");
+ assert_equals(event.newState, "");
+}, "BeforeToggleEventInit argument is empty dictionary");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {currentState: "sample"});
+ assert_equals(event.currentState, "sample");
+}, "currentState set to 'sample'");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {currentState: undefined});
+ assert_equals(event.currentState, "");
+}, "currentState set to undefined");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {currentState: null});
+ assert_equals(event.currentState, "null");
+}, "currentState set to null");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {currentState: false});
+ assert_equals(event.currentState, "false");
+}, "currentState set to false");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {currentState: true});
+ assert_equals(event.currentState, "true");
+}, "currentState set to true");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {currentState: 0.5});
+ assert_equals(event.currentState, "0.5");
+}, "currentState set to a number");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {currentState: []});
+ assert_equals(event.currentState, "");
+}, "currentState set to []");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {currentState: [1, 2, 3]});
+ assert_equals(event.currentState, "1,2,3");
+}, "currentState set to [1, 2, 3]");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {currentState: {sample: 0.5}});
+ assert_equals(event.currentState, "[object Object]");
+}, "currentState set to an object");
+
+test(function() {
+ var event = new BeforeToggleEvent("test",
+ {currentState: {valueOf: function () { return 'sample'; }}});
+ assert_equals(event.currentState, "[object Object]");
+}, "currentState set to an object with a valueOf function");
+
+test(function() {
+ var eventInit = {currentState: "sample",newState: "sample2"};
+ var event = new BeforeToggleEvent("test", eventInit);
+ assert_equals(event.currentState, "sample");
+ assert_equals(event.newState, "sample2");
+}, "BeforeToggleEventInit properties set value");
+
+test(function() {
+ var eventInit = {currentState: "open",newState: "closed"};
+ var event = new BeforeToggleEvent("beforetoggle", eventInit);
+ assert_equals(event.currentState, "open");
+ assert_equals(event.newState, "closed");
+}, "BeforeToggleEventInit properties set value 2");
+
+test(function() {
+ var eventInit = {currentState: "closed",newState: "open"};
+ var event = new BeforeToggleEvent("beforetoggle", eventInit);
+ assert_equals(event.currentState, "closed");
+ assert_equals(event.newState, "open");
+}, "BeforeToggleEventInit properties set value 3");
+
+test(function() {
+ var eventInit = {currentState: "open",newState: "open"};
+ var event = new BeforeToggleEvent("beforetoggle", eventInit);
+ assert_equals(event.currentState, "open");
+ assert_equals(event.newState, "open");
+}, "BeforeToggleEventInit properties set value 4");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {newState: "sample"});
+ assert_equals(event.newState, "sample");
+}, "newState set to 'sample'");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {newState: undefined});
+ assert_equals(event.newState, "");
+}, "newState set to undefined");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {newState: null});
+ assert_equals(event.newState, "null");
+}, "newState set to null");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {newState: false});
+ assert_equals(event.newState, "false");
+}, "newState set to false");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {newState: true});
+ assert_equals(event.newState, "true");
+}, "newState set to true");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {newState: 0.5});
+ assert_equals(event.newState, "0.5");
+}, "newState set to a number");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {newState: []});
+ assert_equals(event.newState, "");
+}, "newState set to []");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {newState: [1, 2, 3]});
+ assert_equals(event.newState, "1,2,3");
+}, "newState set to [1, 2, 3]");
+
+test(function() {
+ var event = new BeforeToggleEvent("test", {newState: {sample: 0.5}});
+ assert_equals(event.newState, "[object Object]");
+}, "newState set to an object");
+
+test(function() {
+ var event = new BeforeToggleEvent("test",
+ {newState: {valueOf: function () { return 'sample'; }}});
+ assert_equals(event.newState, "[object Object]");
+}, "newState set to an object with a valueOf function");
+</script>