| <!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:[email protected]"> |
| <link rel="author" href="mailto:[email protected]"> |
| <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> |