timeline-scope.html (9666B)
1 <!DOCTYPE html> 2 <title>Behavior of the timeline-scope property</title> 3 <link rel="help" src="https://github.com/w3c/csswg-drafts/issues/7759"> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <script src="/web-animations/testcommon.js"></script> 7 8 <main id=main></main> 9 <script> 10 async function inflate(t, template) { 11 t.add_cleanup(() => main.replaceChildren()); 12 return runAndWaitForFrameUpdate(() => { 13 main.append(template.content.cloneNode(true)); 14 }); 15 } 16 17 async function scrollTop(e, value) { 18 e.scrollTop = value; 19 await waitForNextFrame(); 20 } 21 </script> 22 <style> 23 @keyframes anim { 24 from { width: 0px; } 25 to { width: 200px; } 26 } 27 28 .scroller { 29 overflow-y: hidden; 30 width: 200px; 31 height: 200px; 32 } 33 .scroller > .content { 34 margin: 400px 0px; 35 width: 100px; 36 height: 100px; 37 background-color: green; 38 } 39 .target { 40 background-color: coral; 41 width: 0px; 42 animation: anim auto linear; 43 animation-timeline: --t1; 44 } 45 .timeline { 46 scroll-timeline-name: --t1; 47 } 48 .scope { 49 timeline-scope: --t1; 50 } 51 52 </style> 53 54 <!-- Basic Behavior --> 55 56 <template id=deferred_timeline> 57 <div class="scope"> 58 <div class=target>Test</div> 59 <div class="scroller timeline"> 60 <div class=content></div> 61 </div> 62 </div> 63 </template> 64 <script> 65 promise_test(async (t) => { 66 await inflate(t, deferred_timeline); 67 let scroller = main.querySelector('.scroller'); 68 let target = main.querySelector('.target'); 69 70 const anim = target.getAnimations()[0]; 71 await anim.ready; 72 73 await scrollTop(scroller, 350); // 50% 74 assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% 75 }, 'Descendant can attach to deferred timeline'); 76 </script> 77 78 <template id=deferred_timeline_no_attachments> 79 <div class="scope"> 80 <div class=target>Test</div> 81 <div class="scroller"> 82 <div class=content></div> 83 </div> 84 </div> 85 </template> 86 <script> 87 promise_test(async (t) => { 88 await inflate(t, deferred_timeline_no_attachments); 89 let scroller = main.querySelector('.scroller'); 90 let target = main.querySelector('.target'); 91 await scrollTop(scroller, 350); // 50% 92 assert_equals(getComputedStyle(target).width, '0px'); 93 }, 'Deferred timeline with no attachments'); 94 </script> 95 96 <template id=scroll_timeline_inner_interference> 97 <div class="scroller timeline"> 98 <div class=content> 99 <div class=target>Test</div> 100 <div class="scroller timeline"> 101 <div class=content></div> 102 </div> 103 </div> 104 </div> 105 </template> 106 <script> 107 promise_test(async (t) => { 108 await inflate(t, scroll_timeline_inner_interference); 109 let scroller = main.querySelector('.scroller'); 110 let target = main.querySelector('.target'); 111 await scrollTop(scroller, 350); // 50% 112 assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% 113 }, 'Inner timeline does not interfere with outer timeline'); 114 </script> 115 116 <template id=deferred_timeline_two_attachments> 117 <div class="scope"> 118 <div class=target>Test</div> 119 <div class="scroller timeline"> 120 <div class=content></div> 121 </div> 122 <!-- Extra attachment --> 123 <div class="timeline"></div> 124 </div> 125 </template> 126 <script> 127 promise_test(async (t) => { 128 await inflate(t, deferred_timeline_two_attachments); 129 let scroller = main.querySelector('.scroller'); 130 let target = main.querySelector('.target'); 131 await scrollTop(scroller, 350); // 50% 132 assert_equals(getComputedStyle(target).width, '0px'); 133 }, 'Deferred timeline with two attachments'); 134 </script> 135 136 <!-- Dynamic Reattachment --> 137 138 <template id=deferred_timeline_reattach> 139 <div class="scope"> 140 <div class=target>Test</div> 141 <div class="scroller timeline"> 142 <div class=content></div> 143 </div> 144 <div class="scroller"> 145 <div class=content></div> 146 </div> 147 </div> 148 </template> 149 <script> 150 promise_test(async (t) => { 151 await inflate(t, deferred_timeline_reattach); 152 let scrollers = main.querySelectorAll('.scroller'); 153 assert_equals(scrollers.length, 2); 154 let target = main.querySelector('.target'); 155 await scrollTop(scrollers[0], 350); // 50% 156 await scrollTop(scrollers[1], 175); // 25% 157 158 // Attached to scrollers[0]. 159 assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% 160 161 // Reattach to scrollers[1]. 162 await runAndWaitForFrameUpdate(() => { 163 scrollers[0].classList.remove('timeline'); 164 scrollers[1].classList.add('timeline'); 165 }); 166 167 assert_equals(getComputedStyle(target).width, '50px'); // 0px => 200px, 25% 168 }, 'Dynamically re-attaching'); 169 </script> 170 171 <template id=deferred_timeline_dynamic_detach> 172 <div class="scope"> 173 <div class=target>Test</div> 174 <div class="scroller timeline"> 175 <div class=content></div> 176 </div> 177 <div class="scroller timeline"> 178 <div class=content></div> 179 </div> 180 </div> 181 </template> 182 <script> 183 promise_test(async (t) => { 184 await inflate(t, deferred_timeline_dynamic_detach); 185 let scrollers = main.querySelectorAll('.scroller'); 186 assert_equals(scrollers.length, 2); 187 let target = main.querySelector('.target'); 188 await scrollTop(scrollers[0], 350); // 50% 189 await scrollTop(scrollers[1], 175); // 25% 190 191 // Attached to two timelines initially: 192 assert_equals(getComputedStyle(target).width, '0px'); 193 194 // Detach scrollers[1]. 195 await runAndWaitForFrameUpdate(() => { 196 scrollers[1].classList.remove('timeline'); 197 }); 198 199 assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% 200 201 // Also detach scrollers[0]. 202 scrollers[0].classList.remove('timeline'); 203 204 await waitForNextFrame(); 205 assert_equals(getComputedStyle(target).width, '0px'); 206 }, 'Dynamically detaching'); 207 </script> 208 209 <template id=deferred_timeline_attached_removed> 210 <div class="scope"> 211 <div class=target>Test</div> 212 <div class="scroller timeline"> 213 <div class=content></div> 214 </div> 215 </div> 216 </template> 217 <script> 218 promise_test(async (t) => { 219 await inflate(t, deferred_timeline_attached_removed); 220 let scroller = main.querySelector('.scroller'); 221 let target = main.querySelector('.target'); 222 await scrollTop(scroller, 350); // 50% 223 assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% 224 225 let scroller_parent = scroller.parentElement; 226 scroller.remove(); 227 await waitForNextFrame(); 228 assert_equals(getComputedStyle(target).width, '0px'); 229 230 scroller_parent.append(scroller); 231 await scrollTop(scroller, 350); // 50% 232 assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% 233 }, 'Removing/inserting element with attaching timeline'); 234 </script> 235 236 <template id=deferred_timeline_attached_display_none> 237 <div class="scope"> 238 <div class=target>Test</div> 239 <div class="scroller timeline"> 240 <div class=content></div> 241 </div> 242 </div> 243 </template> 244 <script> 245 promise_test(async (t) => { 246 await inflate(t, deferred_timeline_attached_display_none); 247 let scroller = main.querySelector('.scroller'); 248 let target = main.querySelector('.target'); 249 await scrollTop(scroller, 350); // 50% 250 assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% 251 252 scroller.style.display = 'none'; 253 await waitForNextFrame(); 254 assert_equals(getComputedStyle(target).width, '0px'); 255 256 scroller.style.display = 'block'; 257 await scrollTop(scroller, 350); // 50% 258 assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% 259 }, 'Ancestor attached element becoming display:none/block'); 260 </script> 261 262 <template id=deferred_timeline_appearing> 263 <div class=container> 264 <div class=target>Test</div> 265 <div class="scroller timeline"> 266 <div class=content></div> 267 </div> 268 </div> 269 </template> 270 <script> 271 promise_test(async (t) => { 272 await inflate(t, deferred_timeline_appearing); 273 let container = main.querySelector('.container'); 274 let scroller = main.querySelector('.scroller'); 275 let target = main.querySelector('.target'); 276 277 await scrollTop(scroller, 350); // 50% 278 279 // Not attached to any timeline initially. 280 assert_equals(getComputedStyle(target).width, '0px'); 281 282 // Add the deferred timeline. 283 container.classList.add('scope'); 284 await waitForNextFrame(); 285 assert_equals(getComputedStyle(target).width, '100px'); // 0px => 200px, 50% 286 287 // Remove the deferred timeline. 288 container.classList.remove('scope'); 289 await waitForNextFrame(); 290 assert_equals(getComputedStyle(target).width, '0px'); 291 }, 'A deferred timeline appearing dynamically in the ancestor chain'); 292 </script> 293 294 <template id=deferred_timeline_on_self> 295 <div class="scroller timeline scope"> 296 <div class=content> 297 <div class=target></div> 298 </div> 299 <div class=scroller2></div> 300 </div> 301 </template> 302 <script> 303 promise_test(async (t) => { 304 await inflate(t, deferred_timeline_on_self); 305 let scroller = main.querySelector('.scroller'); 306 let target = main.querySelector('.target'); 307 await scrollTop(scroller, 525); // 75% 308 309 assert_equals(getComputedStyle(target).width, '150px'); // 0px => 200px, 75% 310 311 // A second scroll-timeline now attaches to the same root. 312 let scroller2 = main.querySelector('.scroller2'); 313 scroller2.classList.add('timeline'); 314 await waitForNextFrame(); 315 316 // The deferred timeline produced by timeline-scope is now inactive, 317 // but it doesn't matter, because we preferred to attach 318 // to the non-deferred timeline. 319 assert_equals(getComputedStyle(target).width, '150px'); // 0px => 200px, 75% 320 }, 'Animations prefer non-deferred timelines'); 321 322 </script>