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