Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

<!DOCTYPE html>
<meta charset="utf-8" />
<title>Popover light dismiss behavior with command/commandfor</title>
<meta name="timeout" content="long">
<link rel="author" href="mailto:masonf@chromium.org">
<link rel="author" href="mailto:lwarlow@igalia.com">
<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>
<style>
[popover] {
/* Position most popovers at the bottom-right, out of the way */
inset:auto;
bottom:0;
right:0;
}
[popover]::backdrop {
/* This should *not* affect anything: */
pointer-events: auto;
}
</style>
<button id=b1t commandfor='p1' command="toggle-popover">Popover 1</button>
<button id=b1s commandfor='p1' command="show-popover">Popover 1</button>
<span id=outside>Outside all popovers</span>
<div popover id=p1>
<span id=inside1>Inside popover 1</span>
<button id=b2 commandfor='p2' command="show-popover">Popover 2</button>
<span id=inside1after>Inside popover 1 after button</span>
<div popover id=p2>
<span id=inside2>Inside popover 2</span>
</div>
</div>
<button id=after_p1 tabindex="0">Next control after popover1</button>
<style>
#p1 {top: 50px;}
#p2 {top: 120px;}
</style>
<script>
const popover1 = document.querySelector('#p1');
const button1toggle = document.querySelector('#b1t');
const button1show = document.querySelector('#b1s');
const inside1After = document.querySelector('#inside1after');
const button2 = document.querySelector('#b2');
const popover2 = document.querySelector('#p2');
const outside = document.querySelector('#outside');
const inside1 = document.querySelector('#inside1');
const inside2 = document.querySelector('#inside2');
const afterp1 = document.querySelector('#after_p1');
let popover1HideCount = 0;
popover1.addEventListener('beforetoggle',(e) => {
if (e.newState !== "closed")
return;
++popover1HideCount;
e.preventDefault(); // 'beforetoggle' should not be cancellable.
});
let popover2HideCount = 0;
popover2.addEventListener('beforetoggle',(e) => {
if (e.newState !== "closed")
return;
++popover2HideCount;
e.preventDefault(); // 'beforetoggle' should not be cancellable.
});
promise_test(async () => {
await clickOn(button1show);
assert_true(popover1.matches(':popover-open'));
await waitForRender();
p1HideCount = popover1HideCount;
await clickOn(button1show);
assert_true(popover1.matches(':popover-open'),'popover1 should stay open');
assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown');
popover1.hidePopover(); // Cleanup
assert_false(popover1.matches(':popover-open'));
},'Clicking on invoking element, after using it for activation, shouldn\'t close its popover');
promise_test(async () => {
popover1.showPopover();
assert_true(popover1.matches(':popover-open'));
assert_false(popover2.matches(':popover-open'));
await clickOn(button2);
assert_true(popover2.matches(':popover-open'),'button2 should activate popover2');
p2HideCount = popover2HideCount;
await clickOn(button2);
assert_true(popover2.matches(':popover-open'),'popover2 should stay open');
assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown');
popover1.hidePopover(); // Cleanup
assert_false(popover1.matches(':popover-open'));
assert_false(popover2.matches(':popover-open'));
},'Clicking on invoking element, after using it for activation, shouldn\'t close its popover (nested case)');
promise_test(async () => {
popover1.showPopover();
popover2.showPopover();
assert_true(popover1.matches(':popover-open'));
assert_true(popover2.matches(':popover-open'));
p2HideCount = popover2HideCount;
await clickOn(button2);
assert_true(popover2.matches(':popover-open'),'popover2 should stay open');
assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown');
popover1.hidePopover(); // Cleanup
assert_false(popover1.matches(':popover-open'));
assert_false(popover2.matches(':popover-open'));
},'Clicking on invoking element, after using it for activation, shouldn\'t close its popover (nested case, not used for invocation)');
promise_test(async () => {
popover1.showPopover(); // Directly show the popover
assert_true(popover1.matches(':popover-open'));
await waitForRender();
p1HideCount = popover1HideCount;
await clickOn(button1show);
assert_true(popover1.matches(':popover-open'),'popover1 should stay open');
assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown');
popover1.hidePopover(); // Cleanup
assert_false(popover1.matches(':popover-open'));
},'Clicking on invoking element, even if it wasn\'t used for activation, shouldn\'t close its popover');
promise_test(async () => {
popover1.showPopover(); // Directly show the popover
assert_true(popover1.matches(':popover-open'));
await waitForRender();
p1HideCount = popover1HideCount;
await clickOn(button1toggle);
assert_false(popover1.matches(':popover-open'),'popover1 should be hidden by command/commandfor');
assert_equals(popover1HideCount,p1HideCount+1,'popover1 should get hidden only once by command/commandfor');
},'Clicking on command/commandfor element, even if it wasn\'t used for activation, should hide it exactly once');
</script>
<button id=b3 commandfor=p3 command="toggle-popover">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 commandfor=p3 command="toggle-popover">Popover 3 - button 4 - unused</button>
</div>
<style>
#p3 {top:100px;}
#p4 {top:200px;}
#p5 {top:200px;}
</style>
<script>
const popover3 = document.querySelector('#p3');
const popover4 = document.querySelector('#p4');
const popover5 = document.querySelector('#p5');
const button3 = document.querySelector('#b3');
promise_test(async () => {
await clickOn(button3);
assert_true(popover3.matches(':popover-open'),'invoking element should open popover');
popover4.showPopover();
assert_true(popover4.matches(':popover-open'));
assert_false(popover3.matches(':popover-open'),'popover3 is unrelated to popover4');
popover4.hidePopover(); // Cleanup
assert_false(popover4.matches(':popover-open'));
},'A popover inside an invoking element doesn\'t participate in that invoker\'s ancestor chain');
promise_test(async () => {
popover5.showPopover();
assert_true(popover5.matches(':popover-open'));
assert_false(popover3.matches(':popover-open'));
popover3.showPopover();
assert_true(popover3.matches(':popover-open'));
assert_false(popover5.matches(':popover-open'),'Popover 5 was not invoked from popover3\'s invoker');
popover3.hidePopover();
assert_false(popover3.matches(':popover-open'));
},'An invoking element that was not used to invoke the popover is not part of the ancestor chain');
</script>
<my-element id="myElement">
<template shadowrootmode="open">
<button id=b7 commandfor=p7 command=show-popover tabindex="0">Popover7</button>
<div popover id=p7 style="top: 100px;">
<p>Popover content.</p>
<input id="inside7" type="text" placeholder="some text">
</div>
</template>
</my-element>
<script>
const button7 = document.querySelector('#myElement').shadowRoot.querySelector('#b7');
const popover7 = document.querySelector('#myElement').shadowRoot.querySelector('#p7');
const inside7 = document.querySelector('#myElement').shadowRoot.querySelector('#inside7');
promise_test(async () => {
button7.click();
assert_true(popover7.matches(':popover-open'),'invoking element should open popover');
inside7.click();
assert_true(popover7.matches(':popover-open'));
popover7.hidePopover();
},'Clicking inside a shadow DOM popover does not close that popover');
promise_test(async () => {
button7.click();
inside7.click();
assert_true(popover7.matches(':popover-open'));
await clickOn(outside);
assert_false(popover7.matches(':popover-open'));
},'Clicking outside a shadow DOM popover should close that popover');
</script>
<div popover id=p8>
<button tabindex="0">Button</button>
<span id=inside8after>Inside popover 8 after button</span>
</div>
<button id=p8invoker commandfor=p8 command="toggle-popover" tabindex="0">Popover8 invoker (no action)</button>
<script>
promise_test(async () => {
const popover8 = document.querySelector('#p8');
const inside8After = document.querySelector('#inside8after');
const popover8Invoker = document.querySelector('#p8invoker');
assert_false(popover8.matches(':popover-open'));
popover8.showPopover();
await clickOn(inside8After);
assert_true(popover8.matches(':popover-open'));
await sendTab();
assert_equals(document.activeElement,popover8Invoker,'Focus should move to the invoker element');
assert_true(popover8.matches(':popover-open'),'popover should stay open');
popover8.hidePopover(); // Cleanup
},'Moving focus back to the invoker element should not dismiss the popover');
</script>
<!-- Convoluted ancestor relationship -->
<div popover id=convoluted_p1>Popover 1
<button commandfor=convoluted_p2 command="toggle-popover">Open Popover 2</button>
<div popover id=convoluted_p2>Popover 2
<button commandfor=convoluted_p3 command="toggle-popover">Open Popover 3</button>
<button commandfor=convoluted_p2 command=show-popover>Self-linked invoker</button>
</div>
<div popover id=convoluted_p3>Popover 3
<button commandfor=convoluted_p4 command="toggle-popover">Open Popover 4</button>
</div>
<div popover id=convoluted_p4><p>Popover 4</p></div>
</div>
<button onclick="convoluted_p1.showPopover()" tabindex="0">Open convoluted popover</button>
<style>
#convoluted_p1 {top:50px;}
#convoluted_p2 {top:150px;}
#convoluted_p3 {top:250px;}
#convoluted_p4 {top:350px;}
</style>
<script>
const convPopover1 = document.querySelector('#convoluted_p1');
const convPopover2 = document.querySelector('#convoluted_p2');
const convPopover3 = document.querySelector('#convoluted_p3');
const convPopover4 = document.querySelector('#convoluted_p4');
promise_test(async () => {
convPopover1.showPopover(); // Programmatically open p1
assert_true(convPopover1.matches(':popover-open'));
convPopover1.querySelector('button').click(); // Click to invoke p2
assert_true(convPopover1.matches(':popover-open'));
assert_true(convPopover2.matches(':popover-open'));
convPopover2.querySelector('button').click(); // Click to invoke p3
assert_true(convPopover1.matches(':popover-open'));
assert_true(convPopover2.matches(':popover-open'));
assert_true(convPopover3.matches(':popover-open'));
convPopover3.querySelector('button').click(); // Click to invoke p4
assert_true(convPopover1.matches(':popover-open'));
assert_true(convPopover2.matches(':popover-open'));
assert_true(convPopover3.matches(':popover-open'));
assert_true(convPopover4.matches(':popover-open'));
convPopover4.firstElementChild.click(); // Click within p4
assert_true(convPopover1.matches(':popover-open'));
assert_true(convPopover2.matches(':popover-open'));
assert_true(convPopover3.matches(':popover-open'));
assert_true(convPopover4.matches(':popover-open'));
convPopover1.hidePopover();
assert_false(convPopover1.matches(':popover-open'));
assert_false(convPopover2.matches(':popover-open'));
assert_false(convPopover3.matches(':popover-open'));
assert_false(convPopover4.matches(':popover-open'));
},'Ensure circular/convoluted ancestral relationships are functional');
promise_test(async () => {
convPopover1.showPopover(); // Programmatically open p1
convPopover1.querySelector('button').click(); // Click to invoke p2
assert_true(convPopover1.matches(':popover-open'));
assert_true(convPopover2.matches(':popover-open'));
assert_false(convPopover3.matches(':popover-open'));
assert_false(convPopover4.matches(':popover-open'));
convPopover4.showPopover(); // Programmatically open p4
assert_true(convPopover1.matches(':popover-open'),'popover1 stays open because it is a DOM ancestor of popover4');
assert_false(convPopover2.matches(':popover-open'),'popover2 closes because it isn\'t connected to popover4 via active invokers');
assert_true(convPopover4.matches(':popover-open'));
convPopover4.firstElementChild.click(); // Click within p4
assert_true(convPopover1.matches(':popover-open'),'nothing changes');
assert_false(convPopover2.matches(':popover-open'));
assert_true(convPopover4.matches(':popover-open'));
convPopover1.hidePopover();
assert_false(convPopover1.matches(':popover-open'));
assert_false(convPopover2.matches(':popover-open'));
assert_false(convPopover3.matches(':popover-open'));
assert_false(convPopover4.matches(':popover-open'));
},'Ensure circular/convoluted ancestral relationships are functional, with a direct showPopover()');
</script>
<div id=p29 popover>Popover 29</div>
<button id=b29 commandfor=p29 command="toggle-popover">Open popover 29</button>
<iframe id=iframe29 width=100 height=30></iframe>
<script>
promise_test(async () => {
let iframe_url = (new URL("/common/blank.html", location.href)).href;
iframe29.src = iframe_url;
iframe29.contentDocument.body.style.height = '100%';
assert_false(p29.matches(':popover-open'),'initially hidden');
p29.showPopover();
assert_true(p29.matches(':popover-open'),'showing');
let actions = new test_driver.Actions();
// Using the iframe's contentDocument as the origin would throw an error, so
// we are using iframe29 as the origin instead.
const iframe_box = iframe29.getBoundingClientRect();
await actions
.pointerMove(1,1,{origin: b29})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerMove(iframe_box.width / 2, iframe_box.height / 2, {origin: iframe29})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
assert_true(p29.matches(':popover-open'), 'popover should be open after pointerUp in iframe.');
actions = new test_driver.Actions();
await actions
.pointerMove(iframe_box.width / 2, iframe_box.height / 2, {origin: iframe29})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerMove(1,1,{origin: b29})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
assert_true(p29.matches(':popover-open'), 'popover should be open after pointerUp on main frame button.');
},`Pointer down in one document and pointer up in another document shouldn't dismiss popover`);
</script>
<div id=p30 popover>Popover 30</div>
<button id=b30 commandfor=p30 command="toggle-popover">Open popover 30</button>
<button id=b30b>Non-invoker</button>
<script>
promise_test(async () => {
assert_false(p30.matches(':popover-open'),'initially hidden');
p30.showPopover();
assert_true(p30.matches(':popover-open'),'showing');
let actions = new test_driver.Actions();
await actions
.pointerMove(2,2,{origin: b30})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerMove(2,2,{origin: b30b})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
await waitForRender();
assert_true(p30.matches(':popover-open'),'showing after pointerup');
},`Pointer down inside invoker and up outside that invoker shouldn't dismiss popover`);
</script>