| <!DOCTYPE html> |
| <title>The animation-timeline: scroll-timeline-name</title> |
| <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"> |
| <link rel="help" src="https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timelines-named"> |
| <link rel="help" src="https://github.com/w3c/csswg-drafts/issues/6674"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/web-animations/testcommon.js"></script> |
| <script src="support/testcommon.js"></script> |
| <script src="/scroll-animations/scroll-timelines/testcommon.js"></script> |
| <style> |
| @keyframes anim { |
| from { translate: 50px; } |
| to { translate: 150px; } |
| } |
| @keyframes anim-2 { |
| from { z-index: 0; } |
| to { z-index: 100; } |
| } |
| |
| #target { |
| width: 100px; |
| height: 100px; |
| } |
| .square { |
| width: 100px; |
| height: 100px; |
| } |
| .square-container { |
| width: 300px; |
| height: 300px; |
| } |
| .scroller { |
| overflow: scroll; |
| } |
| .content { |
| inline-size: 100%; |
| block-size: 100%; |
| padding-inline-end: 100px; |
| padding-block-end: 100px; |
| } |
| </style> |
| <body> |
| <div id="log"></div> |
| <script> |
| "use strict"; |
| |
| setup(assert_implements_animation_timeline); |
| |
| function createScroller(t, scrollerSizeClass) { |
| let scroller = document.createElement('div'); |
| let className = scrollerSizeClass || 'square'; |
| scroller.className = `scroller ${className}`; |
| let content = document.createElement('div'); |
| content.className = 'content'; |
| |
| scroller.appendChild(content); |
| |
| t.add_cleanup(function() { |
| content.remove(); |
| scroller.remove(); |
| }); |
| |
| return scroller; |
| } |
| |
| function createTarget(t) { |
| let target = document.createElement('div'); |
| target.id = 'target'; |
| |
| t.add_cleanup(function() { |
| target.remove(); |
| }); |
| |
| return target; |
| } |
| |
| function createScrollerAndTarget(t, scrollerSizeClass) { |
| return [createScroller(t, scrollerSizeClass), createTarget(t)]; |
| } |
| |
| async function waitForScrollTop(scroller, percentage) { |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| scroller.scrollTop = maxScroll * percentage / 100; |
| return waitForNextFrame(); |
| } |
| |
| async function waitForScrollLeft(scroller, percentage) { |
| const maxScroll = scroller.scrollWidth - scroller.clientWidth; |
| scroller.scrollLeft = maxScroll * percentage / 100; |
| return waitForNextFrame(); |
| } |
| |
| // ------------------------- |
| // Test scroll-timeline-name |
| // ------------------------- |
| |
| promise_test(async t => { |
| let target = document.createElement('div'); |
| target.id = 'target'; |
| target.className = 'scroller'; |
| let content = document.createElement('div'); |
| content.className = 'content'; |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='target' class='scroller'> |
| // <div id='content'></div> |
| // </div> |
| document.body.appendChild(target); |
| target.appendChild(content); |
| |
| target.style.scrollTimelineName = '--timeline'; |
| target.style.animation = "anim 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| target.scrollTop = 50; // 50% |
| }); |
| |
| assert_equals(getComputedStyle(target).translate, '100px'); |
| |
| content.remove(); |
| target.remove(); |
| }, 'scroll-timeline-name is referenceable in animation-timeline on the ' + |
| 'declaring element itself'); |
| |
| promise_test(async t => { |
| let [parent, target] = createScrollerAndTarget(t, 'square-container'); |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='parent' class='scroller'> |
| // <div id='target'></div> |
| // <div id='content'></div> |
| // </div> |
| document.body.appendChild(parent); |
| parent.insertBefore(target, parent.firstElementChild); |
| |
| parent.style.scrollTimelineName = '--timeline'; |
| target.style.animation = "anim 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| |
| parent.scrollTop = 100; // 50% |
| }); |
| |
| assert_equals(getComputedStyle(target).translate, '100px'); |
| }, "scroll-timeline-name is referenceable in animation-timeline on that " + |
| "element's descendants"); |
| |
| // See https://github.com/w3c/csswg-drafts/issues/7047 |
| promise_test(async t => { |
| let [sibling, target] = createScrollerAndTarget(t); |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='sibling' class='scroller'> ... </div> |
| // <div id='target'></div> |
| document.body.appendChild(sibling); |
| document.body.appendChild(target); |
| |
| // Resolvable if using a deferred timeline, but otherwise can only resolve |
| // if an ancestor container of the target element. |
| sibling.style.scrollTimelineName = '--timeline'; |
| target.style.animation = "anim 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| |
| sibling.scrollTop = 50; // 50% |
| }); |
| |
| assert_equals(getComputedStyle(target).translate, '50px', |
| 'Animation with unknown timeline name holds current time at zero'); |
| }, "scroll-timeline-name is not referenceable in animation-timeline on that " + |
| "element's siblings"); |
| |
| promise_test(async t => { |
| let parent = document.createElement('div'); |
| parent.className = 'square'; |
| parent.style.overflowX = 'clip'; // This makes overflow-y be clip as well. |
| let target = document.createElement('div'); |
| target.id = 'target'; |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='parent' style='overflow-x: clip'>... |
| // <div id='target'></div> |
| // </div> |
| document.body.appendChild(parent); |
| parent.appendChild(target); |
| |
| parent.style.scrollTimelineName = '--timeline'; |
| target.style.animation = "anim 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| }); |
| |
| assert_equals(getComputedStyle(target).translate, 'none', |
| 'Animation with an unresolved current time'); |
| |
| target.remove(); |
| parent.remove(); |
| }, 'scroll-timeline-name on an element which is not a scroll-container'); |
| |
| promise_test(async t => { |
| let [scroller, target] = createScrollerAndTarget(t); |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='scroller' class='scroller'> ... |
| // <div id='target'></div> |
| // </div> |
| |
| document.body.appendChild(scroller); |
| scroller.appendChild(target); |
| |
| scroller.style.scrollTimelineName = '--timeline-A'; |
| scroller.scrollTop = 50; // 25% |
| target.style.animation = "anim 10s linear"; |
| target.style.animationTimeline = '--timeline-B'; |
| }); |
| |
| const anim = target.getAnimations()[0]; |
| assert_true(!!anim, 'Failed to create animation'); |
| assert_equals(anim.timeline, null); |
| // Hold time of animation is zero. |
| assert_equals(getComputedStyle(target).translate, '50px'); |
| |
| scroller.style.scrollTimelineName = '--timeline-B'; |
| await waitForNextFrame(); |
| |
| assert_true(!!anim.timeline, 'Failed to create timeline'); |
| assert_equals(getComputedStyle(target).translate, '75px'); |
| }, 'Change in scroll-timeline-name to match animation timeline updates animation.'); |
| |
| promise_test(async t => { |
| let [scroller, target] = createScrollerAndTarget(t); |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='scroller' class='scroller'> ... |
| // <div id='target'></div> |
| // </div> |
| |
| document.body.appendChild(scroller); |
| scroller.appendChild(target); |
| |
| scroller.style.scrollTimelineName = '--timeline-A'; |
| scroller.scrollTop = 50; // 25% |
| target.style.animation = "anim 10s linear"; |
| target.style.animationTimeline = '--timeline-A'; |
| }); |
| |
| const anim = target.getAnimations()[0]; |
| assert_true(!!anim, 'Failed to create animation'); |
| assert_true(!!anim.timeline, 'Failed to create timeline'); |
| assert_equals(getComputedStyle(target).translate, '75px'); |
| assert_percents_equal(anim.startTime, 0); |
| assert_percents_equal(anim.currentTime, 25); |
| |
| scroller.style.scrollTimelineName = '--timeline-B'; |
| await waitForNextFrame(); |
| |
| // Switching to a null timeline pauses the animation. |
| assert_equals(anim.timeline, null, 'Failed to remove timeline'); |
| assert_equals(getComputedStyle(target).translate, '75px'); |
| assert_equals(anim.startTime, null); |
| assert_times_equal(anim.currentTime, 2500); |
| }, 'Change in scroll-timeline-name to no longer match animation timeline updates animation.'); |
| |
| promise_test(async t => { |
| let target = createTarget(t); |
| let scroller1 = createScroller(t); |
| let scroller2 = createScroller(t); |
| |
| target.style.animation = 'anim 10s linear'; |
| target.style.animationTimeline = '--timeline'; |
| scroller1.style.scrollTimelineName = '--timeline'; |
| scroller1.id = 'A'; |
| scroller2.id = 'B'; |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div class='scroller' id='A'> ... |
| // <div class='scroller' id='B'> ... |
| // <div id='target'></div> |
| // </div> |
| // </div> |
| document.body.appendChild(scroller1); |
| scroller1.appendChild(scroller2); |
| scroller2.appendChild(target); |
| |
| scroller1.style.scrollTimelineName = '--timeline'; |
| scroller1.scrollTop = 50; // 25% |
| scroller2.scrollTop = 100; // 50% |
| }); |
| |
| const anim = target.getAnimations()[0]; |
| assert_true(!!anim.timeline, 'Failed to retrieve animation'); |
| assert_equals(anim.timeline.source.id, 'A'); |
| assert_equals(getComputedStyle(target).translate, '75px'); |
| |
| scroller2.style.scrollTimelineName = '--timeline'; |
| await waitForNextFrame(); |
| |
| // The timeline should be updated to scroller2. |
| assert_true(!!anim.timeline, 'Animation no longer has a timeline'); |
| assert_equals(anim.timeline.source.id, 'B', 'Timeline not updated'); |
| assert_equals(getComputedStyle(target).translate, '100px'); |
| }, 'Timeline lookup updates candidate when closer match available.'); |
| |
| promise_test(async t => { |
| let wrapper = createScroller(t); |
| wrapper.classList.remove('scroller'); |
| let target = createTarget(t); |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='wrapper'> ... |
| // <div id='target'></div> |
| // </div> |
| document.body.appendChild(wrapper); |
| wrapper.appendChild(target); |
| target.style.animation = "anim 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| }); |
| |
| // Timeline initially cannot be resolved, resulting in a null |
| // timeline. The animation's hold time is zero. |
| // let anim = document.getAnimations()[0]; |
| assert_equals(getComputedStyle(target).translate, '50px'); |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='wrapper' class="scroller"> ... |
| // <div id='target'></div> |
| // </div> |
| wrapper.classList.add('scroller'); |
| wrapper.style.scrollTimelineName = '--timeline'; |
| wrapper.scrollTop = 50; // 25% |
| }); |
| |
| // The timeline should be updated to scroller. |
| assert_equals(getComputedStyle(target).translate, '75px'); |
| }, 'Timeline lookup updates candidate when match becomes available.'); |
| |
| |
| // ------------------------- |
| // Test scroll-timeline-axis |
| // ------------------------- |
| |
| promise_test(async t => { |
| let [scroller, target] = createScrollerAndTarget(t); |
| scroller.style.writingMode = 'vertical-lr'; |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='scroller' class='scroller'> ... |
| // <div id='target'></div> |
| // </div> |
| document.body.appendChild(scroller); |
| scroller.appendChild(target); |
| |
| scroller.style.scrollTimeline = '--timeline block'; |
| target.style.animation = "anim-2 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| }); |
| |
| await waitForScrollLeft(scroller, 50); |
| assert_equals(getComputedStyle(target).zIndex, '50'); |
| }, 'scroll-timeline-axis is block'); |
| |
| promise_test(async t => { |
| let [scroller, target] = createScrollerAndTarget(t); |
| scroller.style.writingMode = 'vertical-lr'; |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='scroller' class='scroller'> ... |
| // <div id='target'></div> |
| // </div> |
| document.body.appendChild(scroller); |
| scroller.appendChild(target); |
| |
| scroller.style.scrollTimeline = '--timeline inline'; |
| target.style.animation = "anim-2 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| }); |
| |
| await waitForScrollTop(scroller, 50); |
| assert_equals(getComputedStyle(target).zIndex, '50'); |
| }, 'scroll-timeline-axis is inline'); |
| |
| promise_test(async t => { |
| let [scroller, target] = createScrollerAndTarget(t); |
| scroller.style.writingMode = 'vertical-lr'; |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='scroller' class='scroller'> ... |
| // <div id='target'></div> |
| // </div> |
| document.body.appendChild(scroller); |
| scroller.appendChild(target); |
| |
| scroller.style.scrollTimeline = '--timeline x'; |
| target.style.animation = "anim-2 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| }); |
| |
| await waitForScrollLeft(scroller, 50); |
| assert_equals(getComputedStyle(target).zIndex, '50'); |
| }, 'scroll-timeline-axis is x'); |
| |
| promise_test(async t => { |
| let [scroller, target] = createScrollerAndTarget(t); |
| scroller.style.writingMode = 'vertical-lr'; |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='scroller' class='scroller'> ... |
| // <div id='target'></div> |
| // </div> |
| document.body.appendChild(scroller); |
| scroller.appendChild(target); |
| |
| scroller.style.scrollTimeline = '--timeline y'; |
| target.style.animation = "anim-2 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| }); |
| |
| await waitForScrollTop(scroller, 50); |
| assert_equals(getComputedStyle(target).zIndex, '50'); |
| }, 'scroll-timeline-axis is y'); |
| |
| promise_test(async t => { |
| let [scroller, target] = createScrollerAndTarget(t); |
| |
| await runAndWaitForFrameUpdate(() => { |
| // <div id='scroller' class='scroller'> ... |
| // <div id='target'></div> |
| // </div> |
| document.body.appendChild(scroller); |
| scroller.appendChild(target); |
| |
| scroller.style.scrollTimeline = '--timeline block'; |
| target.style.animation = "anim-2 10s linear"; |
| target.style.animationTimeline = '--timeline'; |
| }); |
| |
| await waitForScrollTop(scroller, 25); |
| await waitForScrollLeft(scroller, 75); |
| assert_equals(getComputedStyle(target).zIndex, '25'); |
| |
| scroller.style.scrollTimelineAxis = 'inline'; |
| await waitForNextFrame(); |
| assert_equals(getComputedStyle(target).zIndex, '75'); |
| }, 'scroll-timeline-axis is mutated'); |
| |
| </script> |
| </body> |