on-scroll-behavior.tentative.html (22429B)
1 <!doctype html> 2 <meta charset="utf-8" /> 3 <meta name="author" title="Chromium" href="https://chromium.org" /> 4 <meta name="timeout" content="long" /> 5 <link rel="help" href="https://open-ui.org/components/invokers.explainer/" /> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="resources/invoker-utils.js"></script> 9 10 <style> 11 .scroll-container { 12 width: 200px; 13 height: 200px; 14 overflow: auto; 15 border: 1px solid black; 16 } 17 18 .scroll-content { 19 width: 1000px; 20 height: 1000px; 21 background: linear-gradient(to bottom right, red, blue); 22 } 23 24 .scroll-container-horizontal { 25 width: 200px; 26 height: 100px; 27 overflow-x: auto; 28 overflow-y: hidden; 29 border: 1px solid black; 30 } 31 32 .scroll-content-horizontal { 33 width: 1000px; 34 height: 100px; 35 background: linear-gradient(to right, red, blue); 36 } 37 38 .scroll-container-vertical { 39 width: 100px; 40 height: 200px; 41 overflow-y: auto; 42 overflow-x: hidden; 43 border: 1px solid black; 44 } 45 46 .scroll-content-vertical { 47 width: 100px; 48 height: 1000px; 49 background: linear-gradient(to bottom, red, blue); 50 } 51 52 .rtl { 53 direction: rtl; 54 } 55 56 .vertical-writing { 57 writing-mode: vertical-rl; 58 } 59 </style> 60 61 <!-- Basic scroll container --> 62 <div id="scrollcontainer" class="scroll-container"> 63 <div class="scroll-content"></div> 64 </div> 65 <button id="pageup" commandfor="scrollcontainer" command="page-up">Page Up</button> 66 <button id="pagedown" commandfor="scrollcontainer" command="page-down">Page Down</button> 67 <button id="pageleft" commandfor="scrollcontainer" command="page-left">Page Left</button> 68 <button id="pageright" commandfor="scrollcontainer" command="page-right">Page Right</button> 69 70 <!-- Horizontal only scroll container --> 71 <div id="horizontalcontainer" class="scroll-container-horizontal"> 72 <div class="scroll-content-horizontal"></div> 73 </div> 74 <button id="hpageleft" commandfor="horizontalcontainer" command="page-left">Page Left</button> 75 <button id="hpageright" commandfor="horizontalcontainer" command="page-right">Page Right</button> 76 77 <!-- Vertical only scroll container --> 78 <div id="verticalcontainer" class="scroll-container-vertical"> 79 <div class="scroll-content-vertical"></div> 80 </div> 81 <button id="vpageup" commandfor="verticalcontainer" command="page-up">Page Up</button> 82 <button id="vpagedown" commandfor="verticalcontainer" command="page-down">Page Down</button> 83 84 <!-- Logical direction tests --> 85 <div id="logicalcontainer" class="scroll-container"> 86 <div class="scroll-content"></div> 87 </div> 88 <button id="blockstart" commandfor="logicalcontainer" command="page-block-start">Block Start</button> 89 <button id="blockend" commandfor="logicalcontainer" command="page-block-end">Block End</button> 90 <button id="inlinestart" commandfor="logicalcontainer" command="page-inline-start">Inline Start</button> 91 <button id="inlineend" commandfor="logicalcontainer" command="page-inline-end">Inline End</button> 92 93 <!-- RTL container --> 94 <div id="rtlcontainer" class="scroll-container rtl"> 95 <div class="scroll-content"></div> 96 </div> 97 <button id="rtlinlinestart" commandfor="rtlcontainer" command="page-inline-start">Inline Start (RTL)</button> 98 <button id="rtlinlineend" commandfor="rtlcontainer" command="page-inline-end">Inline End (RTL)</button> 99 100 <!-- Vertical writing mode container --> 101 <div id="verticalwritingcontainer" class="scroll-container vertical-writing"> 102 <div class="scroll-content"></div> 103 </div> 104 <button id="vwblockstart" commandfor="verticalwritingcontainer" command="page-block-start">Block Start (VW)</button> 105 <button id="vwblockend" commandfor="verticalwritingcontainer" command="page-block-end">Block End (VW)</button> 106 107 <script> 108 function resetScrollPosition(container) { 109 container.scrollTop = 0; 110 container.scrollLeft = 0; 111 } 112 113 // Test page-up command 114 test(function (t) { 115 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 116 scrollcontainer.scrollTop = 400; 117 const initialScrollTop = scrollcontainer.scrollTop; 118 pageup.click(); 119 assert_less_than(scrollcontainer.scrollTop, initialScrollTop, "Scroll position should decrease"); 120 }, "page-up command scrolls up"); 121 122 // Test page-down command 123 test(function (t) { 124 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 125 const initialScrollTop = scrollcontainer.scrollTop; 126 pagedown.click(); 127 assert_greater_than(scrollcontainer.scrollTop, initialScrollTop, "Scroll position should increase"); 128 }, "page-down command scrolls down"); 129 130 // Test page-left command 131 test(function (t) { 132 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 133 scrollcontainer.scrollLeft = 400; 134 const initialScrollLeft = scrollcontainer.scrollLeft; 135 pageleft.click(); 136 assert_less_than(scrollcontainer.scrollLeft, initialScrollLeft, "Scroll position should decrease"); 137 }, "page-left command scrolls left"); 138 139 // Test page-right command 140 test(function (t) { 141 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 142 const initialScrollLeft = scrollcontainer.scrollLeft; 143 pageright.click(); 144 assert_greater_than(scrollcontainer.scrollLeft, initialScrollLeft, "Scroll position should increase"); 145 }, "page-right command scrolls right"); 146 147 // Test that page-up doesn't scroll horizontally 148 test(function (t) { 149 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 150 scrollcontainer.scrollTop = 400; 151 scrollcontainer.scrollLeft = 200; 152 const initialScrollLeft = scrollcontainer.scrollLeft; 153 pageup.click(); 154 assert_equals(scrollcontainer.scrollLeft, initialScrollLeft, "Horizontal scroll should not change"); 155 }, "page-up command doesn't affect horizontal scroll"); 156 157 // Test that page-left doesn't scroll vertically 158 test(function (t) { 159 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 160 scrollcontainer.scrollTop = 200; 161 scrollcontainer.scrollLeft = 400; 162 const initialScrollTop = scrollcontainer.scrollTop; 163 pageleft.click(); 164 assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Vertical scroll should not change"); 165 }, "page-left command doesn't affect vertical scroll"); 166 167 // Test horizontal-only container 168 test(function (t) { 169 t.add_cleanup(() => resetScrollPosition(horizontalcontainer)); 170 const initialScrollLeft = horizontalcontainer.scrollLeft; 171 hpageright.click(); 172 assert_greater_than(horizontalcontainer.scrollLeft, initialScrollLeft, "Horizontal scroll should increase"); 173 assert_equals(horizontalcontainer.scrollTop, 0, "Vertical scroll should remain 0"); 174 }, "page-right works on horizontal-only container"); 175 176 // Test vertical-only container 177 test(function (t) { 178 t.add_cleanup(() => resetScrollPosition(verticalcontainer)); 179 const initialScrollTop = verticalcontainer.scrollTop; 180 vpagedown.click(); 181 assert_greater_than(verticalcontainer.scrollTop, initialScrollTop, "Vertical scroll should increase"); 182 assert_equals(verticalcontainer.scrollLeft, 0, "Horizontal scroll should remain 0"); 183 }, "page-down works on vertical-only container"); 184 185 // Test page-block-end (should scroll down in horizontal writing mode) 186 test(function (t) { 187 t.add_cleanup(() => resetScrollPosition(logicalcontainer)); 188 const initialScrollTop = logicalcontainer.scrollTop; 189 blockend.click(); 190 assert_greater_than(logicalcontainer.scrollTop, initialScrollTop, "Scroll position should increase"); 191 }, "page-block-end scrolls down in horizontal writing mode"); 192 193 // Test page-block-start (should scroll up in horizontal writing mode) 194 test(function (t) { 195 t.add_cleanup(() => resetScrollPosition(logicalcontainer)); 196 logicalcontainer.scrollTop = 400; 197 const initialScrollTop = logicalcontainer.scrollTop; 198 blockstart.click(); 199 assert_less_than(logicalcontainer.scrollTop, initialScrollTop, "Scroll position should decrease"); 200 }, "page-block-start scrolls up in horizontal writing mode"); 201 202 // Test page-inline-end (should scroll right in LTR) 203 test(function (t) { 204 t.add_cleanup(() => resetScrollPosition(logicalcontainer)); 205 const initialScrollLeft = logicalcontainer.scrollLeft; 206 inlineend.click(); 207 assert_greater_than(logicalcontainer.scrollLeft, initialScrollLeft, "Scroll position should increase"); 208 }, "page-inline-end scrolls right in LTR"); 209 210 // Test page-inline-start (should scroll left in LTR) 211 test(function (t) { 212 t.add_cleanup(() => resetScrollPosition(logicalcontainer)); 213 logicalcontainer.scrollLeft = 400; 214 const initialScrollLeft = logicalcontainer.scrollLeft; 215 inlinestart.click(); 216 assert_less_than(logicalcontainer.scrollLeft, initialScrollLeft, "Scroll position should decrease"); 217 }, "page-inline-start scrolls left in LTR"); 218 219 // Test RTL inline directions 220 test(function (t) { 221 t.add_cleanup(() => resetScrollPosition(rtlcontainer)); 222 // In RTL, inline-end should scroll left (in the visual sense) 223 const initialScrollLeft = rtlcontainer.scrollLeft; 224 rtlinlineend.click(); 225 // Note: RTL scrolling behavior can vary, but the command should work 226 assert_not_equals(rtlcontainer.scrollLeft, initialScrollLeft, "Scroll position should change"); 227 }, "page-inline-end works in RTL container"); 228 229 // Test case insensitivity 230 ["page-up", "PAGE-UP", "PaGe-Up"].forEach((command) => { 231 test(function (t) { 232 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 233 const button = document.createElement("button"); 234 button.setAttribute("commandfor", "scrollcontainer"); 235 button.setAttribute("command", command); 236 document.body.appendChild(button); 237 t.add_cleanup(() => button.remove()); 238 239 scrollcontainer.scrollTop = 400; 240 const initialScrollTop = scrollcontainer.scrollTop; 241 button.click(); 242 assert_less_than(scrollcontainer.scrollTop, initialScrollTop, "Scroll should work with " + command); 243 }, `scroll command is case-insensitive: ${command}`); 244 }); 245 246 // Test preventDefault 247 test(function (t) { 248 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 249 scrollcontainer.addEventListener("command", (e) => e.preventDefault(), { once: true }); 250 const initialScrollTop = scrollcontainer.scrollTop; 251 pagedown.click(); 252 assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Scroll should not change when prevented"); 253 }, "preventDefault stops scroll command"); 254 255 // Test that scroll doesn't happen if commandfor is invalid 256 test(function (t) { 257 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 258 const button = document.createElement("button"); 259 button.setAttribute("commandfor", "nonexistent"); 260 button.setAttribute("command", "page-down"); 261 document.body.appendChild(button); 262 t.add_cleanup(() => button.remove()); 263 264 const initialScrollTop = scrollcontainer.scrollTop; 265 button.click(); 266 assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Scroll should not happen with invalid commandfor"); 267 }, "scroll command requires valid commandfor target"); 268 269 // Test that scroll doesn't happen on non-scrollable element 270 test(function (t) { 271 const nonscrollable = document.createElement("div"); 272 nonscrollable.id = "nonscrollable"; 273 nonscrollable.textContent = "Not scrollable"; 274 document.body.appendChild(nonscrollable); 275 t.add_cleanup(() => nonscrollable.remove()); 276 277 const button = document.createElement("button"); 278 button.setAttribute("commandfor", "nonscrollable"); 279 button.setAttribute("command", "page-down"); 280 document.body.appendChild(button); 281 t.add_cleanup(() => button.remove()); 282 283 // Should not throw or cause issues 284 button.click(); 285 assert_equals(nonscrollable.scrollTop, 0, "Non-scrollable element should remain at 0"); 286 }, "scroll command on non-scrollable element does nothing"); 287 288 // Test scroll amount is reasonable (approximately one page) 289 test(function (t) { 290 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 291 const initialScrollTop = scrollcontainer.scrollTop; 292 const containerHeight = scrollcontainer.clientHeight; 293 pagedown.click(); 294 const scrollDistance = scrollcontainer.scrollTop - initialScrollTop; 295 296 // Scroll should be at least 80% of container height (allowing for some overlap) 297 assert_greater_than(scrollDistance, containerHeight * 0.8, 298 "Scroll distance should be approximately one page"); 299 // And not more than 1.2x container height 300 assert_less_than(scrollDistance, containerHeight * 1.2, 301 "Scroll distance should not be much more than one page"); 302 }, "scroll amount is approximately one page"); 303 304 // Edge case: commandfor references non-existent element 305 test(function (t) { 306 const button = document.createElement("button"); 307 button.setAttribute("commandfor", "this-element-does-not-exist"); 308 button.setAttribute("command", "page-down"); 309 document.body.appendChild(button); 310 t.add_cleanup(() => button.remove()); 311 312 // Should not throw 313 assert_equals(button.click(), undefined, "Click should not throw"); 314 }, "scroll command with non-existent commandfor target doesn't throw"); 315 316 // Edge case: commandfor is empty string 317 test(function (t) { 318 const button = document.createElement("button"); 319 button.setAttribute("commandfor", ""); 320 button.setAttribute("command", "page-down"); 321 document.body.appendChild(button); 322 t.add_cleanup(() => button.remove()); 323 324 // Should not throw 325 assert_equals(button.click(), undefined, "Click should not throw"); 326 }, "scroll command with empty commandfor doesn't throw"); 327 328 // Edge case: commandfor is whitespace 329 test(function (t) { 330 const button = document.createElement("button"); 331 button.setAttribute("commandfor", " "); 332 button.setAttribute("command", "page-down"); 333 document.body.appendChild(button); 334 t.add_cleanup(() => button.remove()); 335 336 // Should not throw 337 assert_equals(button.click(), undefined, "Click should not throw"); 338 }, "scroll command with whitespace commandfor doesn't throw"); 339 340 // Edge case: target element is disconnected 341 test(function (t) { 342 const disconnected = document.createElement("div"); 343 disconnected.id = "disconnected"; 344 disconnected.className = "scroll-container"; 345 disconnected.innerHTML = '<div class="scroll-content"></div>'; 346 347 const button = document.createElement("button"); 348 button.setAttribute("commandfor", "disconnected"); 349 button.setAttribute("command", "page-down"); 350 document.body.appendChild(button); 351 t.add_cleanup(() => button.remove()); 352 353 // Should not throw 354 assert_equals(button.click(), undefined, "Click should not throw for disconnected target"); 355 }, "scroll command with disconnected target element doesn't throw"); 356 357 // Edge case: target element is button itself 358 test(function (t) { 359 const button = document.createElement("button"); 360 button.id = "selfbutton"; 361 button.setAttribute("commandfor", "selfbutton"); 362 button.setAttribute("command", "page-down"); 363 document.body.appendChild(button); 364 t.add_cleanup(() => button.remove()); 365 366 // Should not throw 367 assert_equals(button.click(), undefined, "Click should not throw when targeting self"); 368 }, "scroll command targeting self doesn't throw"); 369 370 // Edge case: target element is display:none 371 test(function (t) { 372 const hidden = document.createElement("div"); 373 hidden.id = "hiddenscroll"; 374 hidden.className = "scroll-container"; 375 hidden.style.display = "none"; 376 hidden.innerHTML = '<div class="scroll-content"></div>'; 377 document.body.appendChild(hidden); 378 t.add_cleanup(() => hidden.remove()); 379 380 const button = document.createElement("button"); 381 button.setAttribute("commandfor", "hiddenscroll"); 382 button.setAttribute("command", "page-down"); 383 document.body.appendChild(button); 384 t.add_cleanup(() => button.remove()); 385 386 // Should not throw 387 assert_equals(button.click(), undefined, "Click should not throw for hidden target"); 388 assert_equals(hidden.scrollTop, 0, "Hidden element should not scroll"); 389 }, "scroll command on display:none element does nothing"); 390 391 // Edge case: target element has no computed style (e.g., in detached document) 392 test(function (t) { 393 const newDoc = document.implementation.createHTMLDocument(); 394 const container = newDoc.createElement("div"); 395 container.id = "detachedcontainer"; 396 container.className = "scroll-container"; 397 newDoc.body.appendChild(container); 398 399 // Add the container to main document so commandfor can find it 400 document.body.appendChild(container); 401 t.add_cleanup(() => container.remove()); 402 403 const button = document.createElement("button"); 404 button.setAttribute("commandfor", "detachedcontainer"); 405 button.setAttribute("command", "page-down"); 406 document.body.appendChild(button); 407 t.add_cleanup(() => button.remove()); 408 409 // Should not throw 410 assert_equals(button.click(), undefined, "Click should not throw"); 411 }, "scroll command handles elements with unusual document state"); 412 413 // Edge case: button is disabled 414 test(function (t) { 415 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 416 const button = document.createElement("button"); 417 button.setAttribute("commandfor", "scrollcontainer"); 418 button.setAttribute("command", "page-down"); 419 button.disabled = true; 420 document.body.appendChild(button); 421 t.add_cleanup(() => button.remove()); 422 423 const initialScrollTop = scrollcontainer.scrollTop; 424 button.click(); 425 // Disabled buttons should not trigger commands 426 assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Disabled button should not trigger scroll"); 427 }, "disabled button doesn't trigger scroll command"); 428 429 // Edge case: multiple buttons targeting same element 430 test(function (t) { 431 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 432 const button1 = document.createElement("button"); 433 button1.setAttribute("commandfor", "scrollcontainer"); 434 button1.setAttribute("command", "page-down"); 435 document.body.appendChild(button1); 436 t.add_cleanup(() => button1.remove()); 437 438 const button2 = document.createElement("button"); 439 button2.setAttribute("commandfor", "scrollcontainer"); 440 button2.setAttribute("command", "page-down"); 441 document.body.appendChild(button2); 442 t.add_cleanup(() => button2.remove()); 443 444 const initialScrollTop = scrollcontainer.scrollTop; 445 button1.click(); 446 const afterFirst = scrollcontainer.scrollTop; 447 assert_greater_than(afterFirst, initialScrollTop, "First button should scroll"); 448 449 button2.click(); 450 assert_greater_than(scrollcontainer.scrollTop, afterFirst, "Second button should also scroll"); 451 }, "multiple buttons can target same scroll container"); 452 453 // Edge case: scroll at boundary (can't scroll further up) 454 test(function (t) { 455 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 456 scrollcontainer.scrollTop = 0; 457 458 // Should not throw 459 pageup.click(); 460 assert_equals(scrollcontainer.scrollTop, 0, "Should remain at top"); 461 }, "scroll command at top boundary doesn't throw"); 462 463 // Edge case: scroll at boundary (can't scroll further down) 464 test(function (t) { 465 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 466 scrollcontainer.scrollTop = scrollcontainer.scrollHeight - scrollcontainer.clientHeight; 467 const maxScroll = scrollcontainer.scrollTop; 468 469 // Should not throw 470 pagedown.click(); 471 assert_equals(scrollcontainer.scrollTop, maxScroll, "Should remain at bottom"); 472 }, "scroll command at bottom boundary doesn't throw"); 473 474 // Edge case: invalid command value 475 test(function (t) { 476 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 477 const button = document.createElement("button"); 478 button.setAttribute("commandfor", "scrollcontainer"); 479 button.setAttribute("command", "invalid-scroll-command"); 480 document.body.appendChild(button); 481 t.add_cleanup(() => button.remove()); 482 483 const initialScrollTop = scrollcontainer.scrollTop; 484 button.click(); 485 assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Invalid command should not scroll"); 486 }, "invalid scroll command value doesn't trigger scroll"); 487 488 // Edge case: command attribute is empty 489 test(function (t) { 490 t.add_cleanup(() => resetScrollPosition(scrollcontainer)); 491 const button = document.createElement("button"); 492 button.setAttribute("commandfor", "scrollcontainer"); 493 button.setAttribute("command", ""); 494 document.body.appendChild(button); 495 t.add_cleanup(() => button.remove()); 496 497 const initialScrollTop = scrollcontainer.scrollTop; 498 button.click(); 499 assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Empty command should not scroll"); 500 }, "empty command attribute doesn't trigger scroll"); 501 502 // Edge case: target has overflow:visible (not scrollable) 503 test(function (t) { 504 const visible = document.createElement("div"); 505 visible.id = "visibleoverflow"; 506 visible.style.width = "200px"; 507 visible.style.height = "200px"; 508 visible.style.overflow = "visible"; 509 visible.innerHTML = '<div style="width: 1000px; height: 1000px;"></div>'; 510 document.body.appendChild(visible); 511 t.add_cleanup(() => visible.remove()); 512 513 const button = document.createElement("button"); 514 button.setAttribute("commandfor", "visibleoverflow"); 515 button.setAttribute("command", "page-down"); 516 document.body.appendChild(button); 517 t.add_cleanup(() => button.remove()); 518 519 button.click(); 520 assert_equals(visible.scrollTop, 0, "overflow:visible element should not scroll"); 521 }, "scroll command on overflow:visible element does nothing"); 522 523 // Edge case: target has overflow:clip 524 test(function (t) { 525 const clipped = document.createElement("div"); 526 clipped.id = "clippedoverflow"; 527 clipped.style.width = "200px"; 528 clipped.style.height = "200px"; 529 clipped.style.overflow = "clip"; 530 clipped.innerHTML = '<div style="width: 1000px; height: 1000px;"></div>'; 531 document.body.appendChild(clipped); 532 t.add_cleanup(() => clipped.remove()); 533 534 const button = document.createElement("button"); 535 button.setAttribute("commandfor", "clippedoverflow"); 536 button.setAttribute("command", "page-down"); 537 document.body.appendChild(button); 538 t.add_cleanup(() => button.remove()); 539 540 button.click(); 541 assert_equals(clipped.scrollTop, 0, "overflow:clip element should not scroll"); 542 }, "scroll command on overflow:clip element does nothing"); 543 </script>