blob: fbf19ea891c84ec5bd2cc503f965c7964e286c8f [file] [log] [blame]
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Popup light dismiss 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>
<button id=b1 togglepopup='p1'>Popup 1</button>
<button id=p1anchor>Popup1 anchor (no action)</button>
<span id=outside>Outside all popups</span>
<div popup=popup id=p1 anchor=p1anchor>
<span id=inside1>Inside popup 1</span>
<button id=b2 togglepopup='p2'>Popup 2</button>
<span id=inside1after>Inside popup 1 after button</span>
</div>
<div popup=popup id=p2 anchor=b2>
<span id=inside2>Inside popup 2</span>
</div>
<button id=after_p1>Next control after popup1</button>
<style>
#p1 {top: 50px;}
#p2 {top: 120px;}
</style>
<script>
function spinEventLoop() {
return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}
async function clickOn(element) {
const actions = new test_driver.Actions();
await spinEventLoop();
await actions.pointerMove(0, 0, {origin: element})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
await spinEventLoop();
}
async function sendTab() {
await spinEventLoop();
await new test_driver.send_keys(document.body,'\uE004'); // Tab
await spinEventLoop();
}
const popup1 = document.querySelector('#p1');
const button1 = document.querySelector('#b1');
const popup1anchor = document.querySelector('#p1anchor');
const inside1After = document.querySelector('#inside1after');
const popup2 = 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 popup1HideCount = 0;
popup1.addEventListener('hide',(e) => {
++popup1HideCount;
e.preventDefault(); // 'hide' should not be cancellable.
});
let popup2HideCount = 0;
popup2.addEventListener('hide',(e) => {
++popup2HideCount;
e.preventDefault(); // 'hide' should not be cancellable.
});
promise_test(async () => {
assert_false(popup1.matches(':popup-open'));
popup1.showPopup();
assert_true(popup1.matches(':popup-open'));
let p1HideCount = popup1HideCount;
await clickOn(outside);
assert_false(popup1.matches(':popup-open'));
assert_equals(popup1HideCount,p1HideCount+1);
},'Clicking outside a popup will dismiss the popup');
promise_test(async () => {
assert_false(popup1.matches(':popup-open'));
popup1.showPopup();
await spinEventLoop();
p1HideCount = popup1HideCount;
await clickOn(inside1);
assert_true(popup1.matches(':popup-open'));
assert_equals(popup1HideCount,p1HideCount);
popup1.hidePopup();
},'Clicking inside a popup does not close that popup');
promise_test(async () => {
assert_false(popup1.matches(':popup-open'));
popup1.showPopup();
await clickOn(inside1After);
assert_true(popup1.matches(':popup-open'));
await sendTab();
assert_equals(document.activeElement,afterp1,'Focus should move to a button outside the popup');
assert_false(popup1.matches(':popup-open'));
},'Moving focus outside the popup will dismiss the popup');
promise_test(async () => {
popup1.showPopup();
popup2.showPopup();
await spinEventLoop();
p1HideCount = popup1HideCount;
let p2HideCount = popup2HideCount;
await clickOn(inside2);
assert_true(popup1.matches(':popup-open'),'popup1 should be open');
assert_true(popup2.matches(':popup-open'),'popup2 should be open');
assert_equals(popup1HideCount,p1HideCount,'popup1');
assert_equals(popup2HideCount,p2HideCount,'popup2');
popup1.hidePopup();
popup2.hidePopup();
},'Clicking inside a child popup shouldn\'t close either popup');
promise_test(async () => {
popup1.showPopup();
popup2.showPopup();
await spinEventLoop();
p1HideCount = popup1HideCount;
p2HideCount = popup2HideCount;
await clickOn(inside1);
assert_true(popup1.matches(':popup-open'));
assert_equals(popup1HideCount,p1HideCount);
assert_false(popup2.matches(':popup-open'));
assert_equals(popup2HideCount,p2HideCount+1);
popup1.hidePopup();
},'Clicking inside a parent popup should close child popup');
promise_test(async () => {
popup1.showPopup();
assert_true(popup1.matches(':popup-open'));
await spinEventLoop();
p1HideCount = popup1HideCount;
await clickOn(popup1anchor);
assert_true(popup1.matches(':popup-open'),'popup1 not open');
assert_equals(popup1HideCount,p1HideCount);
popup1.hidePopup(); // Cleanup
assert_false(popup1.matches(':popup-open'));
},'Clicking on anchor element (that isn\'t an invoking element) shouldn\'t close its popup');
promise_test(async () => {
popup1.showPopup();
popup2.showPopup(); // Popup1 is an ancestral element for popup2.
assert_true(popup1.matches(':popup-open'));
assert_true(popup2.matches(':popup-open'));
const drag_actions = new test_driver.Actions();
// Drag *from* popup2 *to* popup1 (its ancestor).
await drag_actions.pointerMove(0,0,{origin: popup2})
.pointerDown({button: drag_actions.ButtonType.LEFT})
.pointerMove(0,0,{origin: popup1})
.pointerUp({button: drag_actions.ButtonType.LEFT})
.send();
assert_true(popup1.matches(':popup-open'),'popup1 should be open');
assert_true(popup2.matches(':popup-open'),'popup1 should be open');
popup1.hidePopup();
assert_false(popup2.matches(':popup-open'));
},'Dragging from an open popup outside an open popup should leave the popup open');
</script>
<button id=b3 togglepopup=p3>Popup 3 - button 3
<div popup=popup id=p4>Inside popup 4</div>
</button>
<div popup=popup id=p3>Inside popup 3</div>
<button id=b4 togglepopup=p3>Popup 3 - button 4
<div popup=popup id=p5>Inside popup 5</div>
</button>
<style>
#p3 {top:100px;}
#p4 {top:200px;}
#p5 {top:200px;}
</style>
<script>
const popup3 = document.querySelector('#p3');
const popup4 = document.querySelector('#p4');
const popup5 = document.querySelector('#p5');
const button3 = document.querySelector('#b3');
promise_test(async () => {
await clickOn(button3);
assert_true(popup3.matches(':popup-open'),'invoking element should open popup');
popup4.showPopup();
assert_true(popup4.matches(':popup-open'));
assert_true(popup3.matches(':popup-open'));
popup3.hidePopup(); // Cleanup
assert_false(popup3.matches(':popup-open'));
assert_false(popup4.matches(':popup-open'));
},'An invoking element should be part of the ancestor chain');
promise_test(async () => {
await clickOn(button3);
assert_true(popup3.matches(':popup-open'));
assert_false(popup4.matches(':popup-open'));
assert_false(popup5.matches(':popup-open'));
popup5.showPopup();
assert_true(popup3.matches(':popup-open'));
assert_false(popup4.matches(':popup-open'));
assert_true(popup5.matches(':popup-open'));
popup3.hidePopup();
assert_false(popup3.matches(':popup-open'));
assert_false(popup4.matches(':popup-open'));
assert_false(popup5.matches(':popup-open'));
},'An invoking element that was not used to invoke the popup can still be part of the ancestor chain');
</script>
<div popup=popup id=p6>Inside popup 6
<div style="height:2000px;background:lightgreen"></div>
Bottom of popup6
</div>
<button togglepopup=p6>Popup 6</button>
<style>
#p6 {
width: 300px;
height: 300px;
overflow-y: scroll;
}
</style>
<script>
const popup6 = document.querySelector('#p6');
promise_test(async () => {
popup6.showPopup();
assert_equals(popup6.scrollTop,0,'popup6 should start non-scrolled');
await new test_driver.Actions()
.scroll(0, 0, 0, 50, {origin: popup6})
.send();
assert_true(popup6.matches(':popup-open'),'popup6 should stay open');
assert_equals(popup6.scrollTop,50,'popup6 should be scrolled');
popup6.hidePopup();
},'Scrolling within a popup should not close the popup');
</script>
<my-element id="myElement">
<template shadowroot="open">
<button id=b7 onclick='showPopup7()'>Popup7</button>
<div popup=popup id=p7 anchor=b7 style="top: 100px;">
<p>Popup content.</p>
<input id="inside7" type="text" placeholder="some text">
</div>
</template>
</my-element>
<script>
const button7 = document.querySelector('#myElement').shadowRoot.querySelector('#b7');
const popup7 = document.querySelector('#myElement').shadowRoot.querySelector('#p7');
const inside7 = document.querySelector('#myElement').shadowRoot.querySelector('#inside7');
function showPopup7() {
popup7.showPopup();
}
promise_test(async () => {
button7.click();
assert_true(popup7.matches(':popup-open'),'invoking element should open popup');
inside7.click();
assert_true(popup7.matches(':popup-open'));
popup7.hidePopup();
},'Clicking inside a shadow DOM popup does not close that popup');
promise_test(async () => {
button7.click();
inside7.click();
assert_true(popup7.matches(':popup-open'));
await clickOn(outside);
assert_false(popup7.matches(':popup-open'));
},'Clicking outside a shadow DOM popup should close that popup');
</script>
<div popup=popup id=p8 anchor=p8anchor>
<button>Button</button>
<span id=inside8after>Inside popup 8 after button</span>
</div>
<button id=p8anchor>Popup8 anchor (no action)</button>
<script>
promise_test(async () => {
const popup8 = document.querySelector('#p8');
const inside8After = document.querySelector('#inside8after');
const popup8Anchor = document.querySelector('#p8anchor');
assert_false(popup8.matches(':popup-open'));
popup8.showPopup();
await clickOn(inside8After);
assert_true(popup8.matches(':popup-open'));
await sendTab();
assert_equals(document.activeElement,popup8Anchor,'Focus should move to the anchor element');
assert_true(popup8.matches(':popup-open'),'popup should stay open');
popup8.hidePopup();
},'Moving focus back to the anchor element should not dismiss the popup');
</script>
<div popup=popup id=p9>
<button>Button</button>
<span id=inside9after>Inside popup 9 after button</span>
</div>
<button id=b9after togglepopup='p9'>Popup 9</button>
<script>
promise_test(async () => {
const popup9 = document.querySelector('#p9');
const inside9After = document.querySelector('#inside9after');
const popup9Invoker = document.querySelector('#b9after');
assert_false(popup9.matches(':popup-open'));
popup9Invoker.click(); // Trigger via the button
await clickOn(inside9After);
assert_true(popup9.matches(':popup-open'));
await sendTab();
assert_equals(document.activeElement,popup9Invoker,'Focus should move to the invoking element');
assert_true(popup9.matches(':popup-open'),'popup should stay open');
popup9.hidePopup();
},'Moving focus back to the active trigger element should not dismiss the popup');
promise_test(async () => {
const popup9 = document.querySelector('#p9');
const inside9After = document.querySelector('#inside9after');
const popup9Invoker = document.querySelector('#b9after');
assert_false(popup9.matches(':popup-open'));
popup9.showPopup(); // Trigger directly
await clickOn(inside9After);
assert_true(popup9.matches(':popup-open'));
await sendTab();
assert_equals(document.activeElement,popup9Invoker,'Focus should move to the invoking element');
assert_true(popup9.matches(':popup-open'),'popup should stay open - even though the trigger wasn\'t used, it points to this popup');
},'Moving focus back to an inactive trigger element should also *not* dismiss the popup');
</script>
<!-- Convoluted ancestor relationship -->
<div popup=popup id=convoluted_p1>Popup 1
<div id=convoluted_anchor>Anchor
<button togglepopup=convoluted_p2>Open Popup 2</button>
<div popup=popup id=convoluted_p4><p>Popup 4</p></div>
</div>
</div>
<div popup=popup id=convoluted_p2>Popup 2
<button togglepopup=convoluted_p3>Open Popup 3</button>
</div>
<div popup=popup id=convoluted_p3 anchor=convoluted_anchor>Popup 3
<button togglepopup=convoluted_p4>Open Popup 4</button>
</div>
<button onclick="convoluted_p1.showPopup()">Open convoluted popup</button>
<style>
#convoluted_p1 {top:50px;}
#convoluted_p2 {top:150px;}
#convoluted_p3 {top:250px;}
#convoluted_p4 {top:350px;}
</style>
<script>
const convPopup1 = document.querySelector('#convoluted_p1');
const convPopup2 = document.querySelector('#convoluted_p2');
const convPopup3 = document.querySelector('#convoluted_p3');
const convPopup4 = document.querySelector('#convoluted_p4');
promise_test(async () => {
convPopup1.showPopup(); // Programmatically open p1
assert_true(convPopup1.matches(':popup-open'));
convPopup1.querySelector('button').click(); // Click to invoke p2
assert_true(convPopup1.matches(':popup-open'));
assert_true(convPopup2.matches(':popup-open'));
convPopup2.querySelector('button').click(); // Click to invoke p3
assert_true(convPopup1.matches(':popup-open'));
assert_true(convPopup2.matches(':popup-open'));
assert_true(convPopup3.matches(':popup-open'));
convPopup3.querySelector('button').click(); // Click to invoke p4
assert_true(convPopup1.matches(':popup-open'));
assert_true(convPopup2.matches(':popup-open'));
assert_true(convPopup3.matches(':popup-open'));
assert_true(convPopup4.matches(':popup-open'));
convPopup4.firstElementChild.click(); // Click within p4
assert_true(convPopup1.matches(':popup-open'));
assert_true(convPopup2.matches(':popup-open'));
assert_true(convPopup3.matches(':popup-open'));
assert_true(convPopup4.matches(':popup-open'));
convPopup1.hidePopup();
assert_false(convPopup1.matches(':popup-open'));
assert_false(convPopup2.matches(':popup-open'));
assert_false(convPopup3.matches(':popup-open'));
assert_false(convPopup4.matches(':popup-open'));
},'Ensure circular/convoluted ancestral relationships are functional');
promise_test(async () => {
convPopup1.showPopup(); // Programmatically open p1
convPopup1.querySelector('button').click(); // Click to invoke p2
assert_true(convPopup1.matches(':popup-open'));
assert_true(convPopup2.matches(':popup-open'));
assert_false(convPopup3.matches(':popup-open'));
assert_false(convPopup4.matches(':popup-open'));
convPopup4.showPopup(); // Programmatically open p4
assert_true(convPopup1.matches(':popup-open'),'popup1 stays open because it is a DOM ancestor of popup4');
assert_false(convPopup2.matches(':popup-open'),'popup2 closes because it isn\'t connected to popup4 via active invokers');
assert_true(convPopup4.matches(':popup-open'));
convPopup4.firstElementChild.click(); // Click within p4
assert_true(convPopup1.matches(':popup-open'),'nothing changes');
assert_false(convPopup2.matches(':popup-open'));
assert_true(convPopup4.matches(':popup-open'));
convPopup1.hidePopup();
assert_false(convPopup1.matches(':popup-open'));
assert_false(convPopup2.matches(':popup-open'));
assert_false(convPopup3.matches(':popup-open'));
assert_false(convPopup4.matches(':popup-open'));
},'Ensure circular/convoluted ancestral relationships are functional, with a direct showPopup()');
</script>