popover-focus-5.html (9193B)
1 <!doctype html> 2 <meta charset="utf-8" /> 3 <title>Popover focus behaviors in slot elements</title> 4 <meta name="timeout" content="long" /> 5 <link rel="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" /> 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/popover-utils.js"></script> 12 13 <meta name="variant" content="?commandforelement" /> 14 <meta name="variant" content="?popovertargetelement" /> 15 <meta name="variant" content="?commandfor" /> 16 <meta name="variant" content="?imperative" /> 17 18 <div data-candidate="form controls inside" data-count="2"> 19 <button tabindex="0" command="toggle-popover">Toggle popover</button> 20 <div popover id="popover1"> 21 <input tabindex="0" type="text" data-expected="1" /> 22 <input tabindex="0" type="text" data-expected="2" /> 23 <button tabindex="0" command="hide-popover" data-expected="close"> 24 Close popover 25 </button> 26 </div> 27 </div> 28 <button tabindex="0" data-expected="after"> 29 This button is where focus should land after traversing this popover 30 </button> 31 32 <div data-candidate="a details element inside" data-count="1"> 33 <button tabindex="0" command="toggle-popover">Toggle popover</button> 34 <div popover id="popover2"> 35 <details> 36 <summary tabindex="0" data-expected="1">A details element</summary> 37 </details> 38 <button tabindex="0" command="hide-popover" data-expected="close"> 39 Close popover 40 </button> 41 </div> 42 </div> 43 <button tabindex="0" data-expected="after"> 44 This button is where focus should land after traversing this popover 45 </button> 46 47 <div 48 data-candidate="a details element inside, and the after button adjacent to invoker" 49 data-count="1" 50 > 51 <button tabindex="0" command="toggle-popover">Toggle popover</button> 52 <button tabindex="0" data-expected="after"> 53 This button is where focus should land after traversing this popover 54 </button> 55 <div popover id="popover3"> 56 <details data-expected="1">A details element</details> 57 <button tabindex="0" command="hide-popover" data-expected="close"> 58 Close popover 59 </button> 60 </div> 61 </div> 62 63 <div 64 data-candidate="a custom-element with delegatesfocus inside" 65 data-count="1" 66 > 67 <button tabindex="0" command="toggle-popover">Toggle popover</button> 68 <div popover id="popover4"> 69 <my-element data-expected="1"> 70 <template shadowrootmode="open" shadowrootdelegatesfocus> 71 <button tabindex="0" data-expected="shadow-1"></button> 72 </template> 73 </my-element> 74 <button tabindex="0" command="hide-popover" data-expected="close"> 75 Close popover 76 </button> 77 </div> 78 </div> 79 <button tabindex="0" data-expected="after"> 80 This button is where focus should land after traversing popover 81 </button> 82 83 <div 84 data-candidate="a custom-element with a slotted button inside" 85 data-count="1" 86 > 87 <button tabindex="0" command="toggle-popover">Toggle popover</button> 88 <div popover id="popover5"> 89 <my-element> 90 <template shadowrootmode="open"> 91 <slot></slot> 92 </template> 93 <button tabindex="0" data-expected="1"></button> 94 </my-element> 95 <button tabindex="0" command="hide-popover" data-expected="close"> 96 Close popover 97 </button> 98 </div> 99 </div> 100 <button tabindex="0" data-expected="after"> 101 This button is where focus should land after traversing popover 102 </button> 103 104 <div 105 data-candidate="custom-element with a slotted button, followed by a details element, where the after button is adjacent to the invoker" 106 data-count="2" 107 > 108 <button tabindex="0" command="toggle-popover">Toggle popover</button> 109 <button tabindex="0" data-expected="after"> 110 This button is where focus should land after traversing popover 111 </button> 112 <div popover id="popover6"> 113 <my-element data-expected="2"> 114 <template shadowrootmode="open"> 115 <slot></slot> 116 <details> 117 <summary tabindex="0" data-expected="shadow-2">A details element</summary> 118 </details> 119 </template> 120 <button tabindex="0" data-expected="1"></button> 121 </my-element> 122 <button tabindex="0" command="hide-popover" data-expected="close"> 123 Close popover 124 </button> 125 </div> 126 </div> 127 128 <script> 129 // The active element might be an element within the expected focus candidate,e.g. a summary 130 // inside a details, we should traverse the tree to find the nearest `[data-expected]` element 131 // to ensure we're not failing a test for chosing to focus an interior element. 132 function getExpectedValue() { 133 return document.activeElement?.closest('[data-expected]')?.getAttribute("data-expected") 134 } 135 136 async function testCandidate(el, style, signal) { 137 const count = parseInt(el.getAttribute("data-count")); 138 const invoker = el.querySelector("button:first-child"); 139 const popover = el.querySelector("[popover]"); 140 const popoverClose = el.querySelector('[data-expected="close"]'); 141 assert_greater_than_equal(count, 0, "test candidate had an invalid count"); 142 assert_not_equals( 143 invoker, 144 null, 145 "could not find invoker in test candidate", 146 ); 147 assert_not_equals( 148 popover, 149 null, 150 "could not find popover in test candidate", 151 ); 152 assert_not_equals( 153 popoverClose, 154 null, 155 "could not find popover close button in test candidate", 156 ); 157 switch (style) { 158 case "popovertarget": 159 invoker.setAttribute("popovertarget", popover.id); 160 popoverClose.setAttribute("popovertarget", popover.id); 161 break; 162 case "popovertargetelement": 163 invoker.popoverTargetElement = popover; 164 popoverClose.popoverTargetElement = popover; 165 break; 166 case "commandfor": 167 invoker.setAttribute("commandfor", popover.id); 168 popoverClose.setAttribute("commandfor", popover.id); 169 break; 170 case "commandforelement": 171 invoker.commandForElement = popover; 172 popoverClose.commandForElement = popover; 173 break; 174 case "imperative": 175 invoker.addEventListener( 176 "click", 177 () => { 178 popover.togglePopover({ source: invoker }); 179 }, 180 { signal }, 181 ); 182 popoverClose.addEventListener( 183 "click", 184 () => { 185 popover.hidePopover({ source: invoker }); 186 }, 187 { signal }, 188 ); 189 break; 190 default: 191 assert_unreached(); 192 } 193 invoker.focus(); 194 assert_equals(document.activeElement, invoker); 195 invoker.click(); 196 assert_true( 197 popover.matches(":popover-open"), 198 "popover should be invoked by invoker", 199 ); 200 assert_equals( 201 document.activeElement, 202 invoker, 203 "invoker should still be focused", 204 ); 205 for (let i = 1; i <= count; i += 1) { 206 await sendTab(); 207 assert_equals( 208 getExpectedValue(), 209 String(i), 210 `tab press should move to active element ${i}`, 211 ); 212 const shadowActive = document.activeElement?.shadowRoot?.activeElement; 213 if (shadowActive) { 214 const expected = `shadow-${i}`; 215 assert_equals( 216 shadowActive.getAttribute("data-expected"), 217 expected, 218 `tab press should move to active element ${expected} in shadow dom`, 219 ); 220 } 221 } 222 await sendTab(); 223 assert_equals( 224 getExpectedValue(), 225 "close", 226 "tab press should move to close popover button", 227 ); 228 await sendShiftTab(); 229 assert_equals( 230 getExpectedValue(), 231 String(count), 232 "shift+tab should move back to last active element", 233 ); 234 await sendTab(); 235 assert_equals( 236 document.activeElement, 237 popoverClose, 238 "tab should move forward to re-land on close popover again", 239 ); 240 await sendTab(); 241 assert_equals( 242 getExpectedValue(), 243 "after", 244 "tab should move forward to land on the button after the popover contents", 245 ); 246 await sendShiftTab(); 247 popoverClose.click(); 248 assert_equals( 249 document.activeElement, 250 invoker, 251 "popover now closed - focus should have returned to initial invoker", 252 ); 253 } 254 255 const style = window.location.search.substring(1) || "popovertarget"; 256 for (const candidate of document.querySelectorAll("[data-candidate]")) { 257 promise_test( 258 async (t) => { 259 const controller = new AbortController(); 260 t.add_cleanup(() => { 261 controller.abort(); 262 for (const el of document.querySelectorAll( 263 ":is([popovertarget],[commandfor],[disabled],[tabindex])", 264 )) { 265 el.removeAttribute("popovertarget"); 266 el.removeAttribute("commandfor"); 267 el.removeAttribute("disabled"); 268 el.removeAttribute("tabindex"); 269 } 270 for (const el of document.querySelectorAll(":popover-open")) { 271 el.hidePopover(); 272 } 273 }); 274 await testCandidate(candidate, style); 275 }, 276 `Focusing elements inside a popover with ${candidate.getAttribute("data-candidate")}, using ${style}`, 277 ); 278 } 279 </script>