popover-focus-2.html (7855B)
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 <meta name=variant content=?test1> 15 <meta name=variant content=?test2> 16 <meta name=variant content=?test3> 17 18 <div id=fixup> 19 <button id=button1 tabindex="0">Button1</button> 20 <div popover id=popover0 tabindex="0" style="top:300px"> 21 </div> 22 <div popover id=popover1 style="top:100px"> 23 <button id=inside_popover1 tabindex="0">Inside1</button> 24 <button id=invoker2 tabindex="0">Nested Invoker 2</button> 25 <button id=inside_popover2 tabindex="0">Inside2</button> 26 </div> 27 <button id=button2 tabindex="0">Button2</button> 28 <div popover id=popover_no_invoker tabindex="0" style="top:300px"></div> 29 <button id=invoker0 tabindex="0">Invoker0</button> 30 <button id=invoker1 tabindex="0">Invoker1</button> 31 <button id=button3 tabindex="0">Button3</button> 32 <div popover id=popover2 style="top:200px"> 33 <button id=inside_popover3 tabindex="0">Inside3</button> 34 <button id=invoker3 tabindex="0">Nested Invoker 3</button> 35 </div> 36 <div popover id=popover3 style="top:300px"> 37 Non-focusable popover 38 </div> 39 <button id=button4 tabindex="0">Button4</button> 40 </div> 41 <style> 42 #fixup [popover] { 43 bottom:auto; 44 } 45 </style> 46 <script> 47 async function testPopoverFocusNavigation() { 48 button1.focus(); 49 assert_equals(document.activeElement,button1); 50 await sendTab(); 51 assert_equals(document.activeElement,button2,'Hidden popover should be skipped'); 52 await sendShiftTab(); 53 assert_equals(document.activeElement,button1,'Hidden popover should be skipped backwards'); 54 popover_no_invoker.showPopover(); 55 await sendTab(); 56 await sendTab(); 57 assert_equals(document.activeElement,popover_no_invoker,"Focusable popover that is opened without an invoker should get focused"); 58 await sendTab(); 59 assert_equals(document.activeElement,invoker0); 60 await sendEnter(); // Activate the invoker0 61 assert_true(popover0.matches(':popover-open'), 'popover0 should be invoked by invoker0'); 62 assert_equals(document.activeElement,invoker0,'Focus should not move when popover is shown'); 63 await sendTab(); 64 await sendEnter(); // Activate the invoker 65 assert_true(popover1.matches(':popover-open'), 'popover1 should be invoked by invoker1'); 66 assert_equals(document.activeElement,invoker1,'Focus should not move when popover is shown'); 67 await sendTab(); 68 // Make invoker1 non-focusable. 69 invoker1.disabled = true; 70 assert_equals(document.activeElement,inside_popover1,'Focus should move from invoker into the open popover'); 71 await sendTab(); 72 assert_equals(document.activeElement,invoker2,'Focus should move within popover'); 73 await sendShiftTab(); 74 await sendShiftTab(); 75 assert_equals(document.activeElement,invoker0,'Focus should not move back to invoker as it is non-focusable'); 76 // Reset invoker1 to focusable. 77 invoker1.disabled = false; 78 await verifyFocusOrder([button1, button2, invoker0, invoker1, inside_popover1, invoker2, inside_popover2, button3, button4],'set 1'); 79 invoker2.focus(); 80 await sendEnter(); // Activate the nested invoker 81 assert_true(popover2.matches(':popover-open'), 'popover2 should be invoked by nested invoker'); 82 assert_equals(document.activeElement,invoker2,'Focus should stay on the invoker'); 83 await sendTab(); 84 assert_equals(document.activeElement,inside_popover3,'Focus should move into nested popover'); 85 await sendTab(); 86 assert_equals(document.activeElement,invoker3); 87 await sendEnter(); // Activate the (empty) nested invoker 88 assert_true(popover3.matches(':popover-open'), 'popover3 should be invoked by nested invoker'); 89 assert_equals(document.activeElement,invoker3,'Focus should stay on the invoker'); 90 await sendTab(); 91 assert_equals(document.activeElement,inside_popover2,'Focus should skip popover without focusable content, going back to higher scope'); 92 await sendShiftTab(); 93 assert_equals(document.activeElement,invoker3,'Shift-tab from the higher scope should return to the lower scope'); 94 await sendTab(); 95 assert_equals(document.activeElement,inside_popover2); 96 await sendTab(); 97 assert_equals(document.activeElement,button3,'Focus should exit popovers'); 98 await sendTab(); 99 assert_equals(document.activeElement,button4,'Focus should skip popovers'); 100 button1.focus(); 101 await verifyFocusOrder([button1, button2, invoker0, invoker1, inside_popover1, invoker2, inside_popover3, invoker3, inside_popover2, button3, button4],'set 2'); 102 } 103 104 // This test is very slow. Variants are used to split it into pieces. 105 switch (window.location.search.substring(1)) { 106 case 'test1': 107 promise_test(async t => { 108 invoker0.setAttribute('popovertarget', 'popover0'); 109 invoker1.setAttribute('popovertarget', 'popover1'); 110 invoker2.setAttribute('popovertarget', 'popover2'); 111 invoker3.setAttribute('popovertarget', 'popover3'); 112 t.add_cleanup(() => { 113 invoker0.removeAttribute('popovertarget'); 114 invoker1.removeAttribute('popovertarget'); 115 invoker2.removeAttribute('popovertarget'); 116 invoker3.removeAttribute('popovertarget'); 117 }); 118 await testPopoverFocusNavigation(); 119 }, "Popover focus navigation with popovertarget invocation"); 120 break; 121 case 'test2': 122 promise_test(async t => { 123 invoker0.setAttribute('commandfor', 'popover0'); 124 invoker1.setAttribute('commandfor', 'popover1'); 125 invoker2.setAttribute('commandfor', 'popover2'); 126 invoker3.setAttribute('commandfor', 'popover3'); 127 invoker0.setAttribute('command', 'toggle-popover'); 128 invoker1.setAttribute('command', 'toggle-popover'); 129 invoker2.setAttribute('command', 'toggle-popover'); 130 invoker3.setAttribute('command', 'toggle-popover'); 131 t.add_cleanup(() => { 132 invoker0.removeAttribute('commandfor'); 133 invoker1.removeAttribute('commandfor'); 134 invoker2.removeAttribute('commandfor'); 135 invoker3.removeAttribute('commandfor'); 136 invoker0.removeAttribute('command'); 137 invoker1.removeAttribute('command'); 138 invoker2.removeAttribute('command'); 139 invoker3.removeAttribute('command'); 140 }); 141 await testPopoverFocusNavigation(); 142 }, "Popover focus navigation with command/commandfor invocation"); 143 break; 144 case 'test3': 145 promise_test(async t => { 146 const invoker0Click = () => { 147 popover0.togglePopover({ source: invoker0 }); 148 }; 149 invoker0.addEventListener('click', invoker0Click); 150 const invoker1Click = () => { 151 popover1.togglePopover({ source: invoker1 }); 152 }; 153 invoker1.addEventListener('click', invoker1Click); 154 const invoker2Click = () => { 155 popover2.togglePopover({ source: invoker2 }); 156 }; 157 invoker2.addEventListener('click', invoker2Click); 158 const invoker3Click = () => { 159 popover3.togglePopover({ source: invoker3 }); 160 }; 161 invoker3.addEventListener('click', invoker3Click); 162 t.add_cleanup(() => { 163 invoker0.removeEventListener('click', invoker0Click); 164 invoker1.removeEventListener('click', invoker1Click); 165 invoker2.removeEventListener('click', invoker2Click); 166 invoker3.removeEventListener('click', invoker3Click); 167 }); 168 await testPopoverFocusNavigation() 169 }, "Popover focus navigation with imperative invocation"); 170 break; 171 default: 172 assert_unreached('Invalid variant'); 173 } 174 </script>