interestfor-keyboard-behavior.tentative.html (11017B)
1 <!DOCTYPE html> 2 <meta charset="utf-8" /> 3 <meta name="timeout" content="long"> 4 <link rel="author" href="mailto:masonf@chromium.org"> 5 <link rel="help" href="https://open-ui.org/components/interest-invokers.explainer/" /> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="/resources/testdriver.js"></script> 9 <script src="/resources/testdriver-actions.js"></script> 10 <script src="/resources/testdriver-vendor.js"></script> 11 <script src="resources/invoker-utils.js"></script> 12 <script src="/html/semantics/popovers/resources/popover-utils.js"></script> 13 14 <button data-testcase="<button>" interestfor=target>Button</button> 15 16 <a data-testcase="<a>" href=foo interestfor=target>Link</a> 17 18 <img src="/images/blue.png" usemap="#map"> 19 <map id=map> 20 <area data-testcase="<area>" interestfor=target href="/" shape=default> 21 </map> 22 23 <svg viewBox="0 0 100 100" style="width: 100px" xmlns="http://www.w3.org/2000/svg"> 24 <a data-testcase="SVG <a>" href=foo interestfor=target> 25 <text x=50 y=90>SVG A</text> 26 </a> 27 </svg> 28 29 <div id=target popover>Popover</div> 30 <button id="otherbutton">Other button</button> 31 <button id="another" interestfor=anothertarget>Another Button</button> 32 <div id=anothertarget popover>Another Popover</div> 33 34 <style> 35 [interestfor] { 36 interest-delay: 0s; 37 } 38 [interestfor].longhide { 39 interest-delay-end: 10000s; 40 } 41 </style> 42 43 <script> 44 const allInterestForElements = document.querySelectorAll('[data-testcase]'); 45 assert_true(allInterestForElements.length > 0); 46 47 function verifyInterest(onlyElements,description) { 48 if (!(onlyElements instanceof Array)) { 49 onlyElements = [onlyElements]; 50 } 51 [...allInterestForElements, another].forEach(el => { 52 const expectInterest = onlyElements.includes(el); 53 assert_equals(el.matches(':interest-source'),expectInterest,`${description}, element ${el.dataset.testcase} should ${expectInterest ? "" : "NOT "}have interest`); 54 }) 55 } 56 allInterestForElements.forEach(el => { 57 const description = el.dataset.testcase; 58 promise_test(async function (t) { 59 t.add_cleanup(() => otherbutton.focus()); 60 target.hidePopover(); // Just in case 61 await focusOn(el); 62 assert_equals(document.activeElement,el,'Elements should all be focusable'); 63 assert_true(target.matches(':popover-open'),'Focusing should trigger interest'); 64 verifyInterest(el,`After show interest in ${description}`); 65 await focusOn(otherbutton); 66 assert_not_equals(document.activeElement,el); 67 assert_false(target.matches(':popover-open'),'Blurring should trigger lose interest'); 68 verifyInterest(undefined,`After lose interest in ${description}`); 69 },`Basic keyboard focus behavior, ${description}`); 70 71 promise_test(async function (t) { 72 t.add_cleanup(() => otherbutton.focus()); 73 target.hidePopover(); // Just in case 74 await focusOn(el); 75 assert_true(target.matches(':popover-open'),'Focusing should trigger interest'); 76 verifyInterest(el,`After show interest in ${description}`); 77 await sendLoseInterestHotkey(); 78 assert_false(target.matches(':popover-open'),'Pressing lose interest hot key should trigger lose interest'); 79 verifyInterest(undefined,`After lose interest in ${description}`); 80 await focusOn(otherbutton); 81 assert_not_equals(document.activeElement,el); 82 assert_false(target.matches(':popover-open'),'Blurring should do nothing at this point'); 83 verifyInterest(undefined,`After blurring ${description}`); 84 },`Lose interest hot key behavior, ${description}`); 85 86 promise_test(async function (t) { 87 t.add_cleanup(() => otherbutton.focus()); 88 // Ensure blurring doesn't immediately lose interest: 89 el.classList.add('longhide'); 90 t.add_cleanup(() => (el.classList.remove('longhide'))); 91 target.hidePopover(); // Just in case 92 await focusOn(el); 93 assert_true(target.matches(':popover-open'),'Focusing should trigger interest'); 94 verifyInterest(el,`After show interest in ${description}`); 95 await focusOn(otherbutton); 96 assert_not_equals(document.activeElement,el); 97 assert_true(target.matches(':popover-open'),'Blurring should not immediately lose interest'); 98 verifyInterest(el,`After blurring ${description}`); 99 // Send lose interest hot key to the other button (not the invoker): 100 await sendLoseInterestHotkey(); 101 assert_false(target.matches(':popover-open'),'Pressing lose interest hot key should trigger lose interest'); 102 verifyInterest(undefined,`After lose interest in ${description}`); 103 },`Lose interest hot key behavior with element not focused, ${description}`); 104 105 promise_test(async function (t) { 106 t.add_cleanup(() => otherbutton.focus()); 107 target.hidePopover(); // Just in case 108 target.addEventListener('interest', (e) => e.preventDefault(), {once: true}); 109 await focusOn(el); 110 assert_false(target.matches(':popover-open')); 111 verifyInterest(undefined,`Nothing has interest, ${description}`); 112 }, `canceling the interest event stops behavior, ${description}`); 113 114 let events = []; 115 function addListeners(t,element) { 116 const signal = t.get_signal(); 117 element.addEventListener('interest',(e) => events.push(`${e.target.id} interest`),{signal}); 118 element.addEventListener('loseinterest',(e) => events.push(`${e.target.id} loseinterest (${e.cancelable ? 'cancelable' : 'not cancelable'})`),{signal}); 119 } 120 promise_test(async function (t) { 121 t.add_cleanup(() => otherbutton.focus()); 122 target.hidePopover(); // Just in case 123 anothertarget.hidePopover(); // Just in case 124 events = []; 125 addListeners(t,target); 126 addListeners(t,anothertarget); 127 await focusOn(el); 128 assert_array_equals(events,['target interest'],'first hotkey'); 129 verifyInterest(el,`After show interest in ${description}`); 130 await focusOn(another); 131 assert_array_equals(events,['target interest','target loseinterest (cancelable)','anothertarget interest'], 132 'showing interest in another trigger should lose interest in the first, then gain interest in second'); 133 verifyInterest(another,`After show interest in ${another.id}`); 134 await sendLoseInterestHotkey(); 135 assert_array_equals(events,['target interest','target loseinterest (cancelable)','anothertarget interest','anothertarget loseinterest (not cancelable)']); 136 verifyInterest(undefined,`After lose interest in ${another.id}`); 137 assert_false(target.matches(':popover-open')); 138 assert_false(anothertarget.matches(':popover-open')); 139 }, `Showing interest in a second element loses interest in the first, ${description}`); 140 141 promise_test(async function (t) { 142 t.add_cleanup(() => otherbutton.focus()); 143 target.hidePopover(); // Just in case 144 anothertarget.hidePopover(); // Just in case 145 events = []; 146 addListeners(t,target); 147 addListeners(t,anothertarget); 148 await focusOn(el); 149 assert_array_equals(events,['target interest'],'setup'); 150 verifyInterest(el,`After show interest in ${description}`); 151 const signal = t.get_signal(); 152 let shouldCancelLoseInterest = true; 153 target.addEventListener('loseinterest',(e) => { 154 if (shouldCancelLoseInterest) { 155 e.preventDefault(); 156 } 157 },{signal}); 158 await focusOn(another); 159 assert_array_equals(events,['target interest','target loseinterest (cancelable)','anothertarget interest','target loseinterest (cancelable)'], 160 'the loseinterest listener should fire but get cancelled, anothertarget should still get interest, and that should close the first target popover firing another loseinterest'); 161 events = []; 162 verifyInterest([el,another],`${description} should still have interest because loseinterest was cancelled`); 163 assert_false(target.matches(':popover-open'),'anothertarget popover opens, closing target'); 164 assert_true(anothertarget.matches(':popover-open')); 165 await sendLoseInterestHotkey(); 166 assert_array_equals(events,['anothertarget loseinterest (not cancelable)', 'target loseinterest (not cancelable)'],'Lose interest hot key loses interest in all elements'); 167 assert_false(target.matches(':popover-open')); 168 assert_false(anothertarget.matches(':popover-open')); 169 verifyInterest(undefined,`Nothing has interest now`); 170 }, `Canceling loseinterest caused by keyboard-gained interest cancels interest, ${description}`); 171 }); 172 </script> 173 174 <button id="esc_invoker1" class="longhide" interestfor="esc_target1">ESC Invoker 1</button> 175 <div id="esc_target1">Non-popover target for ESC test</div> 176 <button id="esc_invoker2" class="longhide" interestfor="esc_target2">ESC Invoker 2</button> 177 <div id="esc_target2">Non-popover target for ESC test</div> 178 <button id="esc_invoker3" class="longhide" interestfor="esc_target3">ESC Invoker 3</button> 179 <div id="esc_target3">Non-popover target for ESC test</div> 180 181 <script> 182 promise_test(async function (t) { 183 const invoker1 = document.getElementById('esc_invoker1'); 184 const target1 = document.getElementById('esc_target1'); 185 const invoker2 = document.getElementById('esc_invoker2'); 186 const target2 = document.getElementById('esc_target2'); 187 const invoker3 = document.getElementById('esc_invoker3'); 188 const target3 = document.getElementById('esc_target3'); 189 const otherbutton = document.getElementById('otherbutton'); 190 t.add_cleanup(() => otherbutton.focus()); 191 192 let events = []; 193 const signal = t.get_signal(); 194 [target1, target2, target3].forEach(target => { 195 target.addEventListener('interest',(e) => events.push(`${e.source.id} interest`),{signal}); 196 target.addEventListener('loseinterest',(e) => events.push(`${e.source.id} loseinterest`),{signal}); 197 // These loseinterest events should not be cancelable: 198 target.addEventListener('loseinterest',(e) => e.preventDefault(),{signal}); 199 }); 200 201 // Invoke them in non-tree order: 202 await focusOn(invoker1); 203 await focusOn(invoker3); 204 await focusOn(invoker2); 205 assert_array_equals(events, 206 ['esc_invoker1 interest','esc_invoker3 interest','esc_invoker2 interest'], 207 'Events after gaining interest'); 208 events = []; 209 210 // Hit ESC once, while focused on the body 211 document.body.focus(); 212 await waitForRender(); 213 assert_true(invoker1.matches(':interest-source'), 'invoker1 should still have interest'); 214 assert_true(invoker3.matches(':interest-source'), 'invoker3 should still have interest'); 215 assert_true(invoker2.matches(':interest-source'), 'invoker2 should still have interest'); 216 const kEscape = '\uE00C'; 217 await new test_driver.Actions() 218 .keyDown(kEscape) 219 .keyUp(kEscape) 220 .send(); 221 await waitForRender(); 222 assert_false(invoker2.matches(':interest-source'), 'invoker2 should lose interest'); 223 assert_false(invoker1.matches(':interest-source'), 'invoker1 should lose interest'); 224 assert_false(invoker3.matches(':interest-source'), 'invoker3 should lose interest'); 225 assert_array_equals(events, [ 226 'esc_invoker2 loseinterest', 'esc_invoker3 loseinterest', 'esc_invoker1 loseinterest'], 227 'ESC should lose interest in all invokers, in reverse order'); 228 }, 'ESC key dismisses all interest invokers'); 229 </script>