Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test has a WPT meta file that expects 5 subtest issues.
- This WPT test may be referenced by the following Test IDs:
- /scroll-animations/css/animation-timeline-named-scroll-progress-timeline.tentative.html - WPT Dashboard Interop Dashboard
<!DOCTYPE html>
<title>The animation-timeline: scroll-timeline-name</title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
<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");
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>