popover-light-dismiss-command.html (15771B)
1 <!DOCTYPE html> 2 <meta charset="utf-8" /> 3 <title>Popover light dismiss behavior with command/commandfor</title> 4 <meta name="timeout" content="long"> 5 <link rel="author" href="mailto:masonf@chromium.org"> 6 <link rel="author" href="mailto:lwarlow@igalia.com"> 7 <link rel=help href="https://open-ui.org/components/popover.research.explainer"> 8 <script src="/resources/testharness.js"></script> 9 <script src="/resources/testharnessreport.js"></script> 10 <script src="/resources/testdriver.js"></script> 11 <script src="/resources/testdriver-actions.js"></script> 12 <script src="/resources/testdriver-vendor.js"></script> 13 <script src="resources/popover-utils.js"></script> 14 <style> 15 [popover] { 16 /* Position most popovers at the bottom-right, out of the way */ 17 inset:auto; 18 bottom:0; 19 right:0; 20 } 21 [popover]::backdrop { 22 /* This should *not* affect anything: */ 23 pointer-events: auto; 24 } 25 </style> 26 <button id=b1t commandfor='p1' command="toggle-popover">Popover 1</button> 27 <button id=b1s commandfor='p1' command="show-popover">Popover 1</button> 28 <span id=outside>Outside all popovers</span> 29 <div popover id=p1> 30 <span id=inside1>Inside popover 1</span> 31 <button id=b2 commandfor='p2' command="show-popover">Popover 2</button> 32 <span id=inside1after>Inside popover 1 after button</span> 33 <div popover id=p2> 34 <span id=inside2>Inside popover 2</span> 35 </div> 36 </div> 37 <button id=after_p1 tabindex="0">Next control after popover1</button> 38 <style> 39 #p1 {top: 50px;} 40 #p2 {top: 120px;} 41 </style> 42 <script> 43 const popover1 = document.querySelector('#p1'); 44 const button1toggle = document.querySelector('#b1t'); 45 const button1show = document.querySelector('#b1s'); 46 const inside1After = document.querySelector('#inside1after'); 47 const button2 = document.querySelector('#b2'); 48 const popover2 = document.querySelector('#p2'); 49 const outside = document.querySelector('#outside'); 50 const inside1 = document.querySelector('#inside1'); 51 const inside2 = document.querySelector('#inside2'); 52 const afterp1 = document.querySelector('#after_p1'); 53 let popover1HideCount = 0; 54 popover1.addEventListener('beforetoggle',(e) => { 55 if (e.newState !== "closed") 56 return; 57 ++popover1HideCount; 58 e.preventDefault(); // 'beforetoggle' should not be cancellable. 59 }); 60 let popover2HideCount = 0; 61 popover2.addEventListener('beforetoggle',(e) => { 62 if (e.newState !== "closed") 63 return; 64 ++popover2HideCount; 65 e.preventDefault(); // 'beforetoggle' should not be cancellable. 66 }); 67 promise_test(async () => { 68 await clickOn(button1show); 69 assert_true(popover1.matches(':popover-open')); 70 await waitForRender(); 71 p1HideCount = popover1HideCount; 72 await clickOn(button1show); 73 assert_true(popover1.matches(':popover-open'),'popover1 should stay open'); 74 assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown'); 75 popover1.hidePopover(); // Cleanup 76 assert_false(popover1.matches(':popover-open')); 77 },'Clicking on invoking element, after using it for activation, shouldn\'t close its popover'); 78 promise_test(async () => { 79 popover1.showPopover(); 80 assert_true(popover1.matches(':popover-open')); 81 assert_false(popover2.matches(':popover-open')); 82 await clickOn(button2); 83 assert_true(popover2.matches(':popover-open'),'button2 should activate popover2'); 84 p2HideCount = popover2HideCount; 85 await clickOn(button2); 86 assert_true(popover2.matches(':popover-open'),'popover2 should stay open'); 87 assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown'); 88 popover1.hidePopover(); // Cleanup 89 assert_false(popover1.matches(':popover-open')); 90 assert_false(popover2.matches(':popover-open')); 91 },'Clicking on invoking element, after using it for activation, shouldn\'t close its popover (nested case)'); 92 promise_test(async () => { 93 popover1.showPopover(); 94 popover2.showPopover(); 95 assert_true(popover1.matches(':popover-open')); 96 assert_true(popover2.matches(':popover-open')); 97 p2HideCount = popover2HideCount; 98 await clickOn(button2); 99 assert_true(popover2.matches(':popover-open'),'popover2 should stay open'); 100 assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown'); 101 popover1.hidePopover(); // Cleanup 102 assert_false(popover1.matches(':popover-open')); 103 assert_false(popover2.matches(':popover-open')); 104 },'Clicking on invoking element, after using it for activation, shouldn\'t close its popover (nested case, not used for invocation)'); 105 promise_test(async () => { 106 popover1.showPopover(); // Directly show the popover 107 assert_true(popover1.matches(':popover-open')); 108 await waitForRender(); 109 p1HideCount = popover1HideCount; 110 await clickOn(button1show); 111 assert_true(popover1.matches(':popover-open'),'popover1 should stay open'); 112 assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown'); 113 popover1.hidePopover(); // Cleanup 114 assert_false(popover1.matches(':popover-open')); 115 },'Clicking on invoking element, even if it wasn\'t used for activation, shouldn\'t close its popover'); 116 promise_test(async () => { 117 popover1.showPopover(); // Directly show the popover 118 assert_true(popover1.matches(':popover-open')); 119 await waitForRender(); 120 p1HideCount = popover1HideCount; 121 await clickOn(button1toggle); 122 assert_false(popover1.matches(':popover-open'),'popover1 should be hidden by command/commandfor'); 123 assert_equals(popover1HideCount,p1HideCount+1,'popover1 should get hidden only once by command/commandfor'); 124 },'Clicking on command/commandfor element, even if it wasn\'t used for activation, should hide it exactly once'); 125 </script> 126 <button id=b3 commandfor=p3 command="toggle-popover">Popover 3 - button 3 127 <div popover id=p4>Inside popover 4</div> 128 </button> 129 <div popover id=p3>Inside popover 3</div> 130 <div popover id=p5>Inside popover 5 131 <button commandfor=p3 command="toggle-popover">Popover 3 - button 4 - unused</button> 132 </div> 133 <style> 134 #p3 {top:100px;} 135 #p4 {top:200px;} 136 #p5 {top:200px;} 137 </style> 138 <script> 139 const popover3 = document.querySelector('#p3'); 140 const popover4 = document.querySelector('#p4'); 141 const popover5 = document.querySelector('#p5'); 142 const button3 = document.querySelector('#b3'); 143 promise_test(async () => { 144 await clickOn(button3); 145 assert_true(popover3.matches(':popover-open'),'invoking element should open popover'); 146 popover4.showPopover(); 147 assert_true(popover4.matches(':popover-open')); 148 assert_false(popover3.matches(':popover-open'),'popover3 is unrelated to popover4'); 149 popover4.hidePopover(); // Cleanup 150 assert_false(popover4.matches(':popover-open')); 151 },'A popover inside an invoking element doesn\'t participate in that invoker\'s ancestor chain'); 152 promise_test(async () => { 153 popover5.showPopover(); 154 assert_true(popover5.matches(':popover-open')); 155 assert_false(popover3.matches(':popover-open')); 156 popover3.showPopover(); 157 assert_true(popover3.matches(':popover-open')); 158 assert_false(popover5.matches(':popover-open'),'Popover 5 was not invoked from popover3\'s invoker'); 159 popover3.hidePopover(); 160 assert_false(popover3.matches(':popover-open')); 161 },'An invoking element that was not used to invoke the popover is not part of the ancestor chain'); 162 </script> 163 <my-element id="myElement"> 164 <template shadowrootmode="open"> 165 <button id=b7 commandfor=p7 command=show-popover tabindex="0">Popover7</button> 166 <div popover id=p7 style="top: 100px;"> 167 <p>Popover content.</p> 168 <input id="inside7" type="text" placeholder="some text"> 169 </div> 170 </template> 171 </my-element> 172 <script> 173 const button7 = document.querySelector('#myElement').shadowRoot.querySelector('#b7'); 174 const popover7 = document.querySelector('#myElement').shadowRoot.querySelector('#p7'); 175 const inside7 = document.querySelector('#myElement').shadowRoot.querySelector('#inside7'); 176 promise_test(async () => { 177 button7.click(); 178 assert_true(popover7.matches(':popover-open'),'invoking element should open popover'); 179 inside7.click(); 180 assert_true(popover7.matches(':popover-open')); 181 popover7.hidePopover(); 182 },'Clicking inside a shadow DOM popover does not close that popover'); 183 promise_test(async () => { 184 button7.click(); 185 inside7.click(); 186 assert_true(popover7.matches(':popover-open')); 187 await clickOn(outside); 188 assert_false(popover7.matches(':popover-open')); 189 },'Clicking outside a shadow DOM popover should close that popover'); 190 </script> 191 <div popover id=p8> 192 <button tabindex="0">Button</button> 193 <span id=inside8after>Inside popover 8 after button</span> 194 </div> 195 <button id=p8invoker commandfor=p8 command="toggle-popover" tabindex="0">Popover8 invoker (no action)</button> 196 <script> 197 promise_test(async () => { 198 const popover8 = document.querySelector('#p8'); 199 const inside8After = document.querySelector('#inside8after'); 200 const popover8Invoker = document.querySelector('#p8invoker'); 201 assert_false(popover8.matches(':popover-open')); 202 popover8.showPopover(); 203 await clickOn(inside8After); 204 assert_true(popover8.matches(':popover-open')); 205 await sendTab(); 206 assert_equals(document.activeElement,popover8Invoker,'Focus should move to the invoker element'); 207 assert_true(popover8.matches(':popover-open'),'popover should stay open'); 208 popover8.hidePopover(); // Cleanup 209 },'Moving focus back to the invoker element should not dismiss the popover'); 210 </script> 211 <!-- Convoluted ancestor relationship --> 212 <div popover id=convoluted_p1>Popover 1 213 <button commandfor=convoluted_p2 command="toggle-popover">Open Popover 2</button> 214 <div popover id=convoluted_p2>Popover 2 215 <button commandfor=convoluted_p3 command="toggle-popover">Open Popover 3</button> 216 <button commandfor=convoluted_p2 command=show-popover>Self-linked invoker</button> 217 </div> 218 <div popover id=convoluted_p3>Popover 3 219 <button commandfor=convoluted_p4 command="toggle-popover">Open Popover 4</button> 220 </div> 221 <div popover id=convoluted_p4><p>Popover 4</p></div> 222 </div> 223 <button onclick="convoluted_p1.showPopover()" tabindex="0">Open convoluted popover</button> 224 <style> 225 #convoluted_p1 {top:50px;} 226 #convoluted_p2 {top:150px;} 227 #convoluted_p3 {top:250px;} 228 #convoluted_p4 {top:350px;} 229 </style> 230 <script> 231 const convPopover1 = document.querySelector('#convoluted_p1'); 232 const convPopover2 = document.querySelector('#convoluted_p2'); 233 const convPopover3 = document.querySelector('#convoluted_p3'); 234 const convPopover4 = document.querySelector('#convoluted_p4'); 235 promise_test(async () => { 236 convPopover1.showPopover(); // Programmatically open p1 237 assert_true(convPopover1.matches(':popover-open')); 238 convPopover1.querySelector('button').click(); // Click to invoke p2 239 assert_true(convPopover1.matches(':popover-open')); 240 assert_true(convPopover2.matches(':popover-open')); 241 convPopover2.querySelector('button').click(); // Click to invoke p3 242 assert_true(convPopover1.matches(':popover-open')); 243 assert_true(convPopover2.matches(':popover-open')); 244 assert_true(convPopover3.matches(':popover-open')); 245 convPopover3.querySelector('button').click(); // Click to invoke p4 246 assert_true(convPopover1.matches(':popover-open')); 247 assert_true(convPopover2.matches(':popover-open')); 248 assert_true(convPopover3.matches(':popover-open')); 249 assert_true(convPopover4.matches(':popover-open')); 250 convPopover4.firstElementChild.click(); // Click within p4 251 assert_true(convPopover1.matches(':popover-open')); 252 assert_true(convPopover2.matches(':popover-open')); 253 assert_true(convPopover3.matches(':popover-open')); 254 assert_true(convPopover4.matches(':popover-open')); 255 convPopover1.hidePopover(); 256 assert_false(convPopover1.matches(':popover-open')); 257 assert_false(convPopover2.matches(':popover-open')); 258 assert_false(convPopover3.matches(':popover-open')); 259 assert_false(convPopover4.matches(':popover-open')); 260 },'Ensure circular/convoluted ancestral relationships are functional'); 261 promise_test(async () => { 262 convPopover1.showPopover(); // Programmatically open p1 263 convPopover1.querySelector('button').click(); // Click to invoke p2 264 assert_true(convPopover1.matches(':popover-open')); 265 assert_true(convPopover2.matches(':popover-open')); 266 assert_false(convPopover3.matches(':popover-open')); 267 assert_false(convPopover4.matches(':popover-open')); 268 convPopover4.showPopover(); // Programmatically open p4 269 assert_true(convPopover1.matches(':popover-open'),'popover1 stays open because it is a DOM ancestor of popover4'); 270 assert_false(convPopover2.matches(':popover-open'),'popover2 closes because it isn\'t connected to popover4 via active invokers'); 271 assert_true(convPopover4.matches(':popover-open')); 272 convPopover4.firstElementChild.click(); // Click within p4 273 assert_true(convPopover1.matches(':popover-open'),'nothing changes'); 274 assert_false(convPopover2.matches(':popover-open')); 275 assert_true(convPopover4.matches(':popover-open')); 276 convPopover1.hidePopover(); 277 assert_false(convPopover1.matches(':popover-open')); 278 assert_false(convPopover2.matches(':popover-open')); 279 assert_false(convPopover3.matches(':popover-open')); 280 assert_false(convPopover4.matches(':popover-open')); 281 },'Ensure circular/convoluted ancestral relationships are functional, with a direct showPopover()'); 282 </script> 283 <div id=p29 popover>Popover 29</div> 284 <button id=b29 commandfor=p29 command="toggle-popover">Open popover 29</button> 285 <iframe id=iframe29 width=100 height=30></iframe> 286 <script> 287 promise_test(async () => { 288 let iframe_url = (new URL("/common/blank.html", location.href)).href; 289 iframe29.src = iframe_url; 290 iframe29.contentDocument.body.style.height = '100%'; 291 assert_false(p29.matches(':popover-open'),'initially hidden'); 292 p29.showPopover(); 293 assert_true(p29.matches(':popover-open'),'showing'); 294 let actions = new test_driver.Actions(); 295 // Using the iframe's contentDocument as the origin would throw an error, so 296 // we are using iframe29 as the origin instead. 297 const iframe_box = iframe29.getBoundingClientRect(); 298 await actions 299 .pointerMove(1,1,{origin: b29}) 300 .pointerDown({button: actions.ButtonType.LEFT}) 301 .pointerMove(iframe_box.width / 2, iframe_box.height / 2, {origin: iframe29}) 302 .pointerUp({button: actions.ButtonType.LEFT}) 303 .send(); 304 assert_true(p29.matches(':popover-open'), 'popover should be open after pointerUp in iframe.'); 305 actions = new test_driver.Actions(); 306 await actions 307 .pointerMove(iframe_box.width / 2, iframe_box.height / 2, {origin: iframe29}) 308 .pointerDown({button: actions.ButtonType.LEFT}) 309 .pointerMove(1,1,{origin: b29}) 310 .pointerUp({button: actions.ButtonType.LEFT}) 311 .send(); 312 assert_true(p29.matches(':popover-open'), 'popover should be open after pointerUp on main frame button.'); 313 },`Pointer down in one document and pointer up in another document shouldn't dismiss popover`); 314 </script> 315 <div id=p30 popover>Popover 30</div> 316 <button id=b30 commandfor=p30 command="toggle-popover">Open popover 30</button> 317 <button id=b30b>Non-invoker</button> 318 <script> 319 promise_test(async () => { 320 assert_false(p30.matches(':popover-open'),'initially hidden'); 321 p30.showPopover(); 322 assert_true(p30.matches(':popover-open'),'showing'); 323 let actions = new test_driver.Actions(); 324 await actions 325 .pointerMove(2,2,{origin: b30}) 326 .pointerDown({button: actions.ButtonType.LEFT}) 327 .pointerMove(2,2,{origin: b30b}) 328 .pointerUp({button: actions.ButtonType.LEFT}) 329 .send(); 330 await waitForRender(); 331 assert_true(p30.matches(':popover-open'),'showing after pointerup'); 332 },`Pointer down inside invoker and up outside that invoker shouldn't dismiss popover`); 333 </script>