diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/html/semantics/popovers | |
parent | Initial commit. (diff) | |
download | firefox-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')
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> |