popover-focus-3.html (9843B)
1 <!DOCTYPE html> 2 <meta charset="utf-8" /> 3 <title>Popover focus behaviors</title> 4 <meta name="timeout" content="long"> 5 <link rel="author" title="Luke Warlow" href="mailto:lwarlow@igalia.com"> 6 <link rel=help href="https://open-ui.org/components/popover.research.explainer"> 7 <script src="/resources/testharness.js"></script> 8 <script src="/resources/testharnessreport.js"></script> 9 <script src="/resources/testdriver.js"></script> 10 <script src="/resources/testdriver-actions.js"></script> 11 <script src="/resources/testdriver-vendor.js"></script> 12 <script src="resources/popover-utils.js"></script> 13 14 <button id=circular0 tabindex="0">Invoker</button> 15 <div id=popover4 popover> 16 <button id=circular1 autofocus tabindex="0"></button> 17 <button id=circular2 tabindex="0"></button> 18 <button id=circular3 tabindex="0"></button> 19 </div> 20 <button id=circular4 tabindex="0">after</button> 21 <script> 22 async function testCircularReferenceTabNavigation() { 23 circular0.focus(); 24 await sendEnter(); // Activate the invoker 25 await verifyFocusOrder([circular0, circular1, circular2, circular3, circular4],'circular reference'); 26 popover4.hidePopover(); 27 } 28 promise_test(async t => { 29 circular0.setAttribute('popovertarget', 'popover4'); 30 circular1.setAttribute('popovertarget', 'popover4'); 31 circular1.setAttribute('popovertargetaction', 'hide'); 32 circular2.setAttribute('popovertarget', 'popover4'); 33 circular2.setAttribute('popovertargetaction', 'show'); 34 circular3.setAttribute('popovertarget', 'popover4'); 35 t.add_cleanup(() => { 36 circular0.removeAttribute('popovertarget'); 37 circular1.removeAttribute('popovertarget'); 38 circular1.removeAttribute('popovertargetaction'); 39 circular2.removeAttribute('popovertarget'); 40 circular2.removeAttribute('popovertargetaction'); 41 circular3.removeAttribute('popovertarget'); 42 }); 43 await testCircularReferenceTabNavigation(); 44 }, "Circular reference tab navigation with popovertarget invocation"); 45 promise_test(async t => { 46 circular0.setAttribute('commandfor', 'popover4'); 47 circular1.setAttribute('commandfor', 'popover4'); 48 circular2.setAttribute('commandfor', 'popover4'); 49 circular3.setAttribute('commandfor', 'popover4'); 50 circular0.setAttribute('command', 'toggle-popover'); 51 circular1.setAttribute('command', 'hide-popover'); 52 circular2.setAttribute('command', 'show-popover'); 53 circular3.setAttribute('command', 'toggle-popover'); 54 t.add_cleanup(() => { 55 circular0.removeAttribute('commandfor'); 56 circular1.removeAttribute('commandfor'); 57 circular2.removeAttribute('commandfor'); 58 circular3.removeAttribute('commandfor'); 59 circular0.removeAttribute('command'); 60 circular1.removeAttribute('command'); 61 circular2.removeAttribute('command'); 62 circular3.removeAttribute('command'); 63 }); 64 await testCircularReferenceTabNavigation(); 65 }, "Circular reference tab navigation with command/commandfor invocation"); 66 promise_test(async t => { 67 const circular0Click = () => { 68 popover4.togglePopover({ source: circular0 }); 69 }; 70 circular0.addEventListener('click', circular0Click); 71 const circular1Click = () => { 72 popover4.hidePopover(); 73 }; 74 circular1.addEventListener('click', circular1Click); 75 const circular2Click = () => { 76 popover4.showPopover({ source: circular2 }); 77 }; 78 circular2.addEventListener('click', circular2Click); 79 const circular3Click = () => { 80 popover4.togglePopover({ source: circular3 }); 81 }; 82 circular3.addEventListener('click', circular3Click); 83 t.add_cleanup(() => { 84 circular0.removeEventListener('click', circular0Click); 85 circular1.removeEventListener('click', circular1Click); 86 circular2.removeEventListener('click', circular2Click); 87 circular3.removeEventListener('click', circular3Click); 88 }); 89 await testCircularReferenceTabNavigation(); 90 }, "Circular reference tab navigation with imperative invocation"); 91 </script> 92 93 <div id=focus-return1> 94 <button tabindex="0">Show popover</button> 95 <div popover id=focus-return1-p> 96 <button autofocus tabindex="0">Hide popover</button> 97 </div> 98 </div> 99 <script> 100 async function testPopoverFocusReturn1() { 101 const invoker = document.querySelector('#focus-return1>button'); 102 const popover = document.querySelector('#focus-return1>[popover]'); 103 const hideButton = popover.querySelector('button'); 104 invoker.focus(); // Make sure button is focused. 105 assert_equals(document.activeElement,invoker); 106 await sendEnter(); // Activate the invoker 107 assert_true(popover.matches(':popover-open'), 'popover should be invoked by invoker'); 108 assert_equals(document.activeElement,hideButton,'Hide button should be focused due to autofocus attribute'); 109 await sendEnter(); // Activate the hide invoker 110 assert_false(popover.matches(':popover-open'), 'popover should be hidden by invoker'); 111 assert_equals(document.activeElement,invoker,'Focus should be returned to the invoker'); 112 } 113 promise_test(async t => { 114 const invoker = document.querySelector('#focus-return1>button'); 115 const popover = document.querySelector('#focus-return1>[popover]'); 116 const hideButton = popover.querySelector('button'); 117 invoker.setAttribute('popovertarget', 'focus-return1-p'); 118 invoker.setAttribute('popovertargetaction', 'show'); 119 hideButton.setAttribute('popovertarget', 'focus-return1-p'); 120 hideButton.setAttribute('popovertargetaction', 'hide'); 121 t.add_cleanup(() => { 122 invoker.removeAttribute('popovertarget'); 123 invoker.removeAttribute('popovertargetaction'); 124 hideButton.removeAttribute('popovertarget'); 125 hideButton.removeAttribute('popovertargetaction'); 126 }); 127 await testPopoverFocusReturn1(); 128 }, "Popover focus returns when popover is hidden by invoker with popovertarget invocation"); 129 promise_test(async t => { 130 const invoker = document.querySelector('#focus-return1>button'); 131 const popover = document.querySelector('#focus-return1>[popover]'); 132 const hideButton = popover.querySelector('button'); 133 invoker.setAttribute('commandfor', 'focus-return1-p'); 134 invoker.setAttribute('command', 'show-popover'); 135 hideButton.setAttribute('commandfor', 'focus-return1-p'); 136 hideButton.setAttribute('command', 'hide-popover'); 137 t.add_cleanup(() => { 138 invoker.removeAttribute('commandfor'); 139 invoker.removeAttribute('command'); 140 hideButton.removeAttribute('commandfor'); 141 hideButton.removeAttribute('command'); 142 }); 143 await testPopoverFocusReturn1(); 144 }, "Popover focus returns when popover is hidden by invoker with commandfor invocation"); 145 promise_test(async t => { 146 const invoker = document.querySelector('#focus-return1>button'); 147 const popover = document.querySelector('#focus-return1>[popover]'); 148 const hideButton = popover.querySelector('button'); 149 const invokerClick = () => { 150 popover.showPopover({ source: invoker }); 151 }; 152 invoker.addEventListener('click', invokerClick); 153 const hideButtonClick = () => { 154 popover.hidePopover(); 155 }; 156 hideButton.addEventListener('click', hideButtonClick); 157 t.add_cleanup(() => { 158 invoker.removeEventListener('click', invokerClick); 159 hideButton.removeEventListener('click', hideButtonClick); 160 }); 161 await testPopoverFocusReturn1(); 162 }, "Popover focus returns when popover is hidden by invoker with imperative invocation"); 163 </script> 164 165 <div id=focus-return2> 166 <button tabindex="0">Toggle popover</button> 167 <div popover id=focus-return2-p>Popover with <button tabindex="0">focusable element</button></div> 168 <span tabindex=0>Other focusable element</span> 169 </div> 170 <script> 171 async function testPopoverFocusReturn2() { 172 const invoker = document.querySelector('#focus-return2>button'); 173 const popover = document.querySelector('#focus-return2>[popover]'); 174 const otherElement = document.querySelector('#focus-return2>span'); 175 invoker.focus(); // Make sure button is focused. 176 assert_equals(document.activeElement,invoker); 177 invoker.click(); // Activate the invoker 178 assert_true(popover.matches(':popover-open'), 'popover should be invoked by invoker'); 179 assert_equals(document.activeElement,invoker,'invoker should still be focused'); 180 await sendTab(); 181 assert_equals(document.activeElement,popover.querySelector('button'),'next up is the popover'); 182 await sendTab(); 183 assert_equals(document.activeElement,otherElement,'next focus stop is outside the popover'); 184 await sendEscape(); // Close the popover via ESC 185 assert_false(popover.matches(':popover-open'), 'popover should be hidden'); 186 assert_equals(document.activeElement,otherElement,'focus does not move because it was not inside the popover'); 187 } 188 promise_test(async t => { 189 const invoker = document.querySelector('#focus-return2>button'); 190 invoker.setAttribute('popovertarget', 'focus-return2-p'); 191 t.add_cleanup(() => { 192 invoker.removeAttribute('popovertarget'); 193 }); 194 await testPopoverFocusReturn2(); 195 }, "Popover focus only returns to invoker when focus is within the popover with popovertarget invocation"); 196 promise_test(async t => { 197 const invoker = document.querySelector('#focus-return2>button'); 198 invoker.setAttribute('command', 'toggle-popover'); 199 invoker.setAttribute('commandfor', 'focus-return2-p'); 200 t.add_cleanup(() => { 201 invoker.removeAttribute('command'); 202 invoker.removeAttribute('commandfor'); 203 }); 204 await testPopoverFocusReturn2(); 205 }, "Popover focus only returns to invoker when focus is within the popover with command/commandfor invocation"); 206 promise_test(async t => { 207 const invoker = document.querySelector('#focus-return2>button'); 208 const popover = document.querySelector('#focus-return2>[popover]'); 209 const invokerClick = () => { 210 popover.togglePopover({ source: invoker }); 211 }; 212 invoker.addEventListener('click', invokerClick); 213 t.add_cleanup(() => { 214 invoker.removeEventListener('click', invokerClick); 215 }); 216 await testPopoverFocusReturn2(); 217 }, "Popover focus only returns to invoker when focus is within the popover with imperative invocation"); 218 </script>