toggle-events.html (11406B)
1 <!doctype html> 2 <link rel="author" href="mailto:jarhar@chromium.org" /> 3 <link rel="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" /> 4 <link rel="help" href="https://github.com/whatwg/html/pull/10091" /> 5 <link rel="help" href="https://github.com/whatwg/html/issues/9733" /> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 9 <dialog id="mydialog">dialog</dialog> 10 11 <script> 12 ["show", "showModal"].forEach((methodName) => { 13 const waitForTick = () => new Promise(resolve => step_timeout(resolve, 0)); 14 15 promise_test(async () => { 16 let openingBeforetoggle = null; 17 let openingToggle = null; 18 19 mydialog.addEventListener( 20 "beforetoggle", 21 (event) => { 22 assert_equals( 23 event.oldState, 24 "closed", 25 'Opening beforetoggle should have oldState be "closed".', 26 ); 27 assert_equals( 28 event.newState, 29 "open", 30 'Opening beforetoggle should have newState be "open".', 31 ); 32 assert_false( 33 mydialog.hasAttribute("open"), 34 "Opening beforetoggle should fire before open attribute is added.", 35 ); 36 openingBeforetoggle = event; 37 }, 38 { once: true }, 39 ); 40 41 mydialog.addEventListener( 42 "toggle", 43 (event) => { 44 assert_equals( 45 event.oldState, 46 "closed", 47 'Opening toggle should have oldState be "closed".', 48 ); 49 assert_equals( 50 event.newState, 51 "open", 52 'Opening toggle should have newState be "open".', 53 ); 54 assert_true( 55 mydialog.hasAttribute("open"), 56 "Opening toggle should fire after open attribute is added.", 57 ); 58 openingToggle = event; 59 }, 60 { once: true }, 61 ); 62 63 mydialog[methodName](); 64 assert_true( 65 !!openingBeforetoggle, 66 "Opening beforetoggle should fire synchronously.", 67 ); 68 assert_false( 69 !!openingToggle, 70 "Opening toggle should fire asynchronously.", 71 ); 72 73 await waitForTick(); 74 assert_true( 75 !!openingToggle, 76 "Opening toggle should have fired after tick.", 77 ); 78 79 let closingBeforetoggle = null; 80 let closingToggle = null; 81 82 mydialog.addEventListener( 83 "beforetoggle", 84 (event) => { 85 assert_equals( 86 event.oldState, 87 "open", 88 'Closing beforetoggle should have oldState be "open".', 89 ); 90 assert_equals( 91 event.newState, 92 "closed", 93 'Closing beforetoggle should have newState be "closed".', 94 ); 95 assert_true( 96 mydialog.hasAttribute("open"), 97 "Closing beforetoggle should fire before open attribute is removed.", 98 ); 99 closingBeforetoggle = event; 100 }, 101 { once: true }, 102 ); 103 mydialog.addEventListener( 104 "toggle", 105 (event) => { 106 assert_equals( 107 event.oldState, 108 "open", 109 'Closing toggle should have oldState be "open".', 110 ); 111 assert_equals( 112 event.newState, 113 "closed", 114 'Closing toggle should have newState be "closed".', 115 ); 116 assert_false( 117 mydialog.hasAttribute("open"), 118 "Closing toggle should fire after open attribute is removed.", 119 ); 120 closingToggle = event; 121 }, 122 { once: true }, 123 ); 124 125 mydialog.close(); 126 assert_true( 127 !!closingBeforetoggle, 128 "Closing beforetoggle should fire synchronously.", 129 ); 130 assert_false( 131 !!closingToggle, 132 "Closing toggle should fire asynchronously.", 133 ); 134 135 await waitForTick(); 136 assert_true( 137 !!closingToggle, 138 "Closing toggle should have fired after tick.", 139 ); 140 }, `dialog.${methodName}() should fire beforetoggle and toggle events.`); 141 142 promise_test(async () => { 143 let openingBeforetoggle = null; 144 let openingToggle = null; 145 146 mydialog.addEventListener( 147 "beforetoggle", 148 (event) => { 149 event.preventDefault(); 150 openingBeforetoggle = event; 151 }, 152 { once: true }, 153 ); 154 155 mydialog.addEventListener( 156 "toggle", 157 (event) => { 158 openingToggle = event; 159 }, 160 { once: true }, 161 ); 162 163 mydialog[methodName](); 164 assert_true( 165 !!openingBeforetoggle, 166 "Opening beforetoggle should fire synchronously.", 167 ); 168 assert_false( 169 !!openingToggle, 170 "Opening toggle should fire.", 171 ); 172 173 await waitForTick(); 174 assert_false( 175 !!openingToggle, 176 "Opening toggle should still not have fired.", 177 ); 178 179 assert_false(mydialog.open, 'dialog should not be open'); 180 }, `dialog.${methodName}() should fire cancelable beforetoggle which does not open dialog if canceled`); 181 182 promise_test(async () => { 183 let openCloseToggleEvent = null; 184 mydialog.addEventListener( 185 "toggle", 186 (event) => { 187 assert_equals( 188 event.oldState, 189 "closed", 190 'Opening and closing dialog should result in oldState being "closed".', 191 ); 192 assert_equals( 193 event.newState, 194 "closed", 195 'Opening and closing dialog should result in newState being "closed".', 196 ); 197 assert_false( 198 mydialog.hasAttribute("open"), 199 "Opening and closing dialog should result in open attribute being removed.", 200 ); 201 openCloseToggleEvent = event; 202 }, 203 { once: true }, 204 ); 205 206 mydialog[methodName](); 207 assert_false( 208 !!openCloseToggleEvent, 209 "Toggle event should not fire synchronously.", 210 ); 211 mydialog.close(); 212 await waitForTick(); 213 assert_true( 214 !!openCloseToggleEvent, 215 "Toggle event should have fired after tick.", 216 ); 217 218 mydialog[methodName](); 219 await waitForTick(); 220 221 let closeOpenToggleEvent = null; 222 mydialog.addEventListener( 223 "toggle", 224 (event) => { 225 assert_equals( 226 event.oldState, 227 "open", 228 'Closing and opening dialog should result in oldState being "open".', 229 ); 230 assert_equals( 231 event.newState, 232 "open", 233 'Closing and opening dialog should result in newState being "open".', 234 ); 235 assert_true( 236 mydialog.hasAttribute("open"), 237 "Closing and opening dialog should result in open attribute being added.", 238 ); 239 closeOpenToggleEvent = event; 240 }, 241 { once: true }, 242 ); 243 244 mydialog.close(); 245 assert_false( 246 !!closeOpenToggleEvent, 247 "Toggle event should not fire synchronously.", 248 ); 249 mydialog[methodName](); 250 await waitForTick(); 251 assert_true( 252 !!closeOpenToggleEvent, 253 "Toggle event should have fired after tick.", 254 ); 255 256 // Clean up for the next test. 257 mydialog.close(); 258 await waitForTick(); 259 }, `dialog.${methodName}() should coalesce asynchronous toggle events.`); 260 261 promise_test(async (t) => { 262 let attributeChanges = 0; 263 const mo = new MutationObserver((records) => { 264 attributeChanges += records.length; 265 }); 266 mo.observe(mydialog, { attributeFilter: ['open'] }); 267 t.add_cleanup(() => { 268 mo.disconnect(); 269 }); 270 mydialog.addEventListener("beforetoggle", () => { 271 mydialog[methodName](); 272 }, { once: true }); 273 274 mydialog[methodName](); 275 assert_true(mydialog.open, "Dialog is open"); 276 await waitForTick(); 277 mo.takeRecords(); 278 assert_equals(attributeChanges, 1, "Should have set open once"); 279 280 attributeChanges = 0; 281 mydialog.addEventListener("beforetoggle", () => { 282 mydialog.close(); 283 }, { once: true }); 284 285 mydialog.close(); 286 assert_false(mydialog.open, "Dialog is closed"); 287 await waitForTick(); 288 mo.takeRecords(); 289 assert_equals(attributeChanges, 1, "Should have removed open once"); 290 }, `dialog.${methodName}() should not double-set open/close if beforetoggle re-opens`); 291 292 promise_test(async (t) => { 293 const abortController = new AbortController(); 294 const signal = abortController.signal; 295 const mydialog = document.getElementById("mydialog"); 296 t.add_cleanup(() => { 297 abortController.abort(); 298 mydialog.close(); 299 document.body.prepend(mydialog); 300 }); 301 mydialog.addEventListener("beforetoggle", () => { 302 mydialog.remove(); 303 }, { once: true }); 304 let toggleEventCounter = 0; 305 mydialog.addEventListener( 306 "toggle", 307 (event) => { 308 toggleEventCounter += 1; 309 }, 310 { signal } 311 ); 312 313 mydialog[methodName](); 314 assert_false(mydialog.isConnected, "Dialog is not connected"); 315 if (methodName == 'show') { 316 assert_true(mydialog.open, "Dialog did open"); 317 } else { 318 assert_false(mydialog.open, "Dialog did not open"); 319 assert_false(mydialog.matches(':modal'), "Dialog is not modal"); 320 } 321 await waitForTick(); 322 if (methodName == 'show') { 323 assert_equals(toggleEventCounter, 1, "toggle event was fired"); 324 } else { 325 assert_equals(toggleEventCounter, 0, "toggle event not fired"); 326 } 327 // Clean up for the next test. 328 document.body.prepend(mydialog); 329 mydialog.close(); 330 await waitForTick(); 331 }, `dialog.${methodName}() should not open if beforetoggle removes`); 332 333 promise_test(async (t) => { 334 assert_true(document.body.contains(mydialog),'still in the document'); 335 assert_false(mydialog.open,'initially closed'); 336 const abortController = new AbortController(); 337 const signal = abortController.signal; 338 t.add_cleanup(() => { 339 try { mydialog.hidePopover(); } catch {} 340 try { mydialog.close(); } catch {} 341 mydialog.removeAttribute('popover'); 342 abortController.abort(); 343 waitForTick(); // Note that cleanups can't await 344 }); 345 mydialog.setAttribute('popover', ''); 346 mydialog.addEventListener("beforetoggle", () => { 347 mydialog.showPopover(); 348 }, { once: true }); 349 let toggleEventCounter = 0; 350 mydialog.addEventListener( 351 "toggle", 352 (event) => { 353 toggleEventCounter += 1; 354 }, 355 { signal } 356 ); 357 358 mydialog[methodName](); 359 if (methodName == 'show') { 360 assert_true(mydialog.open, "Dialog did open"); 361 } else { 362 assert_false(mydialog.open, "Dialog did not open"); 363 assert_false(mydialog.matches(':modal'), "Dialog is not modal"); 364 } 365 await waitForTick(); 366 if (methodName == 'show') { 367 assert_equals(toggleEventCounter, 2, "toggle event was fired for show+showPopover"); 368 } else { 369 assert_equals(toggleEventCounter, 1, "toggle event was fired for showPopover"); 370 } 371 // Clean up for the next test. 372 mydialog.close(); 373 await waitForTick(); 374 }, `dialog.${methodName}() should not open if beforetoggle calls showPopover`); 375 }); 376 </script>