1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
|
<!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">
<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>
<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>
|