summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/semantics/popovers
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/html/semantics/popovers
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/html/semantics/popovers')
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/hide-other-popover-side-effects.html21
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/invoker-show-crash.html12
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/label-in-invoker.html23
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/light-dismiss-event-ordering.html82
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display-ref.html24
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html51
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html34
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-ref.html29
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html109
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-idl-property.tentative.html52
-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.html57
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display.tentative.html56
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-nesting.tentative.html56
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display-ref.html32
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html86
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-anchor-transition.tentative.tentative.html34
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-and-svg-ref.html11
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-and-svg.html18
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-appearance-ref.html18
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-appearance.html26
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-attribute-all-elements.html47
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-attribute-basic.html359
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance-ref.html45
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance.html47
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-beforetoggle-opening-event.html33
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-change-type.html42
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-close-request.html40
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-css-properties.tentative.html54
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance-ref.html33
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance.html27
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-dialog-crash.html26
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-document-open.html30
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-events.html216
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-focus-2.html174
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-focus-harness.html25
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-focus.html292
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hidden-display-ref.html19
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hidden-display.html39
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hint-crash.tentative.html29
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-hide.tentative.html21
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-hover.tentative.html21
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-show.tentative.html21
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-toggle.tentative.html21
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none-ref.html5
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none.html19
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-invoker-reset.html34
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute-hint.tentative.html19
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute.html74
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-flat-tree-nested.html55
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-flat-tree.html46
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-hint.tentative.html107
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-on-scroll.html66
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-with-anchor.tentative.tentative.html90
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.html607
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-manual-crash.html32
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-move-documents.html89
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-not-keyboard-focusable.html49
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-display-ref.html20
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-display.html27
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-in-beforetoggle.html66
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-2.html69
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-ref.html22
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display.tentative.html37
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-overlay.html51
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-removal-2.html29
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-removal.html28
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-shadow-dom.html202
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-shadowhost-focus.html56
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-stacking-anchor-attribute.tentative.html104
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-stacking-context-ref.html29
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-stacking-context.html35
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-stacking.html131
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-target-action-hover.tentative.html180
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-target-element-disabled.html159
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-top-layer-combinations.html155
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-top-layer-interactions.html89
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting-anchor.tentative.html45
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting-hints.tentative.html46
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting.tentative.html45
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-types-with-hints.tentative.html179
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-types.html37
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popover-undefined-remove-crash.html12
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/popovertarget-reflection.html53
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/resources/popover-hover-hide-common.js139
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/resources/popover-invoking-attribute.js122
-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-top-layer-nesting.js108
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/resources/popover-utils.js176
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/togglePopover.html79
-rw-r--r--testing/web-platform/tests/html/semantics/popovers/toggleevent-interface.html208
92 files changed, 6604 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/semantics/popovers/WEB_FEATURES.yml b/testing/web-platform/tests/html/semantics/popovers/WEB_FEATURES.yml
new file mode 100644
index 0000000000..e1b9f82de3
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: popover
+ files: "**"
diff --git a/testing/web-platform/tests/html/semantics/popovers/hide-other-popover-side-effects.html b/testing/web-platform/tests/html/semantics/popovers/hide-other-popover-side-effects.html
new file mode 100644
index 0000000000..7cc95a95e9
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/hide-other-popover-side-effects.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://chromium-review.googlesource.com/c/chromium/src/+/4094463/8/third_party/blink/renderer/core/html/html_element.cc#1404">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id=popover1 popover=auto>popover1</div>
+<div id=popover2 popover=auto>popover2</div>
+
+<script>
+test(() => {
+ const popover2 = document.getElementById('popover2');
+ popover1.showPopover();
+ popover1.addEventListener('beforetoggle', () => {
+ popover2.remove();
+ });
+ assert_throws_dom('InvalidStateError', () => popover2.showPopover(),
+ "popover1's beforetoggle event handler removes popover2 so showPopover should throw.");
+ assert_false(popover2.matches(':popover-open'), 'popover2 should not match :popover-open once it is closed.');
+}, 'Removing a popover while it is opening and force closing another popover should throw an exception.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/invoker-show-crash.html b/testing/web-platform/tests/html/semantics/popovers/invoker-show-crash.html
new file mode 100644
index 0000000000..7da57f9788
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/invoker-show-crash.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://bugs.chromium.org/p/chromium/issues/detail?id=1463384">
+<link rel=help href="https://github.com/whatwg/html/issues/9383">
+
+<button id=button popovertarget=popover>button</button>
+<div id=popover popover=auto>popover</div>
+
+<script>
+button.click();
+popover.showPopover();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/label-in-invoker.html b/testing/web-platform/tests/html/semantics/popovers/label-in-invoker.html
new file mode 100644
index 0000000000..bf8ab9710d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/label-in-invoker.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://bugs.chromium.org/p/chromium/issues/detail?id=1523168">
+<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>
+
+<button popovertarget=mypopover>
+ <label>label</label>
+</button>
+<div id=mypopover popover=auto>popover</div>
+
+<script>
+promise_test(async() => {
+ const label = document.querySelector('label');
+ assert_false(mypopover.matches(':popover-open'),
+ 'Popover should be closed at the start of the test.');
+ await test_driver.click(label);
+ assert_true(mypopover.matches(':popover-open'),
+ 'The popover should be opened by clicking on the label.');
+}, 'Buttons with popovertarget should invoke targets even if there is a label in the button.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/light-dismiss-event-ordering.html b/testing/web-platform/tests/html/semantics/popovers/light-dismiss-event-ordering.html
new file mode 100644
index 0000000000..be39050ac6
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/light-dismiss-event-ordering.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(':popover-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(':popover-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(':popover-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.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display-ref.html
new file mode 100644
index 0000000000..9530e7d3c4
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display-ref.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..435929a6c1
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-change-display.tentative.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:xiaochengh@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=match href="popover-anchor-change-display-ref.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..55a11fafdb
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-none.tentative.html
@@ -0,0 +1,34 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-ref.html
new file mode 100644
index 0000000000..f701810da2
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display-ref.html
@@ -0,0 +1,29 @@
+<!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=ex4><div class=anchor></div><div class=popover></div></div>
+<div class=ex id=ex5><div class=anchor></div><div class=popover></div></div>
+<div class=ex id=ex6><div class=anchor></div><div class=popover></div></div>
+
+<style>
+ .ex {
+ margin: 15px;
+ font-size: 0;
+ }
+ .ex div {
+ display:inline-block;
+ width: 50px;
+ height: 50px;
+ }
+ .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..d50dd6c857
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-display.tentative.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=match href="popover-anchor-display-ref.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>
+
+<!-- Example using an implicit anchor which is not the default anchor -->
+<div class=ex>
+ <div class=anchor id=anchor5></div>
+ <div id=popover5 popover=manual anchor=anchor5 defaultopen></div>
+</div>
+
+<!-- Example using a default anchor which is not the implicit anchor -->
+<div class=ex>
+ <div class=anchor id=anchor6></div>
+ <div id=popover6 popover=manual anchor=anchor1 defaultopen></div>
+</div>
+
+<script>
+showDefaultopenPopoversOnLoad();
+</script>
+
+<style>
+ .ex {
+ margin: 15px;
+ }
+ .ex div {
+ width: 50px;
+ height: 50px;
+ }
+ .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: 50px;
+ color: orange;
+ }
+ #popover4 {
+ left: anchor(right);
+ top: anchor(top);
+ }
+ #popover5 {
+ anchor-default: --anchor1; /* shouldn't be used */
+ left: anchor(implicit right);
+ top: anchor(implicit top);
+ }
+ #anchor6 {
+ anchor-name: --anchor6;
+ }
+ #popover6 {
+ anchor-default: --anchor6;
+ left: anchor(right); /* shouldn't use the implicit anchor */
+ 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..1e255339f8
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-idl-property.tentative.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div>
+ <button id=b1>This is an anchor button</button>
+ <div popover id=p1 anchor=b1>This is a popover</div>
+ <button id=b2 popovertarget=p1>This button invokes the popover but isn't an anchor</button>
+</div>
+
+<script>
+ test(function() {
+ assert_equals(p1.anchorElement,b1);
+ }, "popover anchorElement IDL property returns the anchor element");
+
+ test(function() {
+ assert_equals(p1.anchorElement,b1);
+ p1.anchorElement = b2;
+ assert_equals(p1.anchorElement,b2);
+ assert_equals(p1.getAttribute('anchor'),'','Idref is empty after setting element');
+ p1.anchorElement = b1; // Reset
+ }, "popover anchorElement is settable");
+</script>
+
+<div>
+ <button id=b3>button</button>
+ <div id=p2>Anchored div</div>
+</div>
+<style>
+ * {margin:0;padding:0;}
+ #b3 {width: 200px;}
+ #p2 {
+ position: absolute;
+ left: anchor(right);
+ }
+</style>
+
+<script>
+ test(function() {
+ assert_equals(p2.anchorElement,null);
+ const button = document.getElementById('b3');
+ assert_true(!!button);
+ p2.anchorElement = button;
+ assert_equals(p2.getAttribute('anchor'),'','Idref should be empty after setting element');
+ assert_equals(p2.anchorElement,button,'Element reference should be button');
+ assert_equals(p2.offsetLeft, 200, 'The anchor relationship should be functional');
+ }, "anchorElement affects anchor positioning");
+</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..9942b41e36
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display-ref.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:xiaochengh@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+
+<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..b60ff49e09
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nested-display.tentative.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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=match href="popover-anchor-nested-display-ref.html">
+
+<button id=main-menu-button popovertarget=main-menu>Show menu</button>
+
+<div id=main-menu popover anchor=main-menu-button>
+ <div>Foo</div>
+ <button id=nested-menu-button popovertarget=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..c3ea4f2165
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-nesting.tentative.html
@@ -0,0 +1,56 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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(':popover-open'));
+ assert_true(popover2.matches(':popover-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(':popover-open'));
+ assert_true(popover1.matches(':popover-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.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display-ref.html
new file mode 100644
index 0000000000..926a171e9a
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display-ref.html
@@ -0,0 +1,32 @@
+<!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=ex id=ex2><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..7ed6cf1adf
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-scroll-display.tentative.html
@@ -0,0 +1,86 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=match href="popover-anchor-scroll-display-ref.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>
+
+<!-- Example using a default anchor that is not the implicit anchor -->
+<div class=ex>
+ <div class=anchor id=anchor2></div>
+ <div id=popover2 popover=manual anchor=fake-anchor></div>
+</div>
+
+<!-- A position:fixed fake anchor. Any popover anchored to it won't move when
+ the document is scrolled. -->
+<div id=fake-anchor></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);
+ }
+ #fake-anchor {
+ position: fixed;
+ anchor-name: --fake-anchor;
+ }
+ #anchor2 {
+ anchor-name: --anchor2;
+ }
+ #popover2 {
+ anchor-default: --anchor2;
+ left: anchor(right);
+ top: anchor(top);
+ }
+</style>
+
+<script>
+function raf() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+async function runTest() {
+ document.querySelectorAll('[popover]').forEach(
+ popover => popover.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-anchor-transition.tentative.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-transition.tentative.tentative.html
new file mode 100644
index 0000000000..ae2a3a8e41
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-anchor-transition.tentative.tentative.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests transitioning display property of anchored popover</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel="help" href="https://github.com/whatwg/html/pull/9144">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+body {
+ margin: 0;
+}
+
+#target {
+ transition: display 2s;
+}
+</style>
+
+<div popover anchor id="target">
+ Popover
+</div>
+
+<script>
+test(() => {
+ target.showPopover();
+ const xBefore = target.offsetLeft;
+ const yBefore = target.offsetTop;
+
+ target.hidePopover();
+ assert_equals(target.offsetLeft, xBefore, 'Should not shift in x axis');
+ assert_equals(target.offsetTop, yBefore, 'Should not shift in y axis')
+}, 'Transitioning display property of an anchored popover should not cause a position shift');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-and-svg-ref.html b/testing/web-platform/tests/html/semantics/popovers/popover-and-svg-ref.html
new file mode 100644
index 0000000000..db52e77d2b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-and-svg-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover is only effective on HTMLElement, not on svg element</title>
+<style>
+svg {
+ width: 100px;
+ height: 100px;
+ background-color:green;
+}
+</style>
+<svg ></svg>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-and-svg.html b/testing/web-platform/tests/html/semantics/popovers/popover-and-svg.html
new file mode 100644
index 0000000000..c5e8bb42a8
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-and-svg.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover is only effective on HTMLElement, not on svg element</title>
+<link rel="author" href="mailto:cathiechen@igalia.com">
+<link rel=help href="https://html.spec.whatwg.org/#the-popover-attribute">
+<link rel="match" href="popover-and-svg-ref.html">
+<style>
+svg {
+ width: 100px;
+ height: 100px;
+ background-color:green;
+}
+[popover] {
+ top: 100px;
+ bottom: auto;
+}
+</style>
+<svg popover></svg>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-appearance-ref.html b/testing/web-platform/tests/html/semantics/popovers/popover-appearance-ref.html
new file mode 100644
index 0000000000..7ceca94559
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-appearance-ref.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.html b/testing/web-platform/tests/html/semantics/popovers/popover-appearance.html
new file mode 100644
index 0000000000..e9050bdeb9
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-appearance.html
@@ -0,0 +1,26 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel="match" href="popover-appearance-ref.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-all-elements.html b/testing/web-platform/tests/html/semantics/popovers/popover-attribute-all-elements.html
new file mode 100644
index 0000000000..5a536f026e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-attribute-all-elements.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<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>
+<script src="../../resources/common.js"></script>
+
+<body>
+<script>
+setup({ explicit_done: true });
+window.onload = () => {
+ // Loop through all HTML elements that render a box by default:
+ let elementsThatDontRender = ['area', 'audio','base','br','datalist','dialog','embed','head','link','meta','noscript','optgroup','option','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, true);
+ }, `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.`);
+ });
+ elementsThatDontRender.forEach(tag => {
+ test((t) => {
+ const element = document.createElement(tag);
+ element.setAttribute('popover','auto');
+ document.body.appendChild(element);
+ t.add_cleanup(() => element.remove());
+ assertIsFunctionalPopover(element, false);
+ }, `A <${tag} popover> element should not be rendered.`);
+ });
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-attribute-basic.html b/testing/web-platform/tests/html/semantics/popovers/popover-attribute-basic.html
new file mode 100644
index 0000000000..2af3bbc137
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-attribute-basic.html
@@ -0,0 +1,359 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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 id=popovers>
+ <div popover id=boolean>Popover</div>
+ <div popover="">Popover</div>
+ <div popover=auto>Popover</div>
+ <div popover=hint>Popover</div>
+ <div popover=manual>Popover</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 id=outside></div>
+<style>
+[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');
+
+ // Start with the provided examples:
+ Array.from(document.getElementById('popovers').children).forEach(popover => {
+ test((t) => {
+ assertIsFunctionalPopover(popover, true);
+ }, `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.`);
+ });
+
+ 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');
+ if (popoverHintSupported()) {
+ popover.popover='hint';
+ assert_equals(popover.getAttribute('popover'),'hint');
+ }
+ 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, true);
+ popover.removeAttribute('popover');
+ assertNotAPopover(popover);
+ popover.setAttribute('popover','AuTo');
+ assertIsFunctionalPopover(popover, true);
+ popover.removeAttribute('popover');
+ popover.setAttribute('PoPoVeR','AuTo');
+ assertIsFunctionalPopover(popover, true);
+ // Via IDL also
+ popover.popover = 'auto';
+ assertIsFunctionalPopover(popover, true);
+ popover.popover = 'aUtO';
+ assertIsFunctionalPopover(popover, true);
+ popover.popover = 'invalid'; // treated as "manual"
+ assertIsFunctionalPopover(popover, true);
+ },'Popover attribute value should be case insensitive');
+
+ test((t) => {
+ const popover = createPopover(t);
+ assertIsFunctionalPopover(popover, true);
+ popover.setAttribute('popover','manual'); // Change popover type
+ assertIsFunctionalPopover(popover, true);
+ popover.setAttribute('popover','invalid'); // Change popover type to something invalid
+ assertIsFunctionalPopover(popover, true);
+ popover.popover = 'manual'; // Change popover type via IDL
+ assertIsFunctionalPopover(popover, true);
+ popover.popover = 'invalid'; // Make invalid via IDL (treated as "manual")
+ assertIsFunctionalPopover(popover, true);
+ },'Changing attribute values for popover should work');
+
+ test((t) => {
+ const popover = createPopover(t);
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ if (popoverHintSupported()) {
+ popover.setAttribute('popover','hint'); // Change popover type
+ assert_false(popover.matches(':popover-open'));
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ }
+ popover.setAttribute('popover','manual');
+ assert_false(popover.matches(':popover-open'));
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ popover.setAttribute('popover','invalid');
+ assert_true(popover.matches(':popover-open'),'From "manual" to "invalid" (which is interpreted as "manual") should not close the popover');
+ popover.setAttribute('popover','auto');
+ assert_false(popover.matches(':popover-open'),'From "invalid" ("manual") to "auto" should hide the popover');
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ popover.setAttribute('popover','invalid');
+ assert_false(popover.matches(':popover-open'),'From "auto" to "invalid" (which is interpreted as "manual") should close the popover');
+ },'Changing attribute values should close open popovers');
+
+ const validTypes = popoverHintSupported() ? ["auto","hint","manual"] : ["auto","manual"];
+ validTypes.forEach(type => {
+ test((t) => {
+ const popover = createPopover(t);
+ popover.setAttribute('popover',type);
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ popover.remove();
+ assert_false(popover.matches(':popover-open'));
+ document.body.appendChild(popover);
+ assert_false(popover.matches(':popover-open'));
+ },`Removing a visible popover=${type} element from the document should close the popover`);
+
+ test((t) => {
+ const popover = createPopover(t);
+ popover.setAttribute('popover',type);
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ assert_false(popover.matches(':modal'));
+ popover.hidePopover();
+ },`A showing popover=${type} does not match :modal`);
+
+ test((t) => {
+ const popover = createPopover(t);
+ popover.setAttribute('popover',type);
+ assert_false(popover.matches(':popover-open'));
+ // FIXME: Once :open/:closed are defined in HTML we should remove these two constants.
+ const openPseudoClassIsSupported = CSS.supports('selector(:open))');
+ const closePseudoClassIsSupported = CSS.supports('selector(:closed))');
+ assert_false(openPseudoClassIsSupported && popover.matches(':open'),'popovers never match :open');
+ assert_false(closePseudoClassIsSupported && popover.matches(':closed'),'popovers never match :closed');
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ assert_false(openPseudoClassIsSupported && popover.matches(':open'),'popovers never match :open');
+ assert_false(closePseudoClassIsSupported && popover.matches(':closed'),'popovers never match :closed');
+ popover.hidePopover();
+ },`A popover=${type} never matches :open or :closed`);
+ });
+
+ 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(':popover-open'));
+ assert_false(popover.matches(':popover-open'));
+ assert_throws_dom('InvalidStateError', () => popover.showPopover());
+ assert_false(other_popover.matches(':popover-open'),'unrelated popover is hidden');
+ assert_false(popover.matches(':popover-open'),'popover is not shown if its type changed during show');
+ },`Changing the popover type in a "beforetoggle" event handler should throw an exception (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(':popover-open'));
+ assert_true(other_popover.matches(':popover-open'));
+ popover.hidePopover(); // Calling hidePopover on a hidden popover should not throw.
+ assert_false(other_popover.matches(':popover-open'),'unrelated popover is hidden');
+ assert_false(popover.matches(':popover-open'),'popover is still hidden if its type changed during hide event');
+ other_popover.hidePopover(); // Calling hidePopover on a hidden popover should not throw.
+ },`Changing the popover type in a "beforetoggle" event handler during hidePopover() should not throw an exception`);
+
+ test(t => {
+ const popover = document.createElement('div');
+ assert_throws_dom('NotSupportedError', () => popover.hidePopover(),
+ 'Calling hidePopover on an element without a popover attribute should throw.');
+ popover.setAttribute('popover', 'auto');
+ popover.hidePopover(); // Calling hidePopover on a disconnected popover should not throw.
+ assert_throws_dom('InvalidStateError', () => popover.showPopover(),
+ 'Calling showPopover on a disconnected popover should throw.');
+ },'Calling hidePopover on a disconnected popover should not throw.');
+
+ 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(':popover-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(':popover-open'),'popover should remain open when not changing the type');
+ assert_false(gotEvent);
+ try {
+ popover.hidePopover(); // Cleanup
+ } catch (e) {}
+ } 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(':popover-open'));
+ assert_true(gotEvent);
+ 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(':popover-open'),'Popover should function');
+ await clickOn(outsideElement); // Try to light dismiss
+ switch (interpretedType(inEventType,method)) {
+ case 'manual':
+ assert_true(popover.matches(':popover-open'),'A popover=manual should not light-dismiss');
+ popover.hidePopover();
+ break;
+ case 'auto':
+ case 'hint':
+ assert_false(popover.matches(':popover-open'),'A popover=auto should light-dismiss');
+ break;
+ }
+ }
+ }
+ },`Changing a popover from ${type} to ${newType} (via ${method}), and then ${inEventType} during 'beforetoggle' works`);
+ });
+ });
+ });
+ });
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance-ref.html b/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance-ref.html
new file mode 100644
index 0000000000..bf2b16c3f5
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance-ref.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.html b/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance.html
new file mode 100644
index 0000000000..cf57aee69e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-backdrop-appearance.html
@@ -0,0 +1,47 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel="match" href="popover-backdrop-appearance-ref.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=hint 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.html b/testing/web-platform/tests/html/semantics/popovers/popover-beforetoggle-opening-event.html
new file mode 100644
index 0000000000..41bb9aa82c
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-beforetoggle-opening-event.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover beforetoggle event</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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) => {
+ assert_false(e.bubbles, 'beforetoggle event does not bubble');
+ 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-change-type.html b/testing/web-platform/tests/html/semantics/popovers/popover-change-type.html
new file mode 100644
index 0000000000..978d1d1495
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-change-type.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/issues/9034">
+<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="/html/semantics/popovers/resources/popover-utils.js"></script>
+
+<div id=mypopover>popover</div>
+
+<script>
+promise_test(async () => {
+ const mypopover = document.getElementById('mypopover');
+
+ mypopover.popover = "manual";
+ mypopover.showPopover();
+
+ await new Promise(resolve => {
+ mypopover.addEventListener("beforetoggle", (e) => {
+ if (e.newState === "closed") {
+ mypopover.remove();
+ requestAnimationFrame(() => {
+ document.body.append(mypopover);
+ mypopover.showPopover();
+ resolve();
+ });
+ }
+ }, {once: true});
+
+ mypopover.popover = "auto";
+ });
+
+ assert_true(mypopover.matches(':popover-open'),
+ 'The popover should be open after the toggling sequence.');
+
+ await sendEscape();
+ assert_false(mypopover.matches(':popover-open'),
+ 'The popover should light dismiss because it is in the auto state.');
+}, 'Changing the popover attribute should always update the auto/manual behavior.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-close-request.html b/testing/web-platform/tests/html/semantics/popovers/popover-close-request.html
new file mode 100644
index 0000000000..830a40e060
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-close-request.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popover close request behavior</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/top-layer.js"></script>
+<script src="/close-watcher/resources/helpers.js"></script>
+
+<div popover id=p1>
+ Inside popover 1
+ <div popover id=p2>Inside popover 2</div>
+</div>
+
+<script>
+promise_test(async () => {
+ const popover1 = document.querySelector('#p1');
+ const popover2 = document.querySelector('#p2');
+
+ popover1.showPopover();
+
+ // Bless the opening of popover2, so it doesn't get grouped with popover1 by
+ // the close watcher infrastructure.
+ await blessTopLayer(popover1);
+ popover2.showPopover();
+
+ assert_true(popover1.matches(':popover-open'), "Starting: popover1 must be open");
+ assert_true(popover2.matches(':popover-open'), "Starting: popover2 must be open");
+
+ await sendCloseRequest();
+ assert_true(popover1.matches(':popover-open'), "After one close request, popover1 must be open");
+ assert_false(popover2.matches(':popover-open'), "After one close request, popover2 must be closed");
+
+ await sendCloseRequest();
+ assert_false(popover1.matches(':popover-open'), "After two close requests, popover1 must be closed");
+ assert_false(popover2.matches(':popover-open'), "After two close requests, popover2 must be closed");
+});
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-css-properties.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-css-properties.tentative.html
new file mode 100644
index 0000000000..93d388b02b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-css-properties.tentative.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popover API CSS parsing with computed values</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="/css/support/parsing-testcommon.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<div id=target></div>
+<div id=scratch></div>
+
+<script>
+function testprop(prop) {
+ // Computed values:
+ test_computed_value(prop, '0s');
+ test_computed_value(prop, '0ms', '0s');
+ test_computed_value(prop, '32s');
+ test_computed_value(prop, '123ms', '0.123s');
+
+ // Valid values:
+ test_valid_value(prop, '0s');
+ test_valid_value(prop, '0ms');
+ test_valid_value(prop, '32s');
+ test_valid_value(prop, '123ms');
+ test_valid_value(prop, 'inherit');
+
+ // Invalid values:
+ test_invalid_value(prop, '0');
+ test_invalid_value(prop, 'foo');
+ test_invalid_value(prop, '-1s');
+ test_invalid_value(prop, 'none');
+ test_invalid_value(prop, 'auto');
+
+ // Animations:
+ test_interpolation({
+ property: prop,
+ from: '1s',
+ to: '2000ms',
+ }, [
+ {at: -1.5, expect: '0s'}, // Clamping at 0
+ {at: -0.3, expect: '0.7s'},
+ {at: 0, expect: '1s'},
+ {at: 0.5, expect: '1.5s'},
+ {at: 1, expect: '2s'},
+ {at: 1.5, expect: '2.5s'},
+ ]);
+}
+
+testprop('popover-show-delay');
+testprop('popover-hide-delay');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance-ref.html b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance-ref.html
new file mode 100644
index 0000000000..12efbb6b1e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance-ref.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.html b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance.html
new file mode 100644
index 0000000000..8b4edadee9
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-appearance.html
@@ -0,0 +1,27 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel="match" href="popover-dialog-appearance-ref.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.html b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-crash.html
new file mode 100644
index 0000000000..e7579d5a38
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-dialog-crash.html
@@ -0,0 +1,26 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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.html b/testing/web-platform/tests/html/semantics/popovers/popover-document-open.html
new file mode 100644
index 0000000000..80ac86aced
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-document-open.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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(':popover-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(':popover-open'),'popover1 should have been hidden when it was removed from the document');
+ assert_false(popover1.matches(':popover-open'),'popover2 shouldn\'t be showing yet');
+ popover2.showPopover();
+ assert_true(popover2.matches(':popover-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.html b/testing/web-platform/tests/html/semantics/popovers/popover-events.html
new file mode 100644
index 0000000000..4d58001f7d
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-events.html
@@ -0,0 +1,216 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<div popover>Popover</div>
+
+<script>
+function getPopoverAndSignal(t) {
+ const popover = document.querySelector('[popover]');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ t.add_cleanup(() => controller.abort());
+ return {popover, signal};
+}
+window.onload = () => {
+ for(const method of ["listener","attribute"]) {
+ promise_test(async t => {
+ const {popover,signal} = getPopoverAndSignal(t);
+ assert_false(popover.matches(':popover-open'));
+ let showCount = 0;
+ let afterShowCount = 0;
+ let hideCount = 0;
+ let afterHideCount = 0;
+ function listener(e) {
+ assert_false(e.bubbles,'toggle events should not bubble');
+ if (e.type === "beforetoggle") {
+ if (e.newState === "open") {
+ ++showCount;
+ assert_equals(e.oldState,"closed",'The "beforetoggle" event should be fired before the popover is open');
+ assert_false(e.target.matches(':popover-open'),'The popover should *not* be in the :popover-open state when the opening event fires.');
+ assert_true(e.cancelable,'beforetoggle should be cancelable only for the "show" transition');
+ } else {
+ ++hideCount;
+ assert_equals(e.newState,"closed",'Popover toggleevent states should be "open" and "closed"');
+ assert_equals(e.oldState,"open",'The "beforetoggle" event should be fired before the popover is closed')
+ assert_true(e.target.matches(':popover-open'),'The popover should be in the :popover-open state when the hiding event fires.');
+ assert_false(e.cancelable,'beforetoggle should be cancelable only for the "show" transition');
+ e.preventDefault(); // beforetoggle should be cancelable only for the "show" transition
+ }
+ } else {
+ assert_equals(e.type,"toggle",'Popover events should be "beforetoggle" and "toggle"')
+ assert_false(e.cancelable,'toggle should never be cancelable');
+ e.preventDefault(); // toggle should never be cancelable
+ if (e.newState === "open") {
+ ++afterShowCount;
+ if (document.body.contains(e.target)) {
+ assert_true(e.target.matches(':popover-open'),'The popover should be in the :popover-open state when the after opening event fires.');
+ }
+ } else {
+ ++afterHideCount;
+ assert_equals(e.newState,"closed",'Popover toggleevent states should be "open" and "closed"');
+ assert_false(e.target.matches(':popover-open'),'The popover should *not* be in the :popover-open state when the after hiding event fires.');
+ }
+ e.preventDefault(); // "toggle" should not be cancelable.
+ }
+ };
+ switch (method) {
+ case "listener":
+ // These events do *not* bubble.
+ popover.addEventListener('beforetoggle', listener, {signal});
+ popover.addEventListener('toggle', listener, {signal});
+ break;
+ case "attribute":
+ assert_false(popover.hasAttribute('onbeforetoggle'));
+ t.add_cleanup(() => popover.removeAttribute('onbeforetoggle'));
+ popover.onbeforetoggle = listener;
+ assert_false(popover.hasAttribute('ontoggle'));
+ t.add_cleanup(() => popover.removeAttribute('ontoggle'));
+ popover.ontoggle = listener;
+ break;
+ default: assert_unreached();
+ }
+ assert_equals(0,showCount);
+ assert_equals(0,hideCount);
+ assert_equals(0,afterShowCount);
+ assert_equals(0,afterHideCount);
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ assert_equals(1,showCount);
+ assert_equals(0,hideCount);
+ assert_equals(0,afterShowCount);
+ assert_equals(0,afterHideCount);
+ await waitForRender();
+ assert_equals(1,afterShowCount,'toggle show is fired asynchronously');
+ assert_equals(0,afterHideCount);
+ assert_true(popover.matches(':popover-open'));
+ popover.hidePopover();
+ assert_false(popover.matches(':popover-open'));
+ assert_equals(1,showCount);
+ assert_equals(1,hideCount);
+ assert_equals(1,afterShowCount);
+ assert_equals(0,afterHideCount);
+ await waitForRender();
+ assert_equals(1,afterShowCount);
+ assert_equals(1,afterHideCount,'toggle hide is fired asynchronously');
+ // No additional events
+ await waitForRender();
+ await waitForRender();
+ assert_false(popover.matches(':popover-open'));
+ assert_equals(1,showCount);
+ assert_equals(1,hideCount);
+ assert_equals(1,afterShowCount);
+ assert_equals(1,afterHideCount);
+ }, `The "beforetoggle" event (${method}) get properly dispatched for popovers`);
+ }
+
+ promise_test(async t => {
+ const {popover,signal} = getPopoverAndSignal(t);
+ let cancel = true;
+ popover.addEventListener('beforetoggle',(e) => {
+ if (e.newState !== "open")
+ return;
+ if (cancel)
+ e.preventDefault();
+ }, {signal});
+ assert_false(popover.matches(':popover-open'));
+ popover.showPopover();
+ assert_false(popover.matches(':popover-open'),'The "beforetoggle" event should be cancelable for the "opening" transition');
+ cancel = false;
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ popover.hidePopover();
+ assert_false(popover.matches(':popover-open'));
+ }, 'The "beforetoggle" event is cancelable for the "opening" transition');
+
+ promise_test(async t => {
+ const {popover,signal} = getPopoverAndSignal(t);
+ popover.addEventListener('beforetoggle',(e) => {
+ assert_not_equals(e.newState,"closed",'The "beforetoggle" event was fired for the closing transition');
+ }, {signal});
+ assert_false(popover.matches(':popover-open'));
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ t.add_cleanup(() => {document.body.appendChild(popover);});
+ popover.remove();
+ await waitForRender(); // Check for async events also
+ await waitForRender(); // Check for async events also
+ assert_false(popover.matches(':popover-open'));
+ }, 'The "beforetoggle" event is not fired for element removal');
+
+ promise_test(async t => {
+ const {popover,signal} = getPopoverAndSignal(t);
+ let events;
+ function resetEvents() {
+ events = {
+ singleShow: false,
+ singleHide: false,
+ coalescedShow: false,
+ coalescedHide: false,
+ };
+ }
+ function setEvent(type) {
+ assert_equals(events[type],false,'event repeated');
+ events[type] = true;
+ }
+ function assertOnly(type,msg) {
+ Object.keys(events).forEach(val => {
+ assert_equals(events[val],val===type,`${msg} (${val})`);
+ });
+ }
+ popover.addEventListener('toggle',(e) => {
+ switch (e.newState) {
+ case "open":
+ switch (e.oldState) {
+ case "open": setEvent('coalescedShow'); break;
+ case "closed": setEvent('singleShow'); break;
+ default: assert_unreached();
+ }
+ break;
+ case "closed":
+ switch (e.oldState) {
+ case "closed": setEvent('coalescedHide'); break;
+ case "open": setEvent('singleHide'); break;
+ default: assert_unreached();
+ }
+ break;
+ default: assert_unreached();
+ }
+ }, {signal});
+
+ resetEvents();
+ assertOnly('none');
+ assert_false(popover.matches(':popover-open'));
+ popover.showPopover();
+ await waitForRender();
+ assert_true(popover.matches(':popover-open'));
+ assertOnly('singleShow','Single event should have been fired, which is a "show"');
+
+ resetEvents();
+ popover.hidePopover();
+ popover.showPopover(); // Immediate re-show
+ await waitForRender();
+ assert_true(popover.matches(':popover-open'));
+ assertOnly('coalescedShow','Single coalesced event should have been fired, which is a "show"');
+
+ resetEvents();
+ popover.hidePopover();
+ await waitForRender();
+ assertOnly('singleHide','Single event should have been fired, which is a "hide"');
+ assert_false(popover.matches(':popover-open'));
+
+ resetEvents();
+ popover.showPopover();
+ popover.hidePopover(); // Immediate re-hide
+ await waitForRender();
+ assertOnly('coalescedHide','Single coalesced event should have been fired, which is a "hide"');
+ assert_false(popover.matches(':popover-open'));
+ }, 'The "toggle" event is coalesced');
+};
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.html b/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.html
new file mode 100644
index 0000000000..892e5fd68f
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-focus-2.html
@@ -0,0 +1,174 @@
+<!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 tabindex="0">Button1</button>
+ <div popover id=popover0 tabindex="0" style="top:300px">
+ </div>
+ <div popover id=popover1 style="top:100px">
+ <button id=inside_popover1 tabindex="0">Inside1</button>
+ <button id=invoker2 popovertarget=popover2 tabindex="0">Nested Invoker 2</button>
+ <button id=inside_popover2 tabindex="0">Inside2</button>
+ </div>
+ <button id=button2 tabindex="0">Button2</button>
+ <div popover id=popover_no_invoker tabindex="0" style="top:300px"></div>
+ <button popovertarget=popover0 id=invoker0 tabindex="0">Invoker0</button>
+ <button popovertarget=popover1 id=invoker1 tabindex="0">Invoker1</button>
+ <button id=button3 tabindex="0">Button3</button>
+ <div popover id=popover2 style="top:200px">
+ <button id=inside_popover3 tabindex="0">Inside3</button>
+ <button id=invoker3 popovertarget=popover3 tabindex="0">Nested Invoker 3</button>
+ </div>
+ <div popover id=popover3 style="top:300px">
+ Non-focusable popover
+ </div>
+ <button id=button4 tabindex="0">Button4</button>
+</div>
+<style>
+ #fixup [popover] {
+ bottom:auto;
+ }
+</style>
+<script>
+async function verifyFocusOrder(order,description) {
+ order[0].focus();
+ for(let i=0;i<order.length;++i) {
+ const control = order[i];
+ assert_equals(document.activeElement,control,`${description}: Step ${i+1}`);
+ await sendTab();
+ }
+ for(let i=order.length-1;i>=0;--i) {
+ const control = order[i];
+ await sendShiftTab();
+ assert_equals(document.activeElement,control,`${description}: 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');
+ await sendShiftTab();
+ assert_equals(document.activeElement,button1,'Hidden popover should be skipped backwards');
+ popover_no_invoker.showPopover();
+ await sendTab();
+ await sendTab();
+ assert_equals(document.activeElement,popover_no_invoker,"Focusable popover that is opened without an invoker should get focused");
+ await sendTab();
+ assert_equals(document.activeElement,invoker0);
+ await sendEnter(); // Activate the invoker0
+ assert_true(popover0.matches(':popover-open'), 'popover0 should be invoked by invoker0');
+ assert_equals(document.activeElement,invoker0,'Focus should not move when popover is shown');
+ await sendTab();
+ await sendEnter(); // Activate the invoker
+ assert_true(popover1.matches(':popover-open'), 'popover1 should be invoked by invoker1');
+ assert_equals(document.activeElement,invoker1,'Focus should not move when popover is shown');
+ await sendTab();
+ // Make invoker1 non-focusable.
+ invoker1.disabled = true;
+ 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 sendShiftTab();
+ await sendShiftTab();
+ assert_equals(document.activeElement, button1 ,'Focus should not move back to invoker as it is non-focusable');
+ // Reset invoker1 to focusable.
+ invoker1.disabled = false;
+ await verifyFocusOrder([button1, button2, invoker0, invoker1, inside_popover1, invoker2, inside_popover2, button3, button4],'set 1');
+ invoker2.focus();
+ await sendEnter(); // Activate the nested invoker
+ assert_true(popover2.matches(':popover-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(':popover-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 sendShiftTab();
+ assert_equals(document.activeElement,invoker3,'Shift-tab from the higher scope should return to the lower scope');
+ await sendTab();
+ assert_equals(document.activeElement,inside_popover2);
+ 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, invoker0, invoker1, inside_popover1, invoker2, inside_popover3, invoker3, inside_popover2, button3, button4],'set 2');
+}, "Popover focus navigation");
+</script>
+
+<button id=circular0 popovertarget=popover4 tabindex="0">Invoker</button>
+<div id=popover4 popover>
+ <button id=circular1 autofocus popovertarget=popover4 popovertargetaction=hide tabindex="0"></button>
+ <button id=circular2 popovertarget=popover4 popovertargetaction=show tabindex="0"></button>
+ <button id=circular3 popovertarget=popover4 tabindex="0"></button>
+</div>
+<button id=circular4 tabindex="0">after</button>
+<script>
+promise_test(async t => {
+ circular0.focus();
+ await sendEnter(); // Activate the invoker
+ await verifyFocusOrder([circular0, circular1, circular2, circular3, circular4],'circular reference');
+ popover4.hidePopover();
+}, "Circular reference tab navigation");
+</script>
+
+<div id=focus-return1>
+ <button popovertarget=focus-return1-p popovertargetaction=show tabindex="0">Show popover</button>
+ <div popover id=focus-return1-p>
+ <button popovertarget=focus-return1-p popovertargetaction=hide autofocus tabindex="0">Hide popover</button>
+ </div>
+</div>
+<script>
+promise_test(async t => {
+ const invoker = document.querySelector('#focus-return1>button');
+ const popover = document.querySelector('#focus-return1>[popover]');
+ const hideButton = popover.querySelector('[popovertargetaction=hide]');
+ invoker.focus(); // Make sure button is focused.
+ assert_equals(document.activeElement,invoker);
+ await sendEnter(); // Activate the invoker
+ assert_true(popover.matches(':popover-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(':popover-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>
+
+<div id=focus-return2>
+ <button popovertarget=focus-return2-p tabindex="0">Toggle popover</button>
+ <div popover id=focus-return2-p>Popover with <button tabindex="0">focusable element</button></div>
+ <span tabindex=0>Other focusable element</span>
+</div>
+<script>
+promise_test(async t => {
+ const invoker = document.querySelector('#focus-return2>button');
+ const popover = document.querySelector('#focus-return2>[popover]');
+ const otherElement = document.querySelector('#focus-return2>span');
+ invoker.focus(); // Make sure button is focused.
+ assert_equals(document.activeElement,invoker);
+ invoker.click(); // Activate the invoker
+ assert_true(popover.matches(':popover-open'), 'popover should be invoked by invoker');
+ assert_equals(document.activeElement,invoker,'invoker should still be focused');
+ await sendTab();
+ assert_equals(document.activeElement,popover.querySelector('button'),'next up is the popover');
+ await sendTab();
+ assert_equals(document.activeElement,otherElement,'next focus stop is outside the popover');
+ await sendEscape(); // Close the popover via ESC
+ assert_false(popover.matches(':popover-open'), 'popover should be hidden');
+ assert_equals(document.activeElement,otherElement,'focus does not move because it was not inside the popover');
+}, "Popover focus only returns to invoker when focus is within the popover");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-focus-harness.html b/testing/web-platform/tests/html/semantics/popovers/popover-focus-harness.html
new file mode 100644
index 0000000000..a22a68ea2b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-focus-harness.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover utils - harness test</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<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=button1 tabindex="0">Button1</button>
+<button id=button2 tabindex="0">Button2</button>
+<button id=button3 tabindex="0">Button3</button>
+
+<script>
+promise_test(async t => {
+ button1.focus();
+ assert_equals(document.activeElement,button1);
+ await sendTab();
+ assert_equals(document.activeElement,button2,'Tab should move to button 2');
+ await sendShiftTab();
+ assert_equals(document.activeElement,button1,'Shift-Tab should move back to button 1');
+}, "Test sendShiftTab");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-focus.html b/testing/web-platform/tests/html/semantics/popovers/popover-focus.html
new file mode 100644
index 0000000000..230492022c
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-focus.html
@@ -0,0 +1,292 @@
+<!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 tabindex="0">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 tabindex="0">button</button>
+</div>
+
+<div popover data-test='autofocus child'>
+ <p>This is a popover</p>
+ <button autofocus class=should-be-focused tabindex="0">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 tabindex="0">button</button>
+</div>
+
+<div popover data-test='autofocus multiple children'>
+ <p>This is a popover</p>
+ <button autofocus class=should-be-focused tabindex="0">autofocus button</button>
+ <button autofocus tabindex="0">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 tabindex="0">autofocus button</button>
+ <button autofocus tabindex="0">second autofocus button</button>
+</div>
+
+<dialog popover=auto data-test='Opening dialogs as popovers should use dialog initial focus algorithm.'>
+ <button class=should-be-focused>button</button>
+</dialog>
+
+<dialog popover=auto autofocus class=should-be-focused data-test='Opening dialogs as popovers which have autofocus should focus the dialog.'>
+ <button>button</button>
+</dialog>
+
+<style>
+ [popover] {
+ border: 2px solid black;
+ top:150px;
+ left:150px;
+ }
+ :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('tabindex', '0');
+ button.setAttribute('popovertarget', popoverId);
+ return button;
+ }
+ function addPriorFocus(t) {
+ const priorFocus = document.createElement('button');
+ priorFocus.setAttribute("tabindex", "0");
+ priorFocus.id = 'priorFocus';
+ document.body.appendChild(priorFocus);
+ t.add_cleanup(() => priorFocus.remove());
+ return priorFocus;
+ }
+ 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(':popover-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');
+ assert_false(isElementVisible(popover));
+
+ // Manual popover does not restore focus
+ popover.popover = 'manual';
+ priorFocus.focus();
+ assert_equals(document.activeElement, priorFocus);
+ popover.showPopover();
+ assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
+ popover.hidePopover();
+ if (!popover.hasAttribute('data-no-focus')) {
+ assert_not_equals(document.activeElement, priorFocus, 'prior element should *not* get focus when the popover is manual');
+ }
+ assert_false(isElementVisible(popover));
+ popover.popover = 'auto';
+
+ // 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');
+ assert_false(isElementVisible(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');
+ assert_false(isElementVisible(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 = 'manual';
+ assert_false(popover.matches(':popover-open'), 'Changing the popover type should hide the popover');
+ assert_equals(document.activeElement, priorFocus, 'prior element should get focus when the type is changed');
+ assert_false(isElementVisible(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(':popover-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');
+ assert_false(isElementVisible(popover));
+ dialog.close();
+ dialog.remove();
+
+ // Use an activating element:
+ const button = addInvoker(t, popover);
+ priorFocus.focus();
+ button.click();
+ assert_true(popover.matches(':popover-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)');
+ assert_false(isElementVisible(popover));
+
+ // Make sure we can directly focus the (already open) popover:
+ priorFocus.focus();
+ button.click();
+ assert_true(popover.matches(':popover-open'));
+ assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by button.click()`);
+ popover.focus();
+ assert_equals(document.activeElement, popover.hasAttribute('tabindex') || popover.tagName === 'DIALOG' ? popover : expectedFocusedElement, `${testName} directly focus with popover.focus()`);
+ button.click(); // Button is set to toggle the popover
+ assert_false(popover.matches(':popover-open'));
+ assert_equals(document.activeElement, priorFocus, 'prior element should get focus on button-toggled hide');
+ assert_false(isElementVisible(popover));
+ }, "Popover focus test: " + testName);
+
+ promise_test(async t => {
+ const priorFocus = addPriorFocus(t);
+ assert_false(popover.matches(':popover-open'), 'popover should start out hidden');
+ let button = addInvoker(t, popover);
+ assert_equals(button.getAttribute('popovertarget'), popover.id, 'This test assumes the button uses `popovertarget`.');
+ 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(':popover-open'));
+ await clickOn(button); // This will *not* light dismiss, but will "toggle" the popover.
+ assert_false(popover.matches(':popover-open'));
+ assert_equals(document.activeElement, button, 'focus should move to the button when clicked, and should stay there when the popover closes');
+ assert_false(isElementVisible(popover));
+
+ // Same thing, but the button is contained within the popover
+ button.setAttribute('popovertarget', popover.id);
+ button.setAttribute('popovertargetaction', 'hide');
+ popover.appendChild(button);
+ t.add_cleanup(() => button.remove());
+ priorFocus.focus();
+ popover.showPopover();
+ assert_true(popover.matches(':popover-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(':popover-open'), 'clicking button should hide the popover');
+ assert_equals(document.activeElement, priorFocus, 'Contained button should return focus to the previously focused element');
+ assert_false(isElementVisible(popover));
+
+ // Same thing, but the button is unrelated (no popovertarget)
+ button = document.createElement('button');
+ button.setAttribute("tabindex", "0");
+ document.body.appendChild(button);
+ priorFocus.focus();
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ await clickOn(button); // This will light dismiss the popover, focus the prior focus, then focus this button.
+ assert_false(popover.matches(':popover-open'), 'clicking button should hide the popover (via light dismiss)');
+ assert_equals(document.activeElement, button, 'Focus should go to unrelated button on light dismiss');
+ assert_false(isElementVisible(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(':popover-open'), 'popover should start out hidden');
+
+ // Move the prior focus out of the document
+ priorFocus.focus();
+ popover.showPopover();
+ assert_true(popover.matches(':popover-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');
+ assert_false(isElementVisible(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(':popover-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(':popover-open'), 'popover should stay open');
+ popover.hidePopover();
+ assert_false(isElementVisible(popover));
+ 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.html b/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display-ref.html
new file mode 100644
index 0000000000..2dc0d558b6
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display-ref.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.html b/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display.html
new file mode 100644
index 0000000000..db61802db6
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hidden-display.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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=match href="popover-hidden-display-ref.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:popover-open {
+ background: green;
+ }
+ [popover].nottoplayer:popover-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-hint-crash.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-hint-crash.tentative.html
new file mode 100644
index 0000000000..82f83538e9
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hint-crash.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8" />
+<title>Popover=hint crash test</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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>
+
+<div popover id=popover1>Popover 1
+ <div popover id=popover2 style="top:100px">Popover 2</div>
+</div>
+<div popover=manual id=popover3 style="top:200px">Popover 3</div>
+<div popover=hint id=popover4 anchor=popover3 style="inset:0;top:300px">Popover 4 - Click me</div>
+<script>
+popover1.showPopover();
+popover2.showPopover();
+popover3.showPopover();
+popover4.showPopover();
+clickOn(popover4)
+ .then(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-hide.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-hide.tentative.html
new file mode 100644
index 0000000000..57ca5723de
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-hide.tentative.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>The popover-hide-delay CSS property</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>
+
+<body>
+<script src="resources/popover-hover-hide-common.js"></script>
+<script>
+
+// See popover-hover-hide-common.js for documentation.
+runHoverHideTestsForInvokerAction('hide');
+
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-hover.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-hover.tentative.html
new file mode 100644
index 0000000000..d0036c0fe7
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-hover.tentative.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>The popover-hide-delay CSS property</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>
+
+<body>
+<script src="resources/popover-hover-hide-common.js"></script>
+<script>
+
+// See popover-hover-hide-common.js for documentation.
+runHoverHideTestsForInvokerAction('hover');
+
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-show.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-show.tentative.html
new file mode 100644
index 0000000000..7b3fa2b302
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-show.tentative.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>The popover-hide-delay CSS property</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>
+
+<body>
+<script src="resources/popover-hover-hide-common.js"></script>
+<script>
+
+// See popover-hover-hide-common.js for documentation.
+runHoverHideTestsForInvokerAction('show');
+
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-toggle.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-toggle.tentative.html
new file mode 100644
index 0000000000..d6d4079e7e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-hover-hide-toggle.tentative.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>The popover-hide-delay CSS property</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>
+
+<body>
+<script src="resources/popover-hover-hide-common.js"></script>
+<script>
+
+// See popover-hover-hide-common.js for documentation.
+runHoverHideTestsForInvokerAction('toggle');
+
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none-ref.html b/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none-ref.html
new file mode 100644
index 0000000000..3d58e4ca09
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none-ref.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.html b/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none.html
new file mode 100644
index 0000000000..24ce7c6fc6
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-inside-display-none.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=match href="popover-inside-display-none-ref.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(':popover-open'))
+ document.body.appendChild(document.createTextNode('FAIL'));
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-invoker-reset.html b/testing/web-platform/tests/html/semantics/popovers/popover-invoker-reset.html
new file mode 100644
index 0000000000..bfc79fd629
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-invoker-reset.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/popover.research.explainer">
+<link rel=help href="https://github.com/whatwg/html/issues/9152">
+<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=p1 popover>Popover 1
+ <button popovertarget=p2>Button</button>
+</div>
+<div id=p2 popover>Popover 2</div>
+
+<script>
+ test((t) => {
+ p1.showPopover();
+ assert_true(p1.matches(':popover-open'));
+ const invoker = p1.querySelector('button');
+ p2.addEventListener('beforetoggle',(e) => {
+ assert_equals(e.newState,'open');
+ e.preventDefault();
+ },{once:true});
+ invoker.click(); // Will be cancelled
+ assert_false(p2.matches(':popover-open'));
+ assert_true(p1.matches(':popover-open'));
+ p2.showPopover();
+ assert_true(p2.matches(':popover-open'));
+ assert_false(p1.matches(':popover-open'),'invoker was not used to show p2, so p1 should close');
+ },'Invoker gets reset appropriately');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute-hint.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute-hint.tentative.html
new file mode 100644
index 0000000000..b531ddc460
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute-hint.tentative.html
@@ -0,0 +1,19 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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/popover-invoking-attribute.js"></script>
+
+<body>
+<script>
+runPopoverInvokerTests(["hint"]);
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute.html b/testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute.html
new file mode 100644
index 0000000000..8e312e90d7
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-invoking-attribute.html
@@ -0,0 +1,74 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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/popover-invoking-attribute.js"></script>
+
+<body>
+<script>
+runPopoverInvokerTests(["auto","manual"]);
+</script>
+
+<button popovertarget=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(':popover-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 popovertarget 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 popovertarget button closes an open popover (also check event counts)");
+});
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-flat-tree-nested.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-flat-tree-nested.html
new file mode 100644
index 0000000000..ef3b35aea4
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-flat-tree-nested.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that popover light dismiss uses the flat tree when nested shadow roots.</title>
+ <link rel="author" title="Luke Warlow" href="mailto:luke@warlow.dev">
+</head>
+<body>
+ <p>Test passes if the inner popover opens after clicking the inner toggle.</p>
+ <button popovertarget="outerPopover" popovertargetaction="toggle" id="outerPopoverToggle">Toggle</button>
+ <div id="outerPopover" popover>
+ <template shadowrootmode="open">
+ Outer
+ <button id="innerPopoverToggle">Toggle</button>
+ <div id="innerContainer">
+ <template shadowrootmode="open">
+ <div id="innerPopover" popover>
+ Inner
+ </div>
+ </template>
+ </div>
+ </template>
+ </div>
+
+ <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>
+ promise_test(async () => {
+ const innerPopoverToggle = outerPopover.shadowRoot.querySelector("#innerPopoverToggle");
+ const innerContainer = outerPopover.shadowRoot.querySelector('#innerContainer');
+ const innerPopover = innerContainer.shadowRoot.querySelector("#innerPopover");
+ innerPopoverToggle.onclick = () => {
+ innerPopover.togglePopover();
+ }
+
+ assert_false(outerPopover.matches(":popover-open"), "outer popover is initially hidden");
+ assert_false(innerPopover.matches(":popover-open"), "inner popover is initially hidden");
+
+ await clickOn(outerPopoverToggle);
+
+ assert_true(outerPopover.matches(":popover-open"), "outer popover is open after clicking the toggle");
+ assert_false(innerPopover.matches(":popover-open"), "inner popover is initially hidden");
+
+ await clickOn(innerPopoverToggle);
+
+ assert_true(outerPopover.matches(":popover-open"), "outer popover is not dismissed after clicking the second toggle");
+ assert_true(innerPopover.matches(":popover-open"), "inner popover is open after clicking the second toggle");
+ }, "Popover light dismiss uses the flat tree when nested shadow root");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-flat-tree.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-flat-tree.html
new file mode 100644
index 0000000000..65008e28ef
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-flat-tree.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that popover light dismiss uses the flat tree.</title>
+ <link rel="author" title="Tim Nguyen" href="https://github.com/nt1m">
+</head>
+<body>
+ <p>Test passes if the inner popover opens after clicking the inner toggle.</p>
+ <button popovertarget="outerPopover" popovertargetaction="toggle" id="outerPopoverToggle">Toggle</button>
+ <div id="outerPopover" popover>
+ <template shadowrootmode="open">
+ Outer
+ <button popovertarget="innerPopover" popovertargetaction="toggle" id="innerPopoverToggle">Toggle</button>
+ <div id="innerPopover" popover>
+ Inner
+ </div>
+ </template>
+ </div>
+ <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>
+ promise_test(async () => {
+ const innerPopoverToggle = outerPopover.shadowRoot.querySelector("#innerPopoverToggle");
+ const innerPopover = outerPopover.shadowRoot.querySelector("#innerPopover");
+
+ assert_false(outerPopover.matches(":popover-open"), "outer popover is initially hidden");
+ assert_false(innerPopover.matches(":popover-open"), "inner popover is initially hidden");
+
+ await clickOn(outerPopoverToggle);
+
+ assert_true(outerPopover.matches(":popover-open"), "outer popover is open after clicking the toggle");
+ assert_false(innerPopover.matches(":popover-open"), "inner popover is initially hidden");
+
+ await clickOn(innerPopoverToggle);
+
+ assert_true(outerPopover.matches(":popover-open"), "outer popover is not dismissed after clicking the second toggle");
+ assert_true(innerPopover.matches(":popover-open"), "inner popover is open after clicking the second toggle");
+ }, "Popover light dismiss uses the flat tree");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-hint.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-hint.tentative.html
new file mode 100644
index 0000000000..f07363115f
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-hint.tentative.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover light dismiss behavior for hints</title>
+<meta name="timeout" content="long">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover-hint.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=outside></div>
+<div popover id=auto1>auto 1
+ <div popover id=auto2>auto 2
+ <div popover=hint id=innerhint1>inner hint 1
+ <div popover=hint id=innerhint2>inner hint 2
+ <div popover id=invalidauto1>Improperly nested auto 1</div>
+ </div>
+ </div>
+ </div>
+</div>
+<div popover=hint id=hint1>hint 1
+ <div popover=hint id=hint2>hint 2
+ <div popover id=invalidauto2>Improperly nested auto 2</div>
+ </div>
+</div>
+<div popover=manual id=manual1>Manual</div>
+
+<style>
+ [popover] {right:auto;bottom:auto;}
+ #auto1 {left:100px; top:100px;}
+ #auto2 {left:100px; top:200px;}
+ #innerhint1 {left:100px; top:300px;}
+ #innerhint2 {left:100px; top:400px;}
+ #invalidauto1 {left:100px; top:500px;}
+ #hint1 {left:200px; top:100px;}
+ #hint2 {left:200px; top:200px;}
+ #invalidauto1 {left:200px; top:400px;}
+ #manual1 {left:300px; top:100px;}
+ #outside {width:25px;height:25px}
+</style>
+
+<script>
+ const popovers = [
+ document.querySelector('#auto1'),
+ document.querySelector('#auto2'),
+ document.querySelector('#innerhint1'),
+ document.querySelector('#innerhint2'),
+ document.querySelector('#hint1'),
+ document.querySelector('#hint2'),
+ document.querySelector('#manual1'),
+ ];
+ function assertState(expectedState,description) {
+ description = description || 'Error';
+ const n = popovers.length;
+ assert_equals(expectedState.length,n,'Invalid');
+ for(let i=0;i<n;++i) {
+ assert_equals(popovers[i].matches(':popover-open'),expectedState[i],`${description}, index ${i} (${popovers[i].id})`);
+ }
+ }
+ function openall(t) {
+ // All popovers can be open at once, if shown in order:
+ popovers.forEach((p) => p.hidePopover());
+ popovers.forEach((p) => p.showPopover());
+ assertState(Array(popovers.length).fill(true),'All popovers should be able to be open at once');
+ t.add_cleanup(() => popovers.forEach((p) => p.hidePopover()));
+ }
+ function nvals(n,val) {
+ return new Array(n).fill(val);
+ }
+ for(let i=0;i<(popovers.length-1);++i) {
+ promise_test(async (t) => {
+ openall(t);
+ await clickOn(popovers[i]);
+ let expectedState = [...nvals(i+1,true),...nvals(popovers.length-i-2,false),true];
+ assertState(expectedState);
+ },`Mixed auto/hint light dismiss behavior, click on ${popovers[i].id}`);
+ }
+
+ promise_test(async (t) => {
+ openall(t);
+ await clickOn(outside);
+ assertState([false,false,false,false,false,false,true]);
+ },'Clicking outside closes all');
+
+ promise_test(async (t) => {
+ openall(t);
+ invalidauto1.showPopover();
+ assertState([true,true,false,false,false,false,true],'auto inside hint ignores the hints and gets nested within auto2');
+ assert_true(invalidauto1.matches(':popover-open'),'the inner nested auto should be open');
+ invalidauto1.hidePopover();
+ assertState([true,true,false,false,false,false,true]);
+ assert_false(invalidauto1.matches(':popover-open'));
+ },'Auto cannot be nested inside hint (invalidauto1)');
+
+ promise_test(async (t) => {
+ openall(t);
+ invalidauto2.showPopover();
+ assertState([false,false,false,false,false,false,true],'auto inside hint works as an independent (non-nested) auto');
+ assert_true(invalidauto2.matches(':popover-open'),'the inner nested auto should be open');
+ invalidauto2.hidePopover();
+ assertState([false,false,false,false,false,false,true]);
+ assert_false(invalidauto2.matches(':popover-open'));
+ },'Auto cannot be nested inside hint (invalidauto2)');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-on-scroll.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-on-scroll.html
new file mode 100644
index 0000000000..382addadef
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-on-scroll.html
@@ -0,0 +1,66 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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(':popover-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-with-anchor.tentative.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-with-anchor.tentative.tentative.html
new file mode 100644
index 0000000000..c4e545c4fb
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss-with-anchor.tentative.tentative.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover light dismiss with anchor behavior</title>
+<meta name="timeout" content="long">
+<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>
+
+<button id=p1anchor tabindex="0">Popover1 anchor (no action)</button>
+<div popover id=p1 anchor=p1anchor>
+ <span id=inside1>Inside popover 1</span>
+ <button id=b2 popovertarget='p2' popovertargetaction=show>Popover 2</button>
+</div>
+<div popover id=p2 anchor=b2>
+ <span id=inside2>Inside popover 2</span>
+</div>
+<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 popover1anchor = document.querySelector('#p1anchor');
+ const popover2 = document.querySelector('#p2');
+ const inside1 = document.querySelector('#inside1');
+ const inside2 = document.querySelector('#inside2');
+
+ 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 () => {
+ popover1.showPopover();
+ popover2.showPopover();
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ let p2HideCount = popover2HideCount;
+ await clickOn(inside2);
+ assert_true(popover1.matches(':popover-open'),'popover1 should be open');
+ assert_true(popover2.matches(':popover-open'),'popover2 should be open');
+ assert_equals(popover1HideCount,p1HideCount,'popover1');
+ assert_equals(popover2HideCount,p2HideCount,'popover2');
+ popover1.hidePopover();
+ assert_false(popover1.matches(':popover-open'));
+ assert_false(popover2.matches(':popover-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(':popover-open'));
+ assert_equals(popover1HideCount,p1HideCount);
+ assert_false(popover2.matches(':popover-open'));
+ assert_equals(popover2HideCount,p2HideCount+1);
+ popover1.hidePopover();
+ },'Clicking inside a parent popover should close child popover');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ assert_true(popover1.matches(':popover-open'));
+ await waitForRender();
+ await clickOn(popover1anchor);
+ assert_false(popover1.matches(':popover-open'),'popover1 should close');
+ },'Clicking on anchor element (that isn\'t an invoking element) shouldn\'t prevent its popover from being closed');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.html b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.html
new file mode 100644
index 0000000000..916d52ef5e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-light-dismiss.html
@@ -0,0 +1,607 @@
+<!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/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>
+
+<button id=b1t popovertarget='p1'>Popover 1</button>
+<button id=b1s popovertarget='p1' popovertargetaction=show>Popover 1</button>
+<span id=outside>Outside all popovers</span>
+<div popover id=p1>
+ <span id=inside1>Inside popover 1</span>
+ <button id=b2 popovertarget='p2' popovertargetaction=show>Popover 2</button>
+ <span id=inside1after>Inside popover 1 after button</span>
+ <div popover id=p2>
+ <span id=inside2>Inside popover 2</span>
+ </div>
+</div>
+<button id=after_p1 tabindex="0">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 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(':popover-open'));
+ popover1.showPopover();
+ assert_true(popover1.matches(':popover-open'));
+ let p1HideCount = popover1HideCount;
+ await clickOn(outside);
+ assert_false(popover1.matches(':popover-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(':popover-open'));
+ popover1.showPopover();
+ assert_true(popover1.matches(':popover-open'));
+ let p1HideCount = popover1HideCount;
+ await clickOn(outside);
+ assert_false(popover1.matches(':popover-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(':popover-open'));
+ popover1.showPopover();
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ await clickOn(inside1);
+ assert_true(popover1.matches(':popover-open'));
+ assert_equals(popover1HideCount,p1HideCount);
+ popover1.hidePopover();
+ },'Clicking inside a popover does not close that popover');
+
+ promise_test(async () => {
+ assert_false(popover1.matches(':popover-open'));
+ popover1.showPopover();
+ await waitForRender();
+ assert_true(popover1.matches(':popover-open'));
+ await new test_driver.Actions()
+ .pointerMove(0, 0, {origin: outside})
+ .pointerDown()
+ .send();
+ await waitForRender();
+ assert_true(popover1.matches(':popover-open'),'pointerdown (outside the popover) should not hide the popover');
+ await new test_driver.Actions()
+ .pointerUp()
+ .send();
+ await waitForRender();
+ assert_false(popover1.matches(':popover-open'),'pointerup (outside the popover) should trigger light dismiss');
+ },'Popovers close on pointerup, not pointerdown');
+
+ promise_test(async (t) => {
+ t.add_cleanup(() => popover1.hidePopover());
+ assert_false(popover1.matches(':popover-open'));
+ popover1.showPopover();
+ assert_true(popover1.matches(':popover-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(':popover-open'),`A synthetic "${eventName}" event should not hide the popover`);
+ }
+ await testOne('pointerup');
+ await testOne('pointerdown');
+ await testOne('mouseup');
+ await testOne('mousedown');
+ },'Synthetic events can\'t close popovers');
+
+ promise_test(async (t) => {
+ t.add_cleanup(() => popover1.hidePopover());
+ popover1.showPopover();
+ await clickOn(inside1After);
+ assert_true(popover1.matches(':popover-open'));
+ await sendTab();
+ assert_equals(document.activeElement,afterp1,'Focus should move to a button outside the popover');
+ assert_true(popover1.matches(':popover-open'));
+ },'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(':popover-open'),'popover1 should be open');
+ assert_true(popover2.matches(':popover-open'),'popover2 should be open');
+ assert_equals(popover1HideCount,p1HideCount,'popover1');
+ assert_equals(popover2HideCount,p2HideCount,'popover2');
+ popover1.hidePopover();
+ assert_false(popover1.matches(':popover-open'));
+ assert_false(popover2.matches(':popover-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(':popover-open'));
+ assert_equals(popover1HideCount,p1HideCount);
+ assert_false(popover2.matches(':popover-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(':popover-open'));
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ await clickOn(button1show);
+ assert_true(popover1.matches(':popover-open'),'popover1 should stay open');
+ assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown');
+ popover1.hidePopover(); // Cleanup
+ assert_false(popover1.matches(':popover-open'));
+ },'Clicking on invoking element, after using it for activation, shouldn\'t close its popover');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ assert_true(popover1.matches(':popover-open'));
+ assert_false(popover2.matches(':popover-open'));
+ await clickOn(button2);
+ assert_true(popover2.matches(':popover-open'),'button2 should activate popover2');
+ p2HideCount = popover2HideCount;
+ await clickOn(button2);
+ assert_true(popover2.matches(':popover-open'),'popover2 should stay open');
+ assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown');
+ popover1.hidePopover(); // Cleanup
+ assert_false(popover1.matches(':popover-open'));
+ assert_false(popover2.matches(':popover-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(':popover-open'));
+ assert_true(popover2.matches(':popover-open'));
+ p2HideCount = popover2HideCount;
+ await clickOn(button2);
+ assert_true(popover2.matches(':popover-open'),'popover2 should stay open');
+ assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown');
+ popover1.hidePopover(); // Cleanup
+ assert_false(popover1.matches(':popover-open'));
+ assert_false(popover2.matches(':popover-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(':popover-open'));
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ await clickOn(button1show);
+ assert_true(popover1.matches(':popover-open'),'popover1 should stay open');
+ assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown');
+ popover1.hidePopover(); // Cleanup
+ assert_false(popover1.matches(':popover-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(':popover-open'));
+ await waitForRender();
+ p1HideCount = popover1HideCount;
+ await clickOn(button1toggle);
+ assert_false(popover1.matches(':popover-open'),'popover1 should be hidden by popovertarget');
+ assert_equals(popover1HideCount,p1HideCount+1,'popover1 should get hidden only once by popovertarget');
+ },'Clicking on popovertarget element, even if it wasn\'t used for activation, should hide it exactly once');
+
+ promise_test(async () => {
+ popover1.showPopover();
+ popover2.showPopover(); // Popover1 is an ancestral element for popover2.
+ assert_true(popover1.matches(':popover-open'));
+ assert_true(popover2.matches(':popover-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(':popover-open'),'popover1 should be open');
+ assert_true(popover2.matches(':popover-open'),'popover1 should be open');
+ popover1.hidePopover();
+ assert_false(popover2.matches(':popover-open'));
+ },'Dragging from an open popover outside an open popover should leave the popover open');
+</script>
+
+<button id=b3 popovertarget=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 popovertarget=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(':popover-open'),'invoking element should open popover');
+ popover4.showPopover();
+ assert_true(popover4.matches(':popover-open'));
+ assert_false(popover3.matches(':popover-open'),'popover3 is unrelated to popover4');
+ popover4.hidePopover(); // Cleanup
+ assert_false(popover4.matches(':popover-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(':popover-open'));
+ assert_false(popover3.matches(':popover-open'));
+ popover3.showPopover();
+ assert_true(popover3.matches(':popover-open'));
+ assert_false(popover5.matches(':popover-open'),'Popover 5 was not invoked from popover3\'s invoker');
+ popover3.hidePopover();
+ assert_false(popover3.matches(':popover-open'));
+ },'An invoking element that was not used to invoke the popover is not 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 popovertarget=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();
+ await waitForRender();
+ assert_true(popover6.matches(':popover-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 shadowrootmode="open">
+ <button id=b7 popovertarget=p7 popovertargetaction=show tabindex="0">Popover7</button>
+ <div popover id=p7 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');
+ promise_test(async () => {
+ button7.click();
+ assert_true(popover7.matches(':popover-open'),'invoking element should open popover');
+ inside7.click();
+ assert_true(popover7.matches(':popover-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(':popover-open'));
+ await clickOn(outside);
+ assert_false(popover7.matches(':popover-open'));
+ },'Clicking outside a shadow DOM popover should close that popover');
+</script>
+
+<div popover id=p8>
+ <button tabindex="0">Button</button>
+ <span id=inside8after>Inside popover 8 after button</span>
+</div>
+<button id=p8invoker popovertarget=p8 tabindex="0">Popover8 invoker (no action)</button>
+<script>
+ promise_test(async () => {
+ const popover8 = document.querySelector('#p8');
+ const inside8After = document.querySelector('#inside8after');
+ const popover8Invoker = document.querySelector('#p8invoker');
+ assert_false(popover8.matches(':popover-open'));
+ popover8.showPopover();
+ await clickOn(inside8After);
+ assert_true(popover8.matches(':popover-open'));
+ await sendTab();
+ assert_equals(document.activeElement,popover8Invoker,'Focus should move to the invoker element');
+ assert_true(popover8.matches(':popover-open'),'popover should stay open');
+ popover8.hidePopover(); // Cleanup
+ },'Moving focus back to the invoker element should not dismiss the popover');
+</script>
+
+<!-- Convoluted ancestor relationship -->
+<div popover id=convoluted_p1>Popover 1
+ <button popovertarget=convoluted_p2>Open Popover 2</button>
+<div popover id=convoluted_p2>Popover 2
+ <button popovertarget=convoluted_p3>Open Popover 3</button>
+ <button popovertarget=convoluted_p2 popovertargetaction=show>Self-linked invoker</button>
+ </div>
+ <div popover id=convoluted_p3>Popover 3
+ <button popovertarget=convoluted_p4>Open Popover 4</button>
+ </div>
+ <div popover id=convoluted_p4><p>Popover 4</p></div>
+</div>
+<button onclick="convoluted_p1.showPopover()" tabindex="0">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(':popover-open'));
+ convPopover1.querySelector('button').click(); // Click to invoke p2
+ assert_true(convPopover1.matches(':popover-open'));
+ assert_true(convPopover2.matches(':popover-open'));
+ convPopover2.querySelector('button').click(); // Click to invoke p3
+ assert_true(convPopover1.matches(':popover-open'));
+ assert_true(convPopover2.matches(':popover-open'));
+ assert_true(convPopover3.matches(':popover-open'));
+ convPopover3.querySelector('button').click(); // Click to invoke p4
+ assert_true(convPopover1.matches(':popover-open'));
+ assert_true(convPopover2.matches(':popover-open'));
+ assert_true(convPopover3.matches(':popover-open'));
+ assert_true(convPopover4.matches(':popover-open'));
+ convPopover4.firstElementChild.click(); // Click within p4
+ assert_true(convPopover1.matches(':popover-open'));
+ assert_true(convPopover2.matches(':popover-open'));
+ assert_true(convPopover3.matches(':popover-open'));
+ assert_true(convPopover4.matches(':popover-open'));
+ convPopover1.hidePopover();
+ assert_false(convPopover1.matches(':popover-open'));
+ assert_false(convPopover2.matches(':popover-open'));
+ assert_false(convPopover3.matches(':popover-open'));
+ assert_false(convPopover4.matches(':popover-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(':popover-open'));
+ assert_true(convPopover2.matches(':popover-open'));
+ assert_false(convPopover3.matches(':popover-open'));
+ assert_false(convPopover4.matches(':popover-open'));
+ convPopover4.showPopover(); // Programmatically open p4
+ assert_true(convPopover1.matches(':popover-open'),'popover1 stays open because it is a DOM ancestor of popover4');
+ assert_false(convPopover2.matches(':popover-open'),'popover2 closes because it isn\'t connected to popover4 via active invokers');
+ assert_true(convPopover4.matches(':popover-open'));
+ convPopover4.firstElementChild.click(); // Click within p4
+ assert_true(convPopover1.matches(':popover-open'),'nothing changes');
+ assert_false(convPopover2.matches(':popover-open'));
+ assert_true(convPopover4.matches(':popover-open'));
+ convPopover1.hidePopover();
+ assert_false(convPopover1.matches(':popover-open'));
+ assert_false(convPopover2.matches(':popover-open'));
+ assert_false(convPopover3.matches(':popover-open'));
+ assert_false(convPopover4.matches(':popover-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(':popover-open') && p14.matches(':popover-open') && p15.matches(':popover-open'),'all three should be open');
+ p14.hidePopover();
+ assert_true(p13.matches(':popover-open'),'p13 should still be open');
+ assert_false(p14.matches(':popover-open'));
+ assert_false(p15.matches(':popover-open'));
+ p13.hidePopover(); // Cleanup
+},'Hide the target popover during "hide all popovers until"');
+</script>
+
+<div id=p16 popover>Popover 16
+ <div id=p17 popover>Popover 17</div>
+ <div id=p18 popover>Popover 18</div>
+</div>
+
+<script>
+promise_test(async () => {
+ p16.showPopover();
+ p18.showPopover();
+ let events = [];
+ const logEvents = (e) => {events.push(`${e.newState==='open' ? 'show' : 'hide'} ${e.target.id}`)};
+ p16.addEventListener('beforetoggle', logEvents);
+ p17.addEventListener('beforetoggle', logEvents);
+ p18.addEventListener('beforetoggle', (e) => {
+ logEvents(e);
+ p17.showPopover();
+ });
+ p16.hidePopover();
+ assert_array_equals(events,['hide p18','show p17','hide p16'],'There should not be a hide event for p17');
+ assert_false(p16.matches(':popover-open'));
+ assert_false(p17.matches(':popover-open'));
+ assert_false(p18.matches(':popover-open'));
+},'Show a sibling popover during "hide all popovers until"');
+</script>
+
+<div id=p19 popover>Popover 19</div>
+<div id=p20 popover>Popover 20</div>
+<button id=example2 tabindex="0">Example 2</button>
+
+<script>
+promise_test(async () => {
+ p19.showPopover();
+ let events = [];
+ const logEvents = (e) => {events.push(`${e.newState==='open' ? 'show' : 'hide'} ${e.target.id}`)};
+ p19.addEventListener('beforetoggle', (e) => {
+ logEvents(e);
+ p20.showPopover();
+ });
+ p20.addEventListener('beforetoggle', logEvents);
+ p19.hidePopover();
+ assert_array_equals(events,['hide p19','show p20'],'There should not be a second hide event for 19');
+ assert_false(p19.matches(':popover-open'));
+ assert_true(p20.matches(':popover-open'));
+ p20.hidePopover(); // Cleanup
+},'Show an unrelated popover during "hide popover"');
+</script>
+
+<div id=p21 popover>21
+ <div id=p22 popover>22</div>
+ <div id=p23 popover>23</div>
+ <div id=p24 popover>24</div>
+</div>
+
+<script>
+promise_test(async () => {
+ p21.showPopover();
+ p22.showPopover();
+ let events = [];
+ const logEvents = (e) => { events.push(`${e.newState === 'open' ? 'show' : 'hide'} ${e.target.id}`) };
+ p22.addEventListener('beforetoggle', (e) => {
+ logEvents(e);
+ p24.showPopover()
+ });
+ p23.addEventListener('beforetoggle', logEvents);
+ p24.addEventListener('beforetoggle', logEvents);
+ p23.showPopover();
+ assert_array_equals(events, ['show p23', 'hide p22', 'show p24'], 'hiding p24 does not fire event');
+ assert_false(p22.matches(':popover-open'));
+ assert_true(p23.matches(':popover-open'));
+ assert_false(p24.matches(':popover-open'));
+ p21.hidePopover(); // Cleanup
+},'Show other auto popover during "hide all popover until"');
+</script>
+
+<div id=p25 popover>
+ <div id=p26 popover>26</div>
+ <div id=p27 popover>27</div>
+ <div id=p28 popover>28</div>
+</div>
+<script>
+promise_test(async () => {
+ p25.showPopover();
+ p26.showPopover();
+ let events = [];
+ const logEvents = (e) => { events.push(`${e.newState === 'open' ? 'show' : 'hide'} ${e.target.id}`) };
+ p26.addEventListener('beforetoggle', (e) => {
+ logEvents(e);
+ p28.showPopover();
+ });
+ p27.addEventListener('beforetoggle', logEvents);
+ p28.addEventListener('beforetoggle', (e) => {
+ logEvents(e);
+ p27.showPopover();
+ });
+ p27.showPopover();
+ assert_array_equals(events, ['show p27', 'hide p26', 'show p28', 'show p27'], 'Nested showPopover should not fire event for its HideAllPopoversUntil');
+ assert_false(p26.matches(':popover-open'));
+ assert_true(p27.matches(':popover-open'));
+ assert_false(p28.matches(':popover-open'));
+ p25.hidePopover(); // Cleanup
+}, 'Nested showPopover');
+</script>
+
+<div id=p29 popover>Popover 29</div>
+<button id=b29 popovertarget=p29>Open popover 29</button>
+<iframe id=iframe29 width=100 height=100></iframe>
+<script>
+promise_test(async () => {
+ let iframe_url = (new URL("/common/blank.html", location.href)).href;
+ iframe29.src = iframe_url;
+ iframe29.contentDocument.body.style.height = '100%';
+ assert_false(p29.matches(':popover-open'));
+ p29.showPopover();
+ assert_true(p29.matches(':popover-open'));
+ let actions = new test_driver.Actions();
+ await actions.pointerMove(0,0,{origin: b29})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .send();
+
+ actions = new test_driver.Actions();
+ await actions.pointerMove(0,0,{origin: iframe29.contentDocument.body})
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .send();
+ assert_true(p29.matches(':popover-open'));
+},`Pointer down in one document and pointer up in another document shouldn't dismiss popover`);
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-manual-crash.html b/testing/web-platform/tests/html/semantics/popovers/popover-manual-crash.html
new file mode 100644
index 0000000000..535eb4c7d1
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-manual-crash.html
@@ -0,0 +1,32 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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-move-documents.html b/testing/web-platform/tests/html/semantics/popovers/popover-move-documents.html
new file mode 100644
index 0000000000..11f52c2f2f
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-move-documents.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/issues/9177">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async function iframeLoaded(iframe) {
+ return new Promise(resolve => {
+ if (iframe.contentWindow.document.readyState == 'complete') {
+ resolve();
+ } else {
+ iframe.onload = resolve;
+ }
+ });
+}
+</script>
+
+<iframe id=myframe srcdoc="<p>iframe</p>"></iframe>
+<div id=p1 popover=auto>p1</div>
+<script>
+promise_test(async () => {
+ await iframeLoaded(myframe);
+ await new Promise(resolve => {
+ if (myframe.contentWindow.document.readyState == 'complete') {
+ resolve();
+ } else {
+
+ }
+ });
+ p1.addEventListener('beforetoggle', () => {
+ myframe.contentWindow.document.body.appendChild(p1);
+ });
+ assert_throws_dom('InvalidStateError', () => p1.showPopover());
+}, 'Moving popovers between documents while showing should throw an exception.');
+</script>
+
+<iframe id=myframe2 srcdoc="<p>iframe</p>"></iframe>
+<div id=p2 popover=auto>p2</div>
+<script>
+promise_test(async () => {
+ await iframeLoaded(myframe2);
+ const p2 = document.getElementById('p2');
+ p2.showPopover();
+ p2.addEventListener('beforetoggle', () => {
+ myframe2.contentWindow.document.body.appendChild(p2);
+ });
+ assert_true(p2.matches(':popover-open'),
+ 'The popover should be open after calling showPopover()');
+
+ p2.hidePopover();
+ assert_false(p2.matches(':popover-open'),
+ 'The popover should be closed after moving it between documents.');
+}, 'Moving popovers between documents while hiding should not throw an exception.');
+</script>
+
+<iframe id=myframe3 srcdoc="<p>iframe</p>"></iframe>
+<div id=p3 popover=auto>
+ p3
+ <div id=p4 popover=auto>p4</div>
+ <div id=p5 popover=auto>p5</div>
+</div>
+<script>
+promise_test(async () => {
+ await iframeLoaded(myframe3);
+ p3.showPopover();
+ p4.showPopover();
+ p4.addEventListener('beforetoggle', event => {
+ if (event.newState === 'closed') {
+ assert_true(p3.matches(':popover-open'),
+ 'p3 should be showing in the event handler.');
+ assert_true(p4.matches(':popover-open'),
+ 'p4 should be showing in the event handler.');
+ assert_equals(event.target, p4,
+ 'The events target should be p4.');
+ myframe3.contentWindow.document.body.appendChild(p5);
+ }
+ });
+ assert_true(p3.matches(':popover-open'),
+ 'p3 should be open after calling showPopover on it.');
+ assert_true(p4.matches(':popover-open'),
+ 'p4 should be open after calling showPopover on it.');
+
+ const p5 = document.getElementById('p5');
+ assert_throws_dom('InvalidStateError', () => p5.showPopover());
+ assert_false(p5.matches(':popover-open'),
+ 'p5 should be closed after moving it between documents.');
+}, 'Moving popovers between documents during light dismiss should throw an exception.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-not-keyboard-focusable.html b/testing/web-platform/tests/html/semantics/popovers/popover-not-keyboard-focusable.html
new file mode 100644
index 0000000000..55c70aa643
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-not-keyboard-focusable.html
@@ -0,0 +1,49 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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 tabindex="0">Button 1</button>
+<div popover>
+ <p>This is a popover without a focusable element</p>
+</div>
+<button id=secondfocus tabindex="0">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(':popover-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(':popover-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');
+ button.setAttribute("tabindex", "0");
+ 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(':popover-open'),'changing focus to the popover should leave it showing');
+ popover.hidePopover();
+ assert_false(popover.matches(':popover-open'));
+}, "Popover should not be keyboard focusable");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-open-display-ref.html b/testing/web-platform/tests/html/semantics/popovers/popover-open-display-ref.html
new file mode 100644
index 0000000000..144b81e645
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-display-ref.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.html b/testing/web-platform/tests/html/semantics/popovers/popover-open-display.html
new file mode 100644
index 0000000000..bc4d16fe80
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-display.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=match href="popover-open-display-ref.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-in-beforetoggle.html b/testing/web-platform/tests/html/semantics/popovers/popover-open-in-beforetoggle.html
new file mode 100644
index 0000000000..1e22b73c68
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-in-beforetoggle.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Popover beforetoggle event opening new popovers</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://html.spec.whatwg.org/multipage/indices.html#event-beforetoggle">
+<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 id=p1>Popover 1
+ <div popover id=p2>Popover 2
+ <div popover id=p3>Popover 3</div>
+ </div>
+</div>
+<div id=outside>Outside</div>
+<dialog id=dialog>Dialog</dialog>
+
+<script>
+ function getSignal(t) {
+ const controller = new AbortController();
+ t.add_cleanup(() => controller.abort());
+ return controller.signal;
+ }
+
+ test((t) => {
+ p1.showPopover();
+ p1.addEventListener('beforetoggle',() => p2.showPopover(),{signal: getSignal(t)});
+ p1.hidePopover(); // Potential crash here
+ assert_false(p1.matches(':popover-open'));
+ assert_false(p2.matches(':popover-open'));
+ },'Open popover from closing beforetoggle event');
+
+ test((t) => {
+ p1.showPopover();
+ p1.addEventListener('beforetoggle',() => p2.showPopover(),{signal: getSignal(t)});
+ p2.addEventListener('beforetoggle',() => p3.showPopover(),{signal: getSignal(t)});
+ p1.hidePopover(); // Potential crash here
+ assert_false(p1.matches(':popover-open'));
+ assert_false(p2.matches(':popover-open'));
+ assert_false(p3.matches(':popover-open'));
+ },'Open double-nested popovers from closing beforetoggle event');
+
+ test(t => {
+ p1.showPopover();
+ p1.addEventListener('beforetoggle',() => p2.showPopover(),{signal: getSignal(t)});
+ p2.addEventListener('beforetoggle',() => p3.showPopover(),{signal: getSignal(t)});
+ dialog.showModal(); // Potential crash here
+ assert_false(p1.matches(':popover-open'));
+ assert_false(p2.matches(':popover-open'));
+ assert_false(p3.matches(':popover-open'));
+ dialog.close();
+ },'Open double-nested popovers from closing beforetoggle event, dialog open');
+
+ promise_test(async t => {
+ p1.showPopover();
+ p1.addEventListener('beforetoggle',() => p2.showPopover(),{signal: getSignal(t)});
+ p2.addEventListener('beforetoggle',() => p3.showPopover(),{signal: getSignal(t)});
+ await clickOn(outside); // Potential crash here
+ assert_false(p1.matches(':popover-open'));
+ assert_false(p2.matches(':popover-open'));
+ assert_false(p3.matches(':popover-open'));
+ },'Open double-nested popovers from closing beforetoggle event, light dismiss');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-2.html b/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-2.html
new file mode 100644
index 0000000000..f2388b7642
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-2.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/popover-utils.js"></script>
+<script>
+async function checkStatus(p) {
+ p.showPopover();
+ await waitForRender();
+ assert_true(p.matches(":popover-open"));
+ p.hidePopover();
+ await waitForRender();
+}
+</script>
+
+<div id=container style="overflow: hidden; position: absolute;">
+ <div popover="auto" id=p1 style="position: absolute; top: 100px;">Absolute popover inside absolute element</div>
+</div>
+<script>
+promise_test(async () => {
+ await checkStatus(document.querySelector("#p1"));
+}, "Absolute popover inside absolute element");
+</script>
+
+<div id=p2 popover="auto" style="overflow: hidden; position: absolute;">
+ <div style="position: absolute; top: 100px;">Absolute element inside absolute popover</div>
+</div>
+<script>
+promise_test(async () => {
+ await checkStatus(document.querySelector("#p2"));
+}, "Absolute element inside absolute popover");
+</script>
+
+<div id=container style="overflow: hidden; position: fixed;">
+ <div popover="auto" id=p3 style="position: fixed; top: 100px;">Fixed popover inside fixed element</div>
+</div>
+<script>
+promise_test(async () => {
+ await checkStatus(document.querySelector("#p3"));
+}, "Fixed popover inside fixed element");
+</script>
+
+<div id=p4 popover="auto" style="overflow: hidden; position: fixed;">
+ <div style="position: fixed; top: 100px;">Fixed element inside fixed popover</div>
+</div>
+<script>
+promise_test(async () => {
+ await checkStatus(document.querySelector("#p4"));
+}, "Fixed element inside fixed popover");
+</script>
+
+<div id=container style="overflow: hidden; position: fixed;">
+ <div popover="auto" id=p5 style="position: absolute; top: 100px;">Absolute popover inside fixed element</div>
+</div>
+<script>
+promise_test(async () => {
+ await checkStatus(document.querySelector("#p5"));
+}, "Absolute popover inside fixed element");
+</script>
+
+<div id=p6 popover="auto" style="overflow: hidden; position: absolute;">
+ <div style="position: fixed; top: 100px;">Fixed element inside absolute popover</div>
+</div>
+<script>
+promise_test(async () => {
+ await checkStatus(document.querySelector("#p6"));
+}, "Fixed element inside absolute popover");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-ref.html b/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-ref.html
new file mode 100644
index 0000000000..0d14050e85
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display-ref.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..3d4d833063
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-open-overflow-display.tentative.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel=author href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=match href="popover-open-overflow-display-ref.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-overlay.html b/testing/web-platform/tests/html/semantics/popovers/popover-overlay.html
new file mode 100644
index 0000000000..a607844aee
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-overlay.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<title>popover: overlay</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute">
+<link rel="help" href="https://drafts.csswg.org/css-position-4/#overlay">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<dialog popover id="popover-show-dialog"></dialog>
+<dialog popover id="popover-show-modal-dialog"></dialog>
+<dialog popover id="popover-dialog"></dialog>
+<div popover id="popover-div"></div>
+<script>
+ test(() => {
+ const popover_show_dialog = document.getElementById("popover-show-dialog");
+ assert_equals(getComputedStyle(popover_show_dialog).overlay, "none",
+ "Computed overlay");
+ popover_show_dialog.show();
+ assert_equals(getComputedStyle(popover_show_dialog).overlay, "none",
+ "Computed overlay after show()");
+ popover_show_dialog.close();
+ }, "dialog.show() should not put popover dialog in top layer");
+
+ test(() => {
+ const popover_show_modal_dialog = document.getElementById("popover-show-modal-dialog");
+ assert_equals(getComputedStyle(popover_show_modal_dialog).overlay, "none",
+ "Computed overlay");
+ popover_show_modal_dialog.showModal();
+ assert_equals(getComputedStyle(popover_show_modal_dialog).overlay, "auto",
+ "Computed overlay after showModal()");
+ popover_show_modal_dialog.close();
+ }, "dialog.showModal() should put popover dialog in top layer");
+
+ test(() => {
+ const popover_dialog = document.getElementById("popover-dialog");
+ assert_equals(getComputedStyle(popover_dialog).overlay, "none",
+ "Computed overlay");
+ popover_dialog.showPopover();
+ assert_equals(getComputedStyle(popover_dialog).overlay, "auto",
+ "Computed overlay after showPopover()");
+ popover_dialog.hidePopover();
+ }, "dialog.showPopover() should put popover dialog in top layer");
+
+ test(() => {
+ const popover_div = document.getElementById("popover-div");
+ assert_equals(getComputedStyle(popover_div).overlay, "none",
+ "Computed overlay");
+ popover_div.showPopover();
+ assert_equals(getComputedStyle(popover_div).overlay, "auto",
+ "Computed overlay after showPopover()");
+ popover_div.hidePopover();
+ }, "div.showPopover() should put popover div in top layer");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-removal-2.html b/testing/web-platform/tests/html/semantics/popovers/popover-removal-2.html
new file mode 100644
index 0000000000..b21c0bb557
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-removal-2.html
@@ -0,0 +1,29 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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(':popover-open'));
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ frame2Doc.body.appendChild(popover);
+ assert_false(popover.matches(':popover-open'));
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ }, 'Moving popover between documents shouldn\'t cause issues');
+ };
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-removal.html b/testing/web-platform/tests/html/semantics/popovers/popover-removal.html
new file mode 100644
index 0000000000..d2b664b464
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-removal.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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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(':popover-open'));
+ popover.showPopover();
+ assert_true(popover.matches(':popover-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.html b/testing/web-platform/tests/html/semantics/popovers/popover-shadow-dom.html
new file mode 100644
index 0000000000..62aa135b56
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-shadow-dom.html
@@ -0,0 +1,202 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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 shadowrootmode=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(':popover-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 shadowrootmode=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(':popover-open'));
+ assert_true(isElementVisible(popover1));
+ popover2.showPopover();
+ assert_false(popover1.matches(':popover-open'), 'popover1 open'); // P1 was closed by P2
+ assert_false(isElementVisible(popover1), 'popover1 visible');
+ assert_true(popover2.matches(':popover-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 shadowrootmode=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(':popover-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(':popover-open'));
+ assert_true(isElementVisible(popover2));
+ assert_true(popover1.matches(':popover-open'));
+ assert_true(isElementVisible(popover1));
+ popover1.hidePopover();
+ await waitForRender();
+ assert_false(popover1.matches(':popover-open'));
+ assert_false(isElementVisible(popover1));
+ assert_false(popover2.matches(':popover-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 shadowrootmode=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(':popover-open'), 'popover1 not open');
+ assert_true(isElementVisible(popover1));
+ assert_true(popover2.matches(':popover-open'), 'popover2 not open');
+ assert_true(isElementVisible(popover2));
+ // This should hide both of them.
+ popover1.hidePopover();
+ await waitForRender();
+ assert_false(popover1.matches(':popover-open'));
+ assert_false(isElementVisible(popover1));
+ assert_false(popover2.matches(':popover-open'));
+ assert_false(isElementVisible(popover2));
+ }, "The popover stack is preserved across shadow-inclusive ancestors");
+</script>
+
+
+<div id=test5>
+ <template shadowrootmode=open>
+ <button popovertarget=p1>Test 5 Popover 1</button>
+ <div popover id=p1>Popover 1
+ <p>This should not get hidden when popover2 opens.</p>
+ <button popovertarget=p2>Click</button>
+ </div>
+ <div popover id=p2>Popover 2
+ <p>This should not hide popover1.</p>
+ </div>
+ </template>
+</div>
+<script>
+ promise_test(async function() {
+ const [popover1,popover2] = getPopoverReferences('test5');
+ popover1.showPopover();
+ popover1.querySelector('button').click(); // Use invoker to keep 2 visible
+ // Both 1 and 2 should be open at this point.
+ assert_true(popover1.matches(':popover-open'), 'popover1 not open');
+ assert_true(isElementVisible(popover1));
+ assert_true(popover2.matches(':popover-open'), 'popover2 not open');
+ assert_true(isElementVisible(popover2));
+ // This should hide both of them.
+ popover1.hidePopover();
+ await waitForRender();
+ assert_false(popover1.matches(':popover-open'));
+ assert_false(isElementVisible(popover1));
+ assert_false(popover2.matches(':popover-open'));
+ assert_false(isElementVisible(popover2));
+ }, "Popover ancestor relationships are within a root, not within the document");
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-shadowhost-focus.html b/testing/web-platform/tests/html/semantics/popovers/popover-shadowhost-focus.html
new file mode 100644
index 0000000000..91ee547d4e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-shadowhost-focus.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/issues/8994">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div popover=auto tabindex=0 data-test="autofocus=true, delegatesfocus=false" autofocus class=should-be-focused>
+ <template shadowrootmode=open>
+ <button autofocus>autofocus button</button>
+ </template>
+</div>
+
+<!-- The autofocus popover is what focus() gets called on, but since it has a
+ delegatesFocus shadowroot, focus() itself goes into the shadowroot. -->
+<div popover=auto tabindex=0 data-test="autofocus=true, delegatesfocus=true" autofocus>
+ <template shadowrootmode=open shadowrootdelegatesfocus>
+ <button autofocus class=should-be-focused>autofocus button</button>
+ </template>
+</div>
+
+<div popover=auto tabindex=0 data-test="autofocus=false, delegatesfocus=false">
+ <template shadowrootmode=open>
+ <button autofocus>autofocus button</button>
+ </template>
+</div>
+
+<div popover=auto tabindex=0 data-test="autofocus=false, delegatesfocus=true">
+ <template shadowrootmode=open shadowrootdelegatesfocus>
+ <button autofocus>autofocus button</button>
+ </template>
+</div>
+
+<script>
+document.querySelectorAll('body > [popover]').forEach(popover => {
+ promise_test(async () => {
+ const expectedFocusedElement = (popover.matches('.should-be-focused') ? popover : null)
+ || popover.querySelector('.should-be-focused')
+ || popover.shadowRoot.querySelector('.should-be-focused')
+ || document.body;
+
+ popover.showPopover();
+
+ let actualFocusedElement = document.activeElement;
+ if (actualFocusedElement.shadowRoot && actualFocusedElement.shadowRoot.activeElement) {
+ actualFocusedElement = actualFocusedElement.shadowRoot.activeElement;
+ }
+
+ popover.hidePopover();
+
+ // Resetting focus may happen asynchronously
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ assert_equals(actualFocusedElement, expectedFocusedElement);
+ }, popover.getAttribute('data-test'));
+});
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-stacking-anchor-attribute.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-anchor-attribute.tentative.html
new file mode 100644
index 0000000000..6895b8625a
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-anchor-attribute.tentative.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://www.w3.org/TR/css-anchor-position-1/#implicit">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- Once this test is made non-tentative, it can be folded back into popover-stacking.html -->
+
+<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(':popover-open'));
+ assert_false(child.matches(':popover-open'));
+ ancestor.showPopover();
+ if (clickToActivate)
+ clickToActivate.click();
+ else
+ child.showPopover();
+ assert_true(child.matches(':popover-open'));
+ assert_true(ancestor.matches(':popover-open'));
+ ancestor.hidePopover();
+ assert_false(ancestor.matches(':popover-open'));
+ assert_false(child.matches(':popover-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(':popover-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")
+</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-stacking-context-ref.html b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context-ref.html
new file mode 100644
index 0000000000..4d4ca6973f
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context-ref.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.html b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context.html
new file mode 100644
index 0000000000..ba4e85a897
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-stacking-context.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=match href="popover-stacking-context-ref.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.html b/testing/web-platform/tests/html/semantics/popovers/popover-stacking.html
new file mode 100644
index 0000000000..1c352d566e
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-stacking.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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>popovertarget attribute relationship</p>
+ <div popover class=ancestor><p>Ancestor popover</p>
+ <button popovertarget=trigger1 class=clickme>Button</button>
+ </div>
+ <div id=trigger1 popover class=child><p>Child popover</p></div>
+</div>
+
+<div class="example">
+ <p>nested popovertarget attribute relationship</p>
+ <div popover class=ancestor><p>Ancestor popover</p>
+ <div>
+ <div>
+ <button popovertarget=trigger2 class=clickme>Button</button>
+ </div>
+ </div>
+ </div>
+ <div id=trigger2 popover class=child><p>Child popover</p></div>
+</div>
+
+<!-- Other examples -->
+
+<button id=b1 onclick='p1.showPopover()'>Popover 1</button>
+<div popover id=p1><p>This is popover #1</p>
+ <button id=b2 onclick='p2.showPopover()'>Popover 2</button>
+ <div popover id=p2><p>This is popover #2</p>
+ <button id=b3 onclick='p3.showPopover()'>Popover 3</button>
+ <div popover id=p3><p>This is popover #3</p></div>
+ </div>
+</div>
+
+<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(':popover-open'));
+ assert_false(child.matches(':popover-open'));
+ ancestor.showPopover();
+ if (clickToActivate)
+ clickToActivate.click();
+ else
+ child.showPopover();
+ assert_true(child.matches(':popover-open'));
+ assert_true(ancestor.matches(':popover-open'));
+ ancestor.hidePopover();
+ assert_false(ancestor.matches(':popover-open'));
+ assert_false(child.matches(':popover-open'));
+ },descr);
+ }
+
+ const popovers = [p1, p2, p3];
+
+ function assertState(...states) {
+ assert_equals(popovers.length,states.length);
+ for(let i=0;i<popovers.length;++i) {
+ assert_equals(popovers[i].matches(':popover-open'),states[i],`Popover #${i+1} incorrect state`);
+ }
+ }
+
+ test(function() {
+ function openManyPopovers() {
+ p1.showPopover();
+ p2.showPopover();
+ p3.showPopover();
+ assertState(true,true,true);
+ }
+ openManyPopovers();
+ d1.show(); // Dialog.show() should hide all popovers.
+ assertState(false,false,false);
+ d1.close();
+ openManyPopovers();
+ d1.showModal(); // Dialog.showModal() should also hide all popovers.
+ assertState(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);
+ 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; }
+</style>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-target-action-hover.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-target-action-hover.tentative.html
new file mode 100644
index 0000000000..b03ec78ebf
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-target-action-hover.tentative.html
@@ -0,0 +1,180 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>The popovertargetaction=hover behavior</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>
+
+<body>
+<style>
+.unrelated {top:0;}
+.invoker {top:100px; width:fit-content; height:fit-content;}
+[popover] {top: 200px;}
+.offset-child {top:300px; left:300px;}
+</style>
+
+<script>
+const popoverShowDelay = 100; // The CSS delay setting.
+const hoverWaitTime = 200; // How long to wait to cover the delay for sure.
+async function makePopoverAndInvoker(test, popoverType, invokerType, delayMs) {
+ delayMs = delayMs || popoverShowDelay;
+ const popover = Object.assign(document.createElement('div'),{popover: popoverType});
+ document.body.appendChild(popover);
+ popover.textContent = 'Popover';
+ // Set popover-show-delay on the popover to 0 - it should be ignored.
+ popover.setAttribute('style',`popover-show-delay: 0; popover-hide-delay: 1000s;`);
+ let invoker = document.createElement('button');
+ invoker.setAttribute('class','invoker');
+ invoker.popoverTargetElement = popover;
+ invoker.popoverTargetAction = "hover";
+ // Set popover-hide-delay on the invoker to 0 - it should be ignored.
+ invoker.setAttribute('style',`popover-show-delay: ${delayMs}ms; popover-hide-delay: 0;`);
+ document.body.appendChild(invoker);
+ const actualHoverDelay = Number(getComputedStyle(invoker)['popoverShowDelay'].slice(0,-1))*1000;
+ assert_equals(actualHoverDelay,delayMs,'popover-show-delay is incorrect');
+ const originalInvoker = invoker;
+ const reassignPopoverFn = (p) => {originalInvoker.popoverTargetElement = p};
+ switch (invokerType) {
+ case 'plain':
+ // Invoker is just a button.
+ invoker.textContent = 'Invoker';
+ break;
+ case 'nested':
+ // Invoker is just a button containing a div.
+ const child1 = invoker.appendChild(document.createElement('div'));
+ child1.textContent = 'Invoker';
+ break;
+ case 'nested-offset':
+ // Invoker is a child of the invoking button, and is not contained within
+ // the bounds of the popovertarget element.
+ invoker.textContent = 'Invoker';
+ // Reassign invoker to the child:
+ invoker = invoker.appendChild(document.createElement('div'));
+ invoker.textContent = 'Invoker child';
+ invoker.setAttribute('class','offset-child');
+ break;
+ case 'none':
+ // No invoker.
+ invoker.remove();
+ break;
+ default:
+ assert_unreached(`Invalid invokerType ${invokerType}`);
+ }
+ const unrelated = document.createElement('div');
+ document.body.appendChild(unrelated);
+ unrelated.textContent = 'Unrelated';
+ unrelated.setAttribute('class','unrelated');
+ test.add_cleanup(async () => {
+ popover.remove();
+ invoker.remove();
+ originalInvoker.remove();
+ unrelated.remove();
+ await waitForRender();
+ });
+ await mouseOver(unrelated); // Start by mousing over the unrelated element
+ await waitForRender();
+ return {popover,invoker,reassignPopoverFn};
+}
+
+// NOTE about testing methodology:
+// This test checks whether popovers are triggered *after* the appropriate hover
+// delay. The delay used for testing is kept low, to avoid this test taking too
+// long, but that means that sometimes on a slow bot/client, the hover delay can
+// elapse before we are able to check the popover status. And that can make this
+// test flaky. To avoid that, the msSinceMouseOver() function is used to check
+// that not-too-much time has passed, and if it has, the test is simply skipped.
+
+["auto","hint","manual"].forEach(type => {
+ ["plain","nested","nested-offset"].forEach(invokerType => {
+ promise_test(async (t) => {
+ const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
+ assert_false(popover.matches(':popover-open'));
+ await mouseOver(invoker);
+ let showing = popover.matches(':popover-open');
+ // See NOTE above.
+ if (msSinceMouseOver() < popoverShowDelay)
+ assert_false(showing,'popover should not show immediately');
+ await waitForHoverTime(hoverWaitTime);
+ assert_true(msSinceMouseOver() >= hoverWaitTime,'waitForHoverTime should wait the specified time');
+ assert_true(popover.matches(':popover-open'),'popover should show after delay');
+ assert_true(hoverWaitTime > popoverShowDelay,'popoverShowDelay is the CSS setting, hoverWaitTime should be longer than that');
+ popover.hidePopover(); // Cleanup
+ },`popovertargetaction=hover shows a popover with popover=${type}, invokerType=${invokerType}`);
+
+ promise_test(async (t) => {
+ const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
+ assert_false(popover.matches(':popover-open'));
+ invoker.click(); // Click the invoker
+ assert_true(popover.matches(':popover-open'),'Clicking the invoker should show the popover, even when popovertargetaction=hover');
+ popover.hidePopover(); // Cleanup
+ },`popovertargetaction=hover should also allow click activation, for popover=${type}, invokerType=${invokerType}`);
+
+ promise_test(async (t) => {
+ const longerHoverDelay = hoverWaitTime*2;
+ const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType,longerHoverDelay);
+ await mouseOver(invoker);
+ let showing = popover.matches(':popover-open');
+ // See NOTE above.
+ if (msSinceMouseOver() >= longerHoverDelay)
+ return; // The WPT runner was too slow.
+ assert_false(showing,'popover should not show immediately');
+ await waitForHoverTime(hoverWaitTime);
+ showing = popover.matches(':popover-open');
+ if (msSinceMouseOver() >= longerHoverDelay)
+ return; // The WPT runner was too slow.
+ assert_false(showing,'popover should not show after not long enough of a delay');
+ },`popovertargetaction=hover popover-show-delay is respected (popover=${type}, invokerType=${invokerType})`);
+
+ promise_test(async (t) => {
+ const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+ await mouseOver(invoker);
+ assert_true(popover.matches(':popover-open'),'popover should stay showing on mouseover');
+ await waitForHoverTime(hoverWaitTime);
+ assert_true(popover.matches(':popover-open'),'popover should stay showing after delay');
+ popover.hidePopover(); // Cleanup
+ },`popovertargetaction=hover does nothing when popover is already showing (popover=${type}, invokerType=${invokerType})`);
+
+ promise_test(async (t) => {
+ const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
+ await mouseOver(invoker);
+ let showing = popover.matches(':popover-open');
+ popover.remove();
+ // See NOTE above.
+ if (msSinceMouseOver() >= popoverShowDelay)
+ return; // The WPT runner was too slow.
+ assert_false(showing,'popover should not show immediately');
+ await waitForHoverTime(hoverWaitTime);
+ assert_false(popover.matches(':popover-open'),'popover should not show even after a delay');
+ // Now put it back in the document and make sure it doesn't trigger.
+ document.body.appendChild(popover);
+ await waitForHoverTime(hoverWaitTime);
+ assert_false(popover.matches(':popover-open'),'popover should not show even when returned to the document');
+ },`popovertargetaction=hover does nothing when popover is moved out of the document (popover=${type}, invokerType=${invokerType})`);
+
+ promise_test(async (t) => {
+ const {popover,invoker,reassignPopoverFn} = await makePopoverAndInvoker(t,type,invokerType);
+ const popover2 = Object.assign(document.createElement('div'),{popover: type});
+ document.body.appendChild(popover2);
+ t.add_cleanup(() => popover2.remove());
+ await mouseOver(invoker);
+ let eitherShowing = popover.matches(':popover-open') || popover2.matches(':popover-open');
+ reassignPopoverFn(popover2);
+ // See NOTE above.
+ if (msSinceMouseOver() >= popoverShowDelay)
+ return; // The WPT runner was too slow.
+ assert_false(eitherShowing,'popover should not show immediately');
+ await waitForHoverTime(hoverWaitTime);
+ assert_false(popover.matches(':popover-open'),'popover #1 should not show since popovertarget was reassigned');
+ assert_false(popover2.matches(':popover-open'),'popover #2 should not show since popovertarget was reassigned');
+ },`popovertargetaction=hover does nothing when target changes (popover=${type}, invokerType=${invokerType})`);
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-target-element-disabled.html b/testing/web-platform/tests/html/semantics/popovers/popover-target-element-disabled.html
new file mode 100644
index 0000000000..d5c951768c
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-target-element-disabled.html
@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/pull/8221#discussion_r1049379113">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id=outerpopover popover=auto>
+ <button popovertarget=innerpopover disabled>toggle popover</button>
+</div>
+<div id=innerpopover popover=auto>popover</div>
+<script>
+test(() => {
+ outerpopover.showPopover();
+ outerpopover.querySelector('button').click(); // Invoke innerpopover
+ assert_false(innerpopover.matches(':popover-open'),
+ 'disabled button shouldn\'t open the target popover');
+ assert_true(outerpopover.matches(':popover-open'));
+ innerpopover.showPopover();
+ assert_true(innerpopover.matches(':popover-open'),
+ 'The inner popover should be able to open successfully.');
+ assert_false(outerpopover.matches(':popover-open'),
+ 'The outer popover should be closed by opening the inner one.');
+}, 'Disabled popover*target buttons should not affect the popover heirarchy.');
+</script>
+
+<div id=outerpopover2 popover=auto>
+ <button id=togglebutton2 popovertarget=innerpopover2>toggle popover</button>
+</div>
+<div id=innerpopover2 popover=auto>popover</div>
+<script>
+test(() => {
+ outerpopover2.showPopover();
+ outerpopover2.querySelector('button').click(); // Invoke innerpopover
+ assert_true(innerpopover2.matches(':popover-open'),
+ 'The inner popover should be able to open successfully.');
+ assert_true(outerpopover2.matches(':popover-open'),
+ 'The outer popover should stay open when opening the inner one.');
+
+ togglebutton2.disabled = true;
+ assert_true(innerpopover2.matches(':popover-open'),
+ 'Changing disabled states after popovers are open shouldn\'t close anything');
+ assert_true(outerpopover2.matches(':popover-open'),
+ 'Changing disabled states after popovers are open shouldn\'t close anything');
+}, 'Disabling popover*target buttons when popovers are open should not cause popovers to be closed.');
+</script>
+
+<div id=outerpopover4 popover=auto>
+ <button id=togglebutton4 popovertarget=innerpopover4>toggle popover</button>
+</div>
+<div id=innerpopover4 popover=auto>popover</div>
+<form id=submitform>form</form>
+<script>
+test(() => {
+ outerpopover4.showPopover();
+ outerpopover4.querySelector('button').click(); // Invoke innerpopover
+ assert_true(innerpopover4.matches(':popover-open'),
+ 'The inner popover should be able to open successfully.');
+ assert_true(outerpopover4.matches(':popover-open'),
+ 'The outer popover should stay open when opening the inner one.');
+
+ togglebutton4.setAttribute('form', 'submitform');
+ assert_true(innerpopover4.matches(':popover-open'),
+ 'The inner popover be should be not closed when invoking buttons cease to be invokers.');
+ assert_true(outerpopover4.matches(':popover-open'),
+ 'The outer popover be should be not closed when invoking buttons cease to be invokers.');
+}, 'Setting the form attribute on popover*target buttons when popovers are open should not close them.');
+</script>
+
+<div id=outerpopover5 popover=auto>
+ <input type=button id=togglebutton5 popovertarget=innerpopover5>toggle popover</button>
+</div>
+<div id=innerpopover5 popover=auto>popover</div>
+<script>
+test(() => {
+ outerpopover5.showPopover();
+ outerpopover5.querySelector('input').click(); // Invoke innerpopover
+ assert_true(innerpopover5.matches(':popover-open'),
+ 'The inner popover should be able to open successfully.');
+ assert_true(outerpopover5.matches(':popover-open'),
+ 'The outer popover should stay open when opening the inner one.');
+
+ togglebutton5.setAttribute('type', 'text');
+ assert_true(innerpopover5.matches(':popover-open'),
+ 'The inner popover be should be not closed when invoking buttons cease to be invokers.');
+ assert_true(outerpopover5.matches(':popover-open'),
+ 'The outer popover be should be not closed when invoking buttons cease to be invokers.');
+}, 'Changing the input type on a popover*target button when popovers are open should not close anything.');
+</script>
+
+<div id=outerpopover6 popover=auto>
+ <button id=togglebutton6 popovertarget=innerpopover6>toggle popover</button>
+</div>
+<div id=innerpopover6 popover=auto>popover</div>
+<script>
+test(() => {
+ outerpopover6.showPopover();
+ outerpopover6.querySelector('button').click(); // Invoke innerpopover
+ assert_true(innerpopover6.matches(':popover-open'),
+ 'The inner popover should be able to open successfully.');
+ assert_true(outerpopover6.matches(':popover-open'),
+ 'The outer popover should stay open when opening the inner one.');
+
+ togglebutton6.remove();
+ assert_true(innerpopover6.matches(':popover-open'),
+ 'The inner popover be should be not closed when invoking buttons are removed.');
+ assert_true(outerpopover6.matches(':popover-open'),
+ 'The outer popover be should be not closed when invoking buttons are removed.');
+}, 'Disconnecting popover*target buttons when popovers are open should not close anything.');
+</script>
+
+<div id=outerpopover7 popover=auto>
+ <button id=togglebutton7 popovertarget=innerpopover7>toggle popover</button>
+</div>
+<div id=innerpopover7 popover=auto>popover</div>
+<script>
+test(() => {
+ outerpopover7.showPopover();
+ outerpopover7.querySelector('button').click(); // Invoke innerpopover
+ assert_true(innerpopover7.matches(':popover-open'),
+ 'The inner popover should be able to open successfully.');
+ assert_true(outerpopover7.matches(':popover-open'),
+ 'The outer popover should stay open when opening the inner one.');
+
+ togglebutton7.setAttribute('popovertarget', 'otherpopover7');
+ assert_true(innerpopover7.matches(':popover-open'),
+ 'The inner popover be should be not closed when invoking buttons are retargeted.');
+ assert_true(outerpopover7.matches(':popover-open'),
+ 'The outer popover be should be not closed when invoking buttons are retargeted.');
+}, 'Changing the popovertarget attribute to break the chain should not close anything.');
+</script>
+
+<div id=outerpopover8 popover=auto>
+ <div id=middlepopover8 popover=auto>
+ <div id=innerpopover8 popover=auto>hello</div>
+ </div>
+</div>
+<div id=otherpopover8 popover=auto>other popover</div>
+<button id=togglebutton8 popovertarget=middlepopover8>other button</div>
+<script>
+test(() => {
+ outerpopover8.showPopover();
+ middlepopover8.showPopover();
+ innerpopover8.showPopover();
+ assert_true(innerpopover8.matches(':popover-open'),
+ 'The inner popover should be able to open successfully.');
+ assert_true(middlepopover8.matches(':popover-open'),
+ 'The middle popover should stay open when opening the inner one.');
+ assert_true(outerpopover8.matches(':popover-open'),
+ 'The outer popover should stay open when opening the inner one.');
+
+ togglebutton8.setAttribute('popovertarget', 'otherpopover8');
+ assert_true(innerpopover8.matches(':popover-open'),
+ 'The inner popover should remain open.');
+ assert_true(middlepopover8.matches(':popover-open'),
+ 'The middle popover should remain open.');
+ assert_true(outerpopover8.matches(':popover-open'),
+ 'The outer popover should remain open.');
+}, `Modifying popovertarget on a button which doesn't break the chain shouldn't close any popovers.`);
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-combinations.html b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-combinations.html
new file mode 100644
index 0000000000..024794f578
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-combinations.html
@@ -0,0 +1,155 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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="/common/top-layer.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 :popover-open will eventually support <dialog>, this does extra work to
+ // verify we're dealing with an :popover-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(':popover-open'),`${message}: Popover doesn\'t match :popover-open`);
+ ex.hidePopover(); // Shouldn't throw if this is a showing popover
+ ex.showPopover(); // Show it again to avoid state change
+ assert_true(ex.matches(':popover-open'),`${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');
+ ex.showPopover(); // Should not throw
+ ex.removeAttribute('open');
+ ex.hidePopover();
+ 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_false(ex.matches(':popover-open'),'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');
+ ex.show(); // Should not throw
+ ex.close();
+ ex.showPopover();
+ }
+ 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'));
+ ex.showPopover(); // Should not throw
+ ex.close();
+ assert_false(ex.hasAttribute('open'));
+ ex.hidePopover();
+ 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'));
+ ex.hidePopover();
+ } 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);
+ button.popoverTargetElement = ex;
+ button.popoverTargetAction = "toggle";
+ assert_false(ex.matches(':popover-open'));
+ 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(ex);
+ 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_false(ex.matches(':popover-open'),'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.html b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-interactions.html
new file mode 100644
index 0000000000..6d050ed99b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-interactions.html
@@ -0,0 +1,89 @@
+<!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/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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="/common/top-layer.js"></script>
+<script src="resources/popover-utils.js"></script>
+
+<body>
+<script>
+const types = Object.freeze({
+ popover: Symbol("Popover"),
+ 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(':popover-open')},
+ },
+ {
+ type: types.modalDialog,
+ closes: [types.popover],
+ createElement: () => document.createElement('dialog'),
+ trigger: function() {this.element.showModal()},
+ close: function() {this.element.close()},
+ isTopLayer: function() {return this.element.matches(':modal')},
+ },
+ {
+ type: types.fullscreen,
+ closes: [types.popover],
+ createElement: () => document.createElement('div'),
+ trigger: async function(visibleElement) {assert_false(this.isTopLayer());await blessTopLayer(visibleElement);await this.element.requestFullscreen();},
+ close: async function() {await 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;
+}
+async function doneWithExample(ex) {
+ assert_true(!!ex.element);
+ if (ex.isTopLayer())
+ await 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(() => {
+ return Promise.all([
+ 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-top-layer-nesting-anchor.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting-anchor.tentative.html
new file mode 100644
index 0000000000..4520ab0577
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting-anchor.tentative.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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-vendor.js"></script>
+<script src="/common/top-layer.js"></script>
+<script src="resources/popover-utils.js"></script>
+<script src="resources/popover-top-layer-nesting.js"></script>
+
+<div id=tests>
+ <div> Single popover=auto ancestor
+ <div popover=auto class=target data-stay-open=true></div>
+ </div>
+
+ <div> Single popover=manual ancestor
+ <div popover=auto class=target data-stay-open=true></div>
+ </div>
+
+ <div> Nested popover=auto ancestors
+ <div popover=auto data-stay-open=true>
+ <div popover=auto class=target data-stay-open=true></div>
+ </div>
+ </div>
+
+ <div> Nested popover=auto ancestors, target is outer
+ <div popover=auto class=target data-stay-open=true>
+ <div popover=auto data-stay-open=false></div>
+ </div>
+ </div>
+
+ <div> Top layer inside of nested element
+ <div popover=auto data-stay-open=true>
+ <button class=target></button>
+ </div>
+ </div>
+</div>
+
+<script>
+ const tests = Array.from(document.querySelectorAll('#tests>div'));
+ runTopLayerTests(tests,/*testAnchorAttribute*/true);
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting-hints.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting-hints.tentative.html
new file mode 100644
index 0000000000..4ec1f49bda
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting-hints.tentative.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://open-ui.org/components/popover-hint.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="/common/top-layer.js"></script>
+<script src="resources/popover-utils.js"></script>
+<script src="resources/popover-top-layer-nesting.js"></script>
+
+<div id=tests>
+ <div> Single popover=hint ancestor
+ <div popover=hint class=target data-stay-open=true></div>
+ </div>
+
+ <div> Nested auto/hint ancestors
+ <div popover=auto data-stay-open=true>
+ <div popover=hint class=target data-stay-open=true></div>
+ </div>
+ </div>
+
+ <div> Nested auto/hint ancestors, target is auto
+ <div popover=auto class=target data-stay-open=true>
+ <div popover=hint data-stay-open=false></div>
+ </div>
+ </div>
+
+ <div> Unrelated hint, target=hint
+ <div popover=auto data-stay-open=true></div>
+ <div popover=hint class=target data-stay-open=true></div>
+ </div>
+
+ <div> Unrelated hint, target=auto
+ <div popover=auto class=target data-stay-open=true></div>
+ <div popover=hint data-stay-open=false></div>
+ </div>
+</div>
+
+<script>
+ const tests = Array.from(document.querySelectorAll('#tests>div'));
+ runTopLayerTests(tests);
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting.tentative.html
new file mode 100644
index 0000000000..a0b3b60b72
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-top-layer-nesting.tentative.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<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-vendor.js"></script>
+<script src="/common/top-layer.js"></script>
+<script src="resources/popover-utils.js"></script>
+<script src="resources/popover-top-layer-nesting.js"></script>
+
+<div id=tests>
+ <div> Single popover=auto ancestor
+ <div popover=auto class=target data-stay-open=true></div>
+ </div>
+
+ <div> Single popover=manual ancestor
+ <div popover=auto class=target data-stay-open=true></div>
+ </div>
+
+ <div> Nested popover=auto ancestors
+ <div popover=auto data-stay-open=true>
+ <div popover=auto class=target data-stay-open=true></div>
+ </div>
+ </div>
+
+ <div> Nested popover=auto ancestors, target is outer
+ <div popover=auto class=target data-stay-open=true>
+ <div popover=auto data-stay-open=false></div>
+ </div>
+ </div>
+
+ <div> Top layer inside of nested element
+ <div popover=auto data-stay-open=true>
+ <button class=target></button>
+ </div>
+ </div>
+</div>
+
+<script>
+ const tests = Array.from(document.querySelectorAll('#tests>div'));
+ runTopLayerTests(tests);
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-types-with-hints.tentative.html b/testing/web-platform/tests/html/semantics/popovers/popover-types-with-hints.tentative.html
new file mode 100644
index 0000000000..07f0e26fce
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-types-with-hints.tentative.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<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>
+ function getPopovers() {
+ return Array.from(document.currentScript.parentElement.querySelectorAll('[popover]'));
+ }
+ function assertState(expectedState,description) {
+ description = description || 'Error';
+ const popovers = getPopovers();
+ const n = popovers.length;
+ assert_equals(expectedState.length,n,'Invalid expected state length');
+ for(let i=0;i<n;++i) {
+ const html = '<' + popovers[i].outerHTML.split('<')[1] + '</div>';
+ assert_equals(popovers[i].matches(':popover-open'),expectedState[i],`${description}, index ${i} (${html})`);
+ }
+ }
+</script>
+
+<div>
+ <div popover>Popover</div>
+ <div popover=hint>Hint</div>
+ <div popover=manual>Async</div>
+ <div popover=manual>Async</div>
+ <script>
+ {
+ const auto = getPopovers()[0];
+ const hint = getPopovers()[1];
+ const manual = getPopovers()[2];
+ const manual2 = getPopovers()[3];
+ test(() => {
+ assertState([false,false,false,false]);
+ auto.showPopover();
+ assertState([true,false,false,false]);
+ hint.showPopover();
+ assertState([true,true,false,false]);
+ manual.showPopover();
+ assertState([true,true,true,false]);
+ manual2.showPopover();
+ assertState([true,true,true,true]);
+ hint.hidePopover();
+ assertState([true,false,true,true]);
+ auto.hidePopover();
+ assertState([false,false,true,true]);
+ auto.showPopover();
+ hint.showPopover();
+ assertState([true,true,true,true]);
+ auto.hidePopover();
+ assertState([false,false,true,true]);
+ hint.hidePopover();
+ manual.hidePopover();
+ assertState([false,false,false,true]);
+ manual2.hidePopover();
+ assertState([false,false,false,false]);
+ },'manuals do not close popovers');
+
+ test(() => {
+ assertState([false,false,false,false]);
+ hint.showPopover();
+ manual.showPopover();
+ manual2.showPopover();
+ assertState([false,true,true,true]);
+ auto.showPopover();
+ assertState([true,false,true,true]);
+ auto.hidePopover();
+ assertState([false,false,true,true]);
+ manual.hidePopover();
+ manual2.hidePopover();
+ assertState([false,false,false,false]);
+ },'autos close hints but not manuals');
+ }
+ </script>
+</div>
+
+<div>
+ <div popover>popover 1
+ <div popover>popover 2
+ <p id=anchorid>Anchor</p>
+ <div popover>popover 3</div>
+ </div>
+ </div>
+ <div popover=hint anchor=anchorid>Hint anchored to popover</div>
+ <script>
+ {
+ const popover1 = getPopovers()[0];
+ const popover2 = getPopovers()[1];
+ const popover3 = getPopovers()[2];
+ const hint = getPopovers()[3];
+ test(() => {
+ assertState([false,false,false,false]);
+ popover1.showPopover();
+ popover2.showPopover();
+ popover3.showPopover();
+ assertState([true,true,true,false]);
+ hint.showPopover(); // Because hint is nested in popover2, popover3 should be hidden
+ assertState([true,true,false,true]);
+ popover1.hidePopover(); // Should close the hint, which is anchored to popover2
+ assertState([false,false,false,false]);
+ },'hint is not closed by pre-existing auto');
+ }
+ </script>
+</div>
+
+<div>
+ <div popover=hint>Hint
+ <div popover=hint>Nested hint</div>
+ </div>
+ <script>
+ test(() => {
+ const hint1 = getPopovers()[0];
+ const hint2 = getPopovers()[1];
+ hint1.showPopover();
+ assertState([true,false]);
+ hint2.showPopover();
+ assertState([true,true]);
+ hint1.hidePopover();
+ assertState([false,false]);
+ },'You can nest hint popovers');
+ </script>
+</div>
+
+<div>
+ <div popover="hint">Hint
+ <div popover>Nested auto (note - never visible, since inside display:none subtree)</div>
+ </div>
+ <script>
+ test(() => {
+ const hint = getPopovers()[0];
+ const auto = getPopovers()[1];
+ hint.showPopover();
+ assertState([true,false]);
+ auto.showPopover();
+ assertState([false,true]);
+ auto.hidePopover();
+ assertState([false,false]);
+ },'If a popover=auto is shown, it should hide any open popover=hint, including if the popover=hint is an ancestral popover of the popover=auto. (You can\'t nest a popover=auto inside a popover=hint)');
+ </script>
+</div>
+
+<div>
+ <div popover>Auto
+ <div popover>Nested Auto</div>
+ <div popover=hint>Nested hint</div>
+ </div>
+ <script>
+ test(() => {
+ const auto = getPopovers()[0];
+ const auto2 = getPopovers()[1];
+ const hint = getPopovers()[2];
+ auto.showPopover();
+ auto2.showPopover();
+ assertState([true,true,false]);
+ hint.showPopover(); // This should hide auto2, since it is nested in auto1.
+ assertState([true,false,true]);
+ auto.hidePopover(); // Should hide both auto and hint.
+ assertState([false,false,false]);
+ },'If you: a) show a popover=auto (call it D), then b) show a descendent popover=hint of D (call it T), then c) hide D, then T should be hidden. (A popover=hint can be nested inside a popover=auto)');
+ </script>
+</div>
+
+<div>
+ <div popover>Auto</div>
+ <div popover=hint>Non-Nested hint</div>
+ <script>
+ test(() => {
+ const auto = getPopovers()[0]
+ const hint = getPopovers()[1];
+ auto.showPopover();
+ hint.showPopover();
+ assertState([true,true]);
+ auto.hidePopover();
+ assertState([false,false]);
+ },'If you: a) show a popover=auto (call it D), then b) show a non-descendent popover=hint of D (call it T), then c) hide D, then T should be hidden. (Non-nested popover=hint gets hidden when unrelated popover=autos are hidden)');
+ </script>
+</div>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-types.html b/testing/web-platform/tests/html/semantics/popovers/popover-types.html
new file mode 100644
index 0000000000..d4ad81e52b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-types.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<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>
+
+<div id="container">
+ <div popover>Popover</div>
+ <div popover=manual>Async</div>
+ <div popover=manual>Async</div>
+</div>
+<script>
+ const auto = container.querySelector('[popover=""]');
+ const manual = container.querySelectorAll('[popover=manual]')[0];
+ const manual2 = container.querySelectorAll('[popover=manual]')[1];
+ function assert_state_1(autoOpen,manualOpen,manual2Open) {
+ assert_equals(auto.matches(':popover-open'),autoOpen,'auto open state is incorrect');
+ assert_equals(manual.matches(':popover-open'),manualOpen,'manual open state is incorrect');
+ assert_equals(manual2.matches(':popover-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>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-undefined-remove-crash.html b/testing/web-platform/tests/html/semantics/popovers/popover-undefined-remove-crash.html
new file mode 100644
index 0000000000..3c273ea6f3
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popover-undefined-remove-crash.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/issues/9459#issuecomment-1630466911">
+
+<div id="po" popover>
+PO
+</div>
+
+<script>
+po.popover = undefined;
+po.remove();
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/popovertarget-reflection.html b/testing/web-platform/tests/html/semantics/popovers/popovertarget-reflection.html
new file mode 100644
index 0000000000..d0750fdd4c
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/popovertarget-reflection.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://bugs.chromium.org/p/chromium/issues/detail?id=1523410">
+<link rel=help href="https://bugzilla.mozilla.org/show_bug.cgi?id=1879001">
+<link rel=help href="https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<button id=mybutton popovertarget="mypopover">toggle popover</button>
+<div id=mypopover popover=auto>popover</div>
+
+<script>
+test(() => {
+ assert_equals(mybutton.popoverTargetElement.id, "mypopover",
+ 'Setting element.popoverTargetElement to a valid element should work');
+
+ mybutton.popoverTargetElement = null;
+ assert_false(mybutton.hasAttribute('popovertarget'),
+ 'Setting element.popoverTargetElement to null should unset popovertarget attribute.');
+ assert_equals(mybutton.popoverTargetElement, null,
+ 'Setting element.popoverTargetElement to null should remove the existing element from element.popoverTargetElement.');
+
+ mybutton.popoverTargetElement = mypopover;
+ assert_true(mybutton.hasAttribute('popovertarget'),
+ 'Assigning to element.popoverTargetElement should set the popovertarget attribute.');
+
+ mybutton.removeAttribute('popovertarget');
+ assert_equals(mybutton.popoverTargetElement, null,
+ 'Removing the popovertarget attribute should remove the element from element.popoverTargetElement.');
+
+ mybutton.popoverTargetElement = mypopover;
+ assert_true(mybutton.hasAttribute('popovertarget'),
+ 'Assigning to element.popoverTargetElement should set the popovertarget attribute.');
+
+ mybutton.setAttribute("popovertarget", 'invalid');
+ assert_equals(mybutton.popoverTargetElement, null,
+ 'Setting the popovertarget attribute to a localName that is not attr should remove the existing element from element.popoverTargetElement.');
+
+ mybutton.popoverTargetElement = mypopover;
+ mybutton.setAttribute("popovertarget", "");
+ assert_equals(mybutton.popoverTargetElement.id, "mypopover",
+ 'Setting the popovertarget attribute to empty string right after explicitly setting attribute element should have no effect.');
+
+ mybutton.setAttribute("popovertarget", "mypopover");
+ assert_equals(mybutton.popoverTargetElement.id, "mypopover",
+ 'Setting the popovertarget attribute to a value should set the popover target element.');
+ mybutton.setAttribute("popovertarget", "");
+ assert_equals(mybutton.getAttribute('popovertarget'), "",
+ 'Assigning to element.popoverTargetElement to empty string should update the attribute value to empty string.');
+ assert_equals(mybutton.popoverTargetElement, null,
+ 'Setting the popovertarget attribute to empty string should remove the existing element from element.popoverTargetElement.');
+}, 'Element attribute reflection of popoverTargetElement/popovertarget should be kept in sync.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/resources/popover-hover-hide-common.js b/testing/web-platform/tests/html/semantics/popovers/resources/popover-hover-hide-common.js
new file mode 100644
index 0000000000..9f407ef157
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/resources/popover-hover-hide-common.js
@@ -0,0 +1,139 @@
+// NOTE about testing methodology:
+// This test checks whether popovers are hidden *after* the appropriate de-hover
+// delay. The delay used for testing is kept low, to avoid this test taking too
+// long, but that means that sometimes on a slow bot/client, the delay can
+// elapse before we are able to check the popover status. And that can make this
+// test flaky. To avoid that, the msSinceMouseOver() function is used to check
+// that not-too-much time has passed, and if it has, the test is simply skipped.
+
+const hoverDelays = 100; // This needs to match the style block below.
+const hoverWaitTime = 200; // How long to wait to cover the delay for sure.
+
+async function initialPopoverShow(invoker) {
+ const popover = invoker.popoverTargetElement;
+ assert_false(popover.matches(':popover-open'));
+ await mouseOver(invoker); // Always start with the mouse over the invoker
+ popover.showPopover();
+ assert_true(popover.matches(':popover-open'));
+}
+
+function runHoverHideTest(popoverType, invokerType, invokerAction) {
+ const descr = `popover=${popoverType}, invoker=${invokerType}, popovertargetaction=${invokerAction}`;
+ promise_test(async (t) => {
+ const {popover,invoker} = makeTestParts(t, popoverType, invokerType, invokerAction);
+ await initialPopoverShow(invoker);
+ await mouseOver(unrelated);
+ let showing = popover.matches(':popover-open');
+ if (msSinceMouseOver() >= hoverDelays)
+ return; // The WPT runner was too slow.
+ assert_true(showing,'popover shouldn\'t immediately hide');
+ await mouseHover(unrelated,hoverWaitTime);
+ assert_false(popover.matches(':popover-open'),'popover should hide after delay');
+ },`The popover-hide-delay causes a popover to be hidden after a delay, ${descr}`);
+
+ promise_test(async (t) => {
+ const {popover,invoker} = makeTestParts(t, popoverType, invokerType, invokerAction);
+ await initialPopoverShow(invoker);
+ await mouseHover(popover,hoverWaitTime);
+ assert_true(popover.matches(':popover-open'),'hovering the popover should keep it showing');
+ await mouseOver(unrelated);
+ let showing = popover.matches(':popover-open');
+ if (msSinceMouseOver() >= hoverDelays)
+ return; // The WPT runner was too slow.
+ assert_true(showing,'subsequently hovering unrelated element shouldn\'t immediately hide the popover');
+ await mouseHover(unrelated,hoverWaitTime);
+ assert_false(popover.matches(':popover-open'),'hovering unrelated element should hide popover after delay');
+ },`hovering the popover keeps it from being hidden, ${descr}`);
+
+ promise_test(async (t) => {
+ const {popover,invoker,mouseOverInvoker} = makeTestParts(t, popoverType, invokerType, invokerAction);
+ await initialPopoverShow(invoker);
+ assert_true(popover.matches(':popover-open'));
+ await mouseHover(popover,hoverWaitTime);
+ await mouseHover(mouseOverInvoker,hoverWaitTime);
+ assert_true(popover.matches(':popover-open'),'Moving hover between invoker and popover should keep popover from being hidden');
+ await mouseHover(unrelated,hoverWaitTime);
+ assert_false(popover.matches(':popover-open'),'Moving hover to unrelated should finally hide the popover');
+ },`hovering an invoking element keeps the popover from being hidden, ${descr}`);
+}
+
+function runHoverHideTestsForInvokerAction(invokerAction) {
+ promise_test(async (t) => {
+ const {popover,invoker} = makeTestParts(t, 'auto', 'button', 'show');
+ assert_false(popover.matches(':popover-open'));
+ assert_true(invoker.matches('[popovertarget]'),'invoker needs to match [popovertarget]');
+ assert_equals(invoker.popoverTargetElement,popover,'invoker should point to popover');
+ await mouseHover(invoker,hoverWaitTime);
+ assert_true(msSinceMouseOver() >= hoverWaitTime,'waitForHoverTime should wait the specified time');
+ assert_true(hoverWaitTime > hoverDelays,'hoverDelays is the value from CSS, hoverWaitTime should be longer than that');
+ assert_equals(getComputedStyleTimeMs(invoker,'popoverShowDelay'),hoverDelays,'popover-show-delay is incorrect');
+ assert_equals(getComputedStyleTimeMs(popover,'popoverHideDelay'),hoverDelays,'popover-hide-delay is incorrect');
+ },'Test the harness');
+
+ // Run for all invoker and popover types.
+ ["button","input"].forEach(invokerType => {
+ ["auto","hint","manual"].forEach(popoverType => {
+ runHoverHideTest(popoverType, invokerType, invokerAction);
+ });
+ });
+}
+
+// Setup stuff
+const unrelated = document.createElement('div');
+unrelated.id = 'unrelated';
+unrelated.textContent = 'Unrelated element';
+const style = document.createElement('style');
+document.body.append(unrelated,style);
+style.textContent = `
+ div, button, input {
+ /* Fixed position everything to ensure nothing overlaps */
+ position: fixed;
+ max-height: 100px;
+ }
+ #unrelated {top: 100px;}
+ [popovertarget] {
+ top:200px;
+ popover-show-delay: 100ms;
+ }
+ [popover] {
+ width: 200px;
+ height: 100px;
+ top:300px;
+ popover-hide-delay: 100ms;
+ }
+`;
+
+function makeTestParts(t,popoverType,invokerType,invokerAction) {
+ const popover = document.createElement('div');
+ popover.id = `popover-${popoverType}-${invokerType}-${invokerAction}`;
+ document.body.appendChild(popover);
+ popover.popover = popoverType;
+ assert_equals(popover.popover, popoverType, `Type ${popoverType} not supported`);
+ const invoker = document.createElement(invokerType);
+ document.body.appendChild(invoker);
+ invoker.popoverTargetElement = popover;
+ invoker.popoverTargetAction = invokerAction;
+ assert_equals(invoker.popoverTargetAction, invokerAction, `Invoker action ${invokerAction} not supported`);
+ let mouseOverInvoker;
+ switch (invokerType) {
+ case 'button':
+ invoker.innerHTML = '<span><span data-note=nested_element>Click me</span></span>';
+ mouseOverInvoker = invoker.firstElementChild.firstElementChild;
+ assert_true(!!mouseOverInvoker);
+ break;
+ case 'input':
+ invoker.type = 'button';
+ mouseOverInvoker = invoker;
+ break;
+ default:
+ assert_unreached('Invalid invokerType ' + invokerType);
+ break;
+ }
+ t.add_cleanup(() => {popover.remove(); invoker.remove();});
+ return {popover, invoker, mouseOverInvoker};
+}
+
+function getComputedStyleTimeMs(element,property) {
+ // Times are in seconds, so just strip off the 's'.
+ return Number(getComputedStyle(element)[property].slice(0,-1))*1000;
+}
diff --git a/testing/web-platform/tests/html/semantics/popovers/resources/popover-invoking-attribute.js b/testing/web-platform/tests/html/semantics/popovers/resources/popover-invoking-attribute.js
new file mode 100644
index 0000000000..d2911647e1
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/resources/popover-invoking-attribute.js
@@ -0,0 +1,122 @@
+const actionReflectionLogic = (action) => {
+ switch (action?.toLowerCase()) {
+ case "show": return "show";
+ case "hide": return "hide";
+ default: return "toggle";
+ }
+}
+const noActivationLogic = (action) => {
+ return "none";
+}
+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: actionReflectionLogic,
+ };
+});
+const supportedInputButtonTypes = ['button','reset','submit','image'].map(type => {
+ return {
+ name: `<input type="${type}">`,
+ makeElement: makeElementWithType('input',type),
+ invokeFn: el => {el.focus(); el.click()},
+ getExpectedLogic: actionReflectionLogic,
+ };
+});
+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
+ };
+});
+const invokers = [
+ ...supportedButtonTypes,
+ ...supportedInputButtonTypes,
+ ...unsupportedTypes,
+];
+function runPopoverInvokerTests(popoverTypes) {
+ window.addEventListener('load', () => {
+ popoverTypes.forEach(type => {
+ invokers.forEach(testcase => {
+ ["toggle","hide","show","ShOw","garbage",null,undefined].forEach(action => {
+ [false,true].forEach(use_idl_for_target => {
+ [false,true].forEach(use_idl_for_action => {
+ promise_test(async test => {
+ const popover = Object.assign(document.createElement('div'),{popover: type, id: 'my-popover'});
+ assert_equals(popover.popover,type,'reflection');
+ const invoker = testcase.makeElement(test);
+ if (use_idl_for_target) {
+ invoker.popoverTargetElement = popover;
+ assert_equals(invoker.getAttribute('popovertarget'),'','attribute value');
+ } else {
+ invoker.setAttribute('popovertarget',popover.id);
+ }
+ if (use_idl_for_action) {
+ invoker.popoverTargetAction = action;
+ assert_equals(invoker.getAttribute('popovertargetaction'),String(action),'action reflection');
+ } else {
+ invoker.setAttribute('popovertargetaction',action);
+ }
+ assert_true(!document.getElementById(popover.id));
+ assert_equals(invoker.popoverTargetElement,null,'targetElement should be null before the popover is in the document');
+ assert_equals(invoker.popoverTargetAction,actionReflectionLogic(action),'action should be correct immediately');
+ document.body.appendChild(popover);
+ test.add_cleanup(() => {popover.remove();});
+ assert_equals(invoker.popoverTargetElement,popover,'target element should be returned once it\'s in the document');
+ assert_false(popover.matches(':popover-open'));
+ await testcase.invokeFn(invoker);
+ assert_equals(document.activeElement,invoker,'Focus should end up on the invoker');
+ expectedBehavior = testcase.getExpectedLogic(action);
+ switch (expectedBehavior) {
+ case "toggle":
+ case "show":
+ assert_true(popover.matches(':popover-open'),'Toggle or show should show the popover');
+ popover.hidePopover(); // Hide the popover
+ break;
+ case "hide":
+ case "none":
+ assert_false(popover.matches(':popover-open'),'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;
+ }
+ assert_false(popover.matches(':popover-open'));
+ popover.showPopover(); // Show the popover directly
+ assert_equals(document.activeElement,invoker,'The popover should not shift focus');
+ assert_true(popover.matches(':popover-open'));
+ await testcase.invokeFn(invoker);
+ switch (expectedBehavior) {
+ case "toggle":
+ case "hide":
+ assert_false(popover.matches(':popover-open'),'Toggle or hide should hide the popover');
+ break;
+ case "show":
+ assert_true(popover.matches(':popover-open'),'Show should leave the popover showing');
+ break;
+ default:
+ assert_unreached();
+ }
+ },`Test ${testcase.name}, action=${action}, ${use_idl_for_target ? "popoverTarget IDL" : "popovertarget attr"}, ${use_idl_for_action ? "popoverTargetAction IDL" : "popovertargetaction attr"}, with popover=${type}`);
+ });
+ });
+ });
+ });
+ });
+ });
+}
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-top-layer-nesting.js b/testing/web-platform/tests/html/semantics/popovers/resources/popover-top-layer-nesting.js
new file mode 100644
index 0000000000..ace10b3f7b
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/resources/popover-top-layer-nesting.js
@@ -0,0 +1,108 @@
+function createTopLayerElement(t,topLayerType) {
+ let element, show, showing;
+ switch (topLayerType) {
+ case 'dialog':
+ element = document.createElement('dialog');
+ show = () => element.showModal();
+ showing = () => element.matches(':modal');
+ break;
+ case 'fullscreen':
+ element = document.createElement('div');
+ show = async (topmostElement) => {
+ // Be sure to add user activation to the topmost visible target:
+ await blessTopLayer(topmostElement);
+ await element.requestFullscreen();
+ };
+ showing = () => document.fullscreenElement === element;
+ break;
+ default:
+ assert_unreached('Invalid top layer type');
+ }
+ t.add_cleanup(() => element.remove());
+ return {element,show,showing};
+}
+function runTopLayerTests(testCases, testAnchorAttribute) {
+ testAnchorAttribute = testAnchorAttribute || false;
+ testCases.forEach(test => {
+ const description = test.firstChild.data.trim();
+ assert_equals(test.querySelectorAll('.target').length,1,'There should be exactly one target');
+ const target = test.querySelector('.target');
+ assert_true(!!target,'Invalid test case');
+ const popovers = Array.from(test.querySelectorAll('[popover]'));
+ assert_true(popovers.length > 0,'No popovers found');
+ ['dialog','fullscreen'].forEach(topLayerType => {
+ promise_test(async t => {
+ const {element,show,showing} = createTopLayerElement(t,topLayerType);
+ target.appendChild(element);
+
+ // Show the popovers.
+ t.add_cleanup(() => popovers.forEach(popover => popover.hidePopover()));
+ popovers.forEach(popover => popover.showPopover());
+ popovers.forEach(popover => assert_true(popover.matches(':popover-open'),'All popovers should be open'));
+
+ // Activate the top layer element.
+ await show(popovers[popovers.length-1]);
+ assert_true(showing());
+ popovers.forEach(popover => assert_equals(popover.matches(':popover-open'),popover.dataset.stayOpen==='true','Incorrect behavior'));
+
+ // Add another popover within the top layer element and make sure entire stack stays open.
+ const newPopover = document.createElement('div');
+ t.add_cleanup(() => newPopover.remove());
+ newPopover.popover = popoverHintSupported() ? 'hint' : 'auto';
+ element.appendChild(newPopover);
+ popovers.forEach(popover => assert_equals(popover.matches(':popover-open'),popover.dataset.stayOpen==='true','Adding another popover shouldn\'t change anything'));
+ assert_true(showing(),'top layer element should still be top layer');
+ newPopover.showPopover();
+ assert_true(newPopover.matches(':popover-open'));
+ popovers.forEach(popover => assert_equals(popover.matches(':popover-open'),popover.dataset.stayOpen==='true','Showing the popover shouldn\'t change anything'));
+ assert_true(showing(),'top layer element should still be top layer');
+ },`${description} with ${topLayerType}`);
+
+ promise_test(async t => {
+ const {element,show,showing} = createTopLayerElement(t,topLayerType);
+ element.popover = popoverHintSupported() ? 'hint' : 'auto';
+ target.appendChild(element);
+
+ // Show the popovers.
+ t.add_cleanup(() => popovers.forEach(popover => popover.hidePopover()));
+ popovers.forEach(popover => popover.showPopover());
+ popovers.forEach(popover => assert_true(popover.matches(':popover-open'),'All popovers should be open'));
+ const targetWasOpenPopover = target.matches(':popover-open');
+
+ // Show the top layer element as a popover first.
+ element.showPopover();
+ assert_true(element.matches(':popover-open'),'element should be open as a popover');
+ assert_equals(target.matches(':popover-open'),targetWasOpenPopover,'target shouldn\'t change popover state');
+
+ try {
+ await show(element);
+ assert_unreached('It is an error to activate a top layer element that is already a showing popover');
+ } catch (e) {
+ // We expect an InvalidStateError for dialogs, and a TypeError for fullscreens.
+ // Anything else should fall through to the test harness.
+ if (e.name !== 'InvalidStateError' && e.name !== 'TypeError') {
+ throw e;
+ }
+ }
+ },`${description} with ${topLayerType}, top layer element *is* a popover`);
+
+ if (testAnchorAttribute) {
+ promise_test(async t => {
+ const {element,show,showing} = createTopLayerElement(t,topLayerType);
+ element.anchorElement = target;
+ document.body.appendChild(element);
+
+ // Show the popovers.
+ t.add_cleanup(() => popovers.forEach(popover => popover.hidePopover()));
+ popovers.forEach(popover => popover.showPopover());
+ popovers.forEach(popover => assert_true(popover.matches(':popover-open'),'All popovers should be open'));
+
+ // Activate the top layer element.
+ await show(popovers[popovers.length-1]);
+ assert_true(showing());
+ popovers.forEach(popover => assert_equals(popover.matches(':popover-open'),popover.dataset.stayOpen==='true','Incorrect behavior'));
+ },`${description} with ${topLayerType}, anchor attribute`);
+ }
+ });
+ });
+}
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..bfc1f89ec1
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/resources/popover-utils.js
@@ -0,0 +1,176 @@
+function waitForRender() {
+ return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
+}
+
+function waitForTick() {
+ return new Promise(resolve => step_timeout(resolve, 0));
+}
+
+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.activeElement || document.documentElement, kTab);
+ await waitForRender();
+}
+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.activeElement || document.documentElement,'\uE00C'); // Escape
+ await waitForRender();
+}
+async function sendEnter() {
+ await waitForRender();
+ await new test_driver.send_keys(document.activeElement || document.documentElement,'\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 mousemoveInfo;
+function mouseOver(element) {
+ mousemoveInfo?.controller?.abort();
+ const controller = new AbortController();
+ mousemoveInfo = {element, controller, moved: false, started: performance.now()};
+ return (new test_driver.Actions())
+ .pointerMove(0, 0, {origin: element})
+ .send()
+ .then(() => {
+ document.addEventListener("mousemove", (e) => {mousemoveInfo.moved = true;}, {signal: controller.signal});
+ })
+}
+function msSinceMouseOver() {
+ return performance.now() - mousemoveInfo.started;
+}
+function assertMouseStillOver(element) {
+ assert_equals(mousemoveInfo.element, element, 'Broken test harness');
+ assert_false(mousemoveInfo.moved,'Broken test harness');
+}
+async function waitForHoverTime(hoverWaitTimeMs) {
+ await new Promise(resolve => step_timeout(resolve,hoverWaitTimeMs));
+ await waitForRender();
+};
+async function mouseHover(element,hoverWaitTimeMs) {
+ await mouseOver(element);
+ await waitForHoverTime(hoverWaitTimeMs);
+ assertMouseStillOver(element);
+}
+
+// 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]: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});
+ }
+}
+function popoverHintSupported() {
+ // TODO(crbug.com/1416284): This function should be removed, and
+ // any calls replaced with `true`, once popover=hint ships.
+ const testElement = document.createElement('div');
+ testElement.popover = 'hint';
+ return testElement.popover === 'hint';
+}
+
+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(':popover-open'),isPopover,`${message}: Visible popovers should match :popover-open`);
+ } else {
+ assert_equals(window.getComputedStyle(popover).display,'none',`${message}: Non-showing popovers should have display:none`);
+ assert_false(popover.matches(':popover-open'),`${message}: Non-showing popovers should *not* match :popover-open`);
+ }
+}
+
+function assertIsFunctionalPopover(popover, checkVisibility) {
+ assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'A popover should start out hidden');
+ popover.showPopover();
+ if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After showPopover(), a popover should be visible');
+ popover.showPopover(); // Calling showPopover on a showing popover should not throw.
+ popover.hidePopover();
+ if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After hidePopover(), a popover should be hidden');
+ popover.hidePopover(); // Calling hidePopover on a hidden popover should not throw.
+ popover.togglePopover();
+ if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover() on hidden popover, it should be visible');
+ popover.togglePopover();
+ if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover() on visible popover, it should be hidden');
+ popover.togglePopover(/*force=*/true);
+ if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover(true) on hidden popover, it should be visible');
+ popover.togglePopover(/*force=*/true);
+ if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover(true) on visible popover, it should be visible');
+ popover.togglePopover(/*force=*/false);
+ if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover(false) on visible popover, it should be hidden');
+ popover.togglePopover(/*force=*/false);
+ if (checkVisibility) 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');
+ popover.hidePopover(); // Calling hidePopover on a disconnected popover should not throw.
+ 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 NotSupported');
+ 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 NotSupported');
+ 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 NotSupported');
+ assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling togglePopover on a non-popover should leave it visible');
+}
diff --git a/testing/web-platform/tests/html/semantics/popovers/togglePopover.html b/testing/web-platform/tests/html/semantics/popovers/togglePopover.html
new file mode 100644
index 0000000000..388617fccc
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/togglePopover.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/issues/9043">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id=popover popover=auto>popover</div>
+<div id=popover2 popover=auto>popover</div>
+
+<script>
+test(() => {
+ assert_false(popover.matches(':popover-open'),
+ 'Popover should be closed when the test starts.');
+
+ assert_true(popover.togglePopover(),
+ 'togglePopover() should return true.');
+ assert_true(popover.matches(':popover-open'),
+ 'togglePopover() should open the popover.');
+
+ assert_true(popover.togglePopover(/*force=*/true),
+ 'togglePopover(true) should return true.');
+ assert_true(popover.matches(':popover-open'),
+ 'togglePopover(true) should open the popover.');
+
+ assert_false(popover.togglePopover(),
+ 'togglePopover() should return false.');
+ assert_false(popover.matches(':popover-open'),
+ 'togglePopover() should close the popover.');
+
+ assert_false(popover.togglePopover(/*force=*/false),
+ 'togglePopover(false) should return false.');
+ assert_false(popover.matches(':popover-open'),
+ 'togglePopover(false) should close the popover.');
+}, 'togglePopover should toggle the popover and return true or false as specified.');
+
+test(() => {
+ const popover = document.getElementById('popover2');
+ popover.addEventListener('beforetoggle', event => event.preventDefault(), {once: true});
+ assert_false(popover.togglePopover(/*force=*/true),
+ 'togglePopover(true) should return false when the popover does not get opened.');
+ assert_false(popover.matches(':popover-open'));
+
+ // We could also add a test for the return value of togglePopover(false),
+ // but every way to prevent that from hiding the popover also throws an
+ // exception, so the return value is not testable.
+}, `togglePopover's return value should reflect what the end state is, not just the force parameter.`);
+
+test(() => {
+ const popover = document.createElement('div');
+ document.body.appendChild(popover);
+
+ assert_throws_dom('NotSupportedError', () => popover.togglePopover(),
+ 'togglePopover() should throw an exception when the element has no popover attribute.');
+ assert_throws_dom('NotSupportedError', () => popover.togglePopover(true),
+ 'togglePopover(true) should throw an exception when the element has no popover attribute.');
+ assert_throws_dom('NotSupportedError', () => popover.togglePopover(false),
+ 'togglePopover(false) should throw an exception when the element has no popover attribute.');
+
+ popover.setAttribute('popover', 'auto');
+ popover.remove();
+
+ assert_throws_dom('InvalidStateError', () => popover.togglePopover(),
+ 'togglePopover() should throw an exception when the element is disconnected.');
+ assert_throws_dom('InvalidStateError', () => popover.togglePopover(true),
+ 'togglePopover(true) should throw an exception when the element is disconnected.');
+ assert_throws_dom('InvalidStateError', () => popover.togglePopover(false),
+ 'togglePopover(false) should throw an exception when the element is disconnected.');
+
+ document.body.appendChild(popover);
+ // togglePopover(false) should not throw just because the popover is already hidden.
+ popover.togglePopover(false);
+ popover.showPopover();
+ // togglePopover(true) should not throw just because the popover is already showing.
+ popover.togglePopover(true);
+
+ // cleanup
+ popover.hidePopover();
+}, 'togglePopover should throw an exception when there is no popover attribute.');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/popovers/toggleevent-interface.html b/testing/web-platform/tests/html/semantics/popovers/toggleevent-interface.html
new file mode 100644
index 0000000000..09ce3f3b56
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/popovers/toggleevent-interface.html
@@ -0,0 +1,208 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel=help href="https://open-ui.org/components/popover.research.explainer">
+<link rel=help href="https://html.spec.whatwg.org/multipage/popover.html">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+test(function() {
+ var event = new ToggleEvent("");
+ assert_true(event instanceof window.ToggleEvent);
+}, "the event is an instance of ToggleEvent");
+
+test(function() {
+ var event = new ToggleEvent("");
+ assert_true(event instanceof window.Event);
+}, "the event inherts from Event");
+
+test(function() {
+ assert_throws_js(TypeError, function() {
+ new ToggleEvent();
+ }, 'First argument (type) is required, so was expecting a TypeError.');
+}, 'Missing type argument');
+
+test(function() {
+ var event = new ToggleEvent("test");
+ assert_equals(event.type, "test");
+}, "type argument is string");
+
+test(function() {
+ var event = new ToggleEvent(null);
+ assert_equals(event.type, "null");
+}, "type argument is null");
+
+test(function() {
+ var event = new ToggleEvent(undefined);
+ assert_equals(event.type, "undefined");
+}, "event type set to undefined");
+
+test(function() {
+ var event = new ToggleEvent("test");
+ assert_equals(event.oldState, "");
+}, "oldState has default value of empty string");
+
+test(function() {
+ var event = new ToggleEvent("test");
+ assert_readonly(event, "oldState", "readonly attribute value");
+}, "oldState is readonly");
+
+test(function() {
+ var event = new ToggleEvent("test");
+ assert_equals(event.newState, "");
+}, "newState has default value of empty string");
+
+test(function() {
+ var event = new ToggleEvent("test");
+ assert_readonly(event, "newState", "readonly attribute value");
+}, "newState is readonly");
+
+test(function() {
+ var event = new ToggleEvent("test", null);
+ assert_equals(event.oldState, "");
+ assert_equals(event.newState, "");
+}, "ToggleEventInit argument is null");
+
+test(function() {
+ var event = new ToggleEvent("test", undefined);
+ assert_equals(event.oldState, "");
+ assert_equals(event.newState, "");
+}, "ToggleEventInit argument is undefined");
+
+test(function() {
+ var event = new ToggleEvent("test", {});
+ assert_equals(event.oldState, "");
+ assert_equals(event.newState, "");
+}, "ToggleEventInit argument is empty dictionary");
+
+test(function() {
+ var event = new ToggleEvent("test", {oldState: "sample"});
+ assert_equals(event.oldState, "sample");
+}, "oldState set to 'sample'");
+
+test(function() {
+ var event = new ToggleEvent("test", {oldState: undefined});
+ assert_equals(event.oldState, "");
+}, "oldState set to undefined");
+
+test(function() {
+ var event = new ToggleEvent("test", {oldState: null});
+ assert_equals(event.oldState, "null");
+}, "oldState set to null");
+
+test(function() {
+ var event = new ToggleEvent("test", {oldState: false});
+ assert_equals(event.oldState, "false");
+}, "oldState set to false");
+
+test(function() {
+ var event = new ToggleEvent("test", {oldState: true});
+ assert_equals(event.oldState, "true");
+}, "oldState set to true");
+
+test(function() {
+ var event = new ToggleEvent("test", {oldState: 0.5});
+ assert_equals(event.oldState, "0.5");
+}, "oldState set to a number");
+
+test(function() {
+ var event = new ToggleEvent("test", {oldState: []});
+ assert_equals(event.oldState, "");
+}, "oldState set to []");
+
+test(function() {
+ var event = new ToggleEvent("test", {oldState: [1, 2, 3]});
+ assert_equals(event.oldState, "1,2,3");
+}, "oldState set to [1, 2, 3]");
+
+test(function() {
+ var event = new ToggleEvent("test", {oldState: {sample: 0.5}});
+ assert_equals(event.oldState, "[object Object]");
+}, "oldState set to an object");
+
+test(function() {
+ var event = new ToggleEvent("test",
+ {oldState: {valueOf: function () { return 'sample'; }}});
+ assert_equals(event.oldState, "[object Object]");
+}, "oldState set to an object with a valueOf function");
+
+test(function() {
+ var eventInit = {oldState: "sample",newState: "sample2"};
+ var event = new ToggleEvent("test", eventInit);
+ assert_equals(event.oldState, "sample");
+ assert_equals(event.newState, "sample2");
+}, "ToggleEventInit properties set value");
+
+test(function() {
+ var eventInit = {oldState: "open",newState: "closed"};
+ var event = new ToggleEvent("beforetoggle", eventInit);
+ assert_equals(event.oldState, "open");
+ assert_equals(event.newState, "closed");
+}, "ToggleEventInit properties set value 2");
+
+test(function() {
+ var eventInit = {oldState: "closed",newState: "open"};
+ var event = new ToggleEvent("toggle", eventInit);
+ assert_equals(event.oldState, "closed");
+ assert_equals(event.newState, "open");
+}, "ToggleEventInit properties set value 3");
+
+test(function() {
+ var eventInit = {oldState: "open",newState: "open"};
+ var event = new ToggleEvent("beforetoggle", eventInit);
+ assert_equals(event.oldState, "open");
+ assert_equals(event.newState, "open");
+}, "ToggleEventInit properties set value 4");
+
+test(function() {
+ var event = new ToggleEvent("test", {newState: "sample"});
+ assert_equals(event.newState, "sample");
+}, "newState set to 'sample'");
+
+test(function() {
+ var event = new ToggleEvent("test", {newState: undefined});
+ assert_equals(event.newState, "");
+}, "newState set to undefined");
+
+test(function() {
+ var event = new ToggleEvent("test", {newState: null});
+ assert_equals(event.newState, "null");
+}, "newState set to null");
+
+test(function() {
+ var event = new ToggleEvent("test", {newState: false});
+ assert_equals(event.newState, "false");
+}, "newState set to false");
+
+test(function() {
+ var event = new ToggleEvent("test", {newState: true});
+ assert_equals(event.newState, "true");
+}, "newState set to true");
+
+test(function() {
+ var event = new ToggleEvent("test", {newState: 0.5});
+ assert_equals(event.newState, "0.5");
+}, "newState set to a number");
+
+test(function() {
+ var event = new ToggleEvent("test", {newState: []});
+ assert_equals(event.newState, "");
+}, "newState set to []");
+
+test(function() {
+ var event = new ToggleEvent("test", {newState: [1, 2, 3]});
+ assert_equals(event.newState, "1,2,3");
+}, "newState set to [1, 2, 3]");
+
+test(function() {
+ var event = new ToggleEvent("test", {newState: {sample: 0.5}});
+ assert_equals(event.newState, "[object Object]");
+}, "newState set to an object");
+
+test(function() {
+ var event = new ToggleEvent("test",
+ {newState: {valueOf: function () { return 'sample'; }}});
+ assert_equals(event.newState, "[object Object]");
+}, "newState set to an object with a valueOf function");
+</script>