Bug 1754897 - Part 4: Hook named scroll timelines to animation-timeline. r=emilio

Basically, animation-timeline could be
1. auto
2. none
3. <timeline-name>

We extend the <timeline-name> to cover both @scroll-timeline rule and
scroll-timeline-name property. We check @scroll-timeline rule first. If
it doesn't exist, we check scroll-timeline-name of the element itself,
the previous silbings, and their ancestors.

Differential Revision: https://phabricator.services.mozilla.com/D146358
This commit is contained in:
Boris Chiou 2022-06-13 20:26:45 +00:00
parent f1b5d17847
commit 2b8751f919
5 changed files with 389 additions and 16 deletions

View File

@ -149,6 +149,73 @@ already_AddRefed<ScrollTimeline> ScrollTimeline::FromAnonymousScroll(
return timeline.forget();
}
/* static*/ already_AddRefed<ScrollTimeline> ScrollTimeline::FromNamedScroll(
Document* aDocument, const NonOwningAnimationTarget& aTarget,
const nsAtom* aName) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aTarget);
// A named scroll progress timeline is referenceable in animation-timeline by:
// 1. the declaring element itself
// 2. that elements descendants
// 3. that elements following siblings and their descendants
// https://drafts.csswg.org/scroll-animations-1/rewrite#timeline-scope
//
// Note: It's unclear to us about the scope of scroll-timeline, so we
// intentionally don't let it cross the shadow dom boundary for now.
//
// FIXME: We may have to support global scope. This depends on the result of
// this spec issue: https://github.com/w3c/csswg-drafts/issues/7047
Element* result = nullptr;
StyleScrollAxis axis = StyleScrollAxis::Block;
for (Element* curr = aTarget.mElement; curr;
curr = curr->GetParentElement()) {
// If multiple elements have declared the same timeline name, the matching
// timeline is the one declared on the nearest element in tree order, which
// considers siblings closer than parents.
// Note: This should be fine for parallel traversal because we update
// animations by SequentialTask.
for (Element* e = curr; e; e = e->GetPreviousElementSibling()) {
const ComputedStyle* style = Servo_Element_GetMaybeOutOfDateStyle(e);
// The elements in the shadow dom might not be in the flat tree.
if (!style) {
continue;
}
const nsStyleUIReset* styleUIReset = style->StyleUIReset();
if (styleUIReset->mScrollTimelineName._0.AsAtom() == aName) {
result = e;
axis = styleUIReset->mScrollTimelineAxis;
break;
}
}
if (result) {
break;
}
}
// If we cannot find a matched scroll-timeline-name, this animation is not
// associated with a timeline.
// https://drafts.csswg.org/css-animations-2/#typedef-timeline-name
if (!result) {
return nullptr;
}
Scroller scroller = Scroller::Named(result);
RefPtr<ScrollTimeline> timeline;
auto* set =
ScrollTimelineSet::GetOrCreateScrollTimelineSet(scroller.mElement);
auto p = set->LookupForAdd(axis);
if (!p) {
timeline = new ScrollTimeline(aDocument, scroller, axis);
set->Add(p, axis, timeline);
} else {
timeline = p->value();
}
return timeline.forget();
}
Nullable<TimeDuration> ScrollTimeline::GetCurrentTimeAsDuration() const {
// If no layout box, this timeline is inactive.
if (!mSource || !mSource.mElement->GetPrimaryFrame()) {
@ -244,13 +311,14 @@ const nsIScrollableFrame* ScrollTimeline::GetScrollFrame() const {
}
switch (mSource.mType) {
case StyleScroller::Root:
case Scroller::Type::Root:
if (const PresShell* presShell =
mSource.mElement->OwnerDoc()->GetPresShell()) {
return presShell->GetRootScrollFrameAsScrollable();
}
return nullptr;
case StyleScroller::Nearest:
case Scroller::Type::Nearest:
case Scroller::Type::Name:
return nsLayoutUtils::FindScrollableFrameFor(mSource.mElement);
}

View File

@ -65,7 +65,15 @@ class Element;
class ScrollTimeline final : public AnimationTimeline {
public:
struct Scroller {
StyleScroller mType = StyleScroller::Root;
// FIXME: Once we support <custom-ident> for <scroller>, we can use
// StyleScroller here.
// https://drafts.csswg.org/scroll-animations-1/rewrite#typedef-scroller
enum class Type {
Root,
Nearest,
Name,
};
Type mType = Type::Root;
RefPtr<Element> mElement;
// We use the owner doc of the animation target. This may be different from
@ -76,13 +84,15 @@ class ScrollTimeline final : public AnimationTimeline {
// we always register the ScrollTimeline to the document element (i.e.
// root element) because the content of the root scroll frame is the root
// element.
return {StyleScroller::Root, aOwnerDoc->GetDocumentElement()};
return {Type::Root, aOwnerDoc->GetDocumentElement()};
}
static Scroller Nearest(Element* aElement) {
return {StyleScroller::Nearest, aElement};
return {Type::Nearest, aElement};
}
static Scroller Named(Element* aElement) { return {Type::Name, aElement}; }
explicit operator bool() const { return mElement; }
bool operator==(const Scroller& aOther) const {
return mType == aOther.mType && mElement == aOther.mElement;
@ -97,6 +107,10 @@ class ScrollTimeline final : public AnimationTimeline {
Document* aDocument, const NonOwningAnimationTarget& aTarget,
StyleScrollAxis aAxis, StyleScroller aScroller);
static already_AddRefed<ScrollTimeline> FromNamedScroll(
Document* aDocument, const NonOwningAnimationTarget& aTarget,
const nsAtom* aName);
bool operator==(const ScrollTimeline& aOther) const {
return mDocument == aOther.mDocument && mSource == aOther.mSource &&
mAxis == aOther.mAxis;

View File

@ -215,16 +215,18 @@ static already_AddRefed<dom::AnimationTimeline> GetTimeline(
// That's how we represent `none`.
return nullptr;
}
const auto* rule =
aPresContext->StyleSet()->ScrollTimelineRuleForName(name);
if (!rule) {
// Unknown timeline, so treat is as no timeline. Keep nullptr.
return nullptr;
// 1. Check @scroll-timeline rule.
if (const auto* rule =
aPresContext->StyleSet()->ScrollTimelineRuleForName(name)) {
// We do intentionally use the pres context's document for the owner of
// ScrollTimeline since it's consistent with what we do for
// KeyframeEffect instance.
return ScrollTimeline::FromRule(*rule, aPresContext->Document(),
aTarget);
}
// We do intentionally use the pres context's document for the owner of
// ScrollTimeline since it's consistent with what we do for
// KeyframeEffect instance.
return ScrollTimeline::FromRule(*rule, aPresContext->Document(), aTarget);
// 2. Check scroll-timeline-name property.
return ScrollTimeline::FromNamedScroll(aPresContext->Document(), aTarget,
name);
}
case StyleAnimationTimeline::Tag::Scroll: {
const auto& scroll = aStyleTimeline.AsScroll();

View File

@ -844,9 +844,13 @@ fn is_default<T: Default + PartialEq>(value: &T) -> bool {
pub enum AnimationTimeline {
/// Use default timeline. The animations timeline is a DocumentTimeline.
Auto,
/// The scroll-timeline name
/// The scroll-timeline name.
///
/// Note: This could be the timeline name from @scroll-timeline rule, or scroll-timeline-name
/// from itself, its ancestors, or its previous siblings.
/// https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-timelines-named
Timeline(TimelineName),
/// The scroll() notation
/// The scroll() notation.
/// https://drafts.csswg.org/scroll-animations-1/rewrite#scroll-notation
#[css(function)]
Scroll(

View File

@ -0,0 +1,285 @@
<!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>
<style>
@keyframes anim {
from { translate: 50px; }
to { translate: 150px; }
}
#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";
function createScrollerAndTarget(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);
let target = document.createElement('div');
target.id = 'target';
t.add_cleanup(function() {
content.remove();
scroller.remove();
target.remove();
});
return [scroller, target];
}
// -------------------------
// Test scroll-timeline-name
// -------------------------
test(t => {
let target = document.createElement('div');
target.id = 'target';
target.className = 'scroller';
let content = document.createElement('div');
content.className = 'content';
// <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 timeline";
target.scrollTop = 50; // 50%, in [0, 100].
assert_equals(getComputedStyle(target).translate, '100px');
content.remove();
target.remove();
}, 'scroll-timeline-name is referenceable in animation-timeline on the ' +
'declaring element itself');
test(t => {
let [parent, target] = createScrollerAndTarget(t, 'square-container');
// <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 timeline";
parent.scrollTop = 100; // 50%, in [0, 200].
assert_equals(getComputedStyle(target).translate, '100px');
}, "scroll-timeline-name is referenceable in animation-timeline on that " +
"element's descendants");
test(t => {
let [sibling, target] = createScrollerAndTarget(t);
// <div id='sibling' class='scroller'> ... </div>
// <div id='target'></div>
document.body.appendChild(sibling);
document.body.appendChild(target);
sibling.style.scrollTimelineName = 'timeline';
target.style.animation = "anim 10s linear timeline";
sibling.scrollTop = 50; // 50%, in [0, 100].
assert_equals(getComputedStyle(target).translate, '100px');
}, "scroll-timeline-name is referenceable in animation-timeline on that " +
"element's following siblings");
test(t => {
let [sibling, target] = createScrollerAndTarget(t);
let parent = document.createElement('div');
// <div id='sibling' class='scroller'> ... </div>
// <div id='parent'>
// <div id='target'></div>
// </div>
document.body.appendChild(sibling);
document.body.appendChild(parent);
parent.appendChild(target);
sibling.style.scrollTimelineName = 'timeline';
target.style.animation = "anim 10s linear timeline";
sibling.scrollTop = 50; // 50%, in [0, 100].
assert_equals(getComputedStyle(target).translate, '100px');
parent.remove();
}, "scroll-timeline-name is referenceable in animation-timeline on that " +
"element's following siblings' descendants");
// FIXME: We may use global scope for scroll-timeline-name.
// See https://github.com/w3c/csswg-drafts/issues/7047
test(t => {
let [sibling, target] = createScrollerAndTarget(t);
// <div id='target'></div>
// <div id='sibling' class='scroller'> ... </div>
document.body.appendChild(target);
document.body.appendChild(sibling);
sibling.style.scrollTimelineName = 'timeline';
target.style.animation = "anim 10s linear timeline";
sibling.scrollTop = 50; // 50%, in [0, 100].
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 previous siblings");
test(t => {
let [sibling, target] = createScrollerAndTarget(t);
let parent = document.createElement('div');
parent.className = 'scroller square-container';
let content = document.createElement('div');
content.className = 'content';
// <div id='parent' class='scroller'>
// <div id='sibling' class='scroller'> ... </div>
// <div id='target'></div>
// <div id='content'></div>
// </div>
document.body.appendChild(parent);
parent.appendChild(sibling);
parent.appendChild(target);
parent.appendChild(content);
parent.style.scrollTimelineName = 'timeline';
parent.style.scrollTimelineAxis = 'inline';
sibling.style.scrollTimelineName = 'timeline';
target.style.animation = "anim 10s linear timeline";
parent.scrollTop = 50; // 25%, in [0, 200].
sibling.scrollTop = 50; // 50%, in [0, 100].
assert_equals(getComputedStyle(target).translate, '100px');
content.remove();
parent.remove();
}, 'scroll-timeline-name is matched based on tree order, which considers ' +
'siblings closer than parents');
test(t => {
let sibling = document.createElement('div');
sibling.className = 'square';
sibling.style.overflowX = 'clip'; // This makes overflow-y be clip as well.
let target = document.createElement('div');
target.id = 'target';
// <div id='sibling' style='overflow-x: clip'></div>
// <div id='target'></div>
document.body.appendChild(sibling);
document.body.appendChild(target);
sibling.style.scrollTimelineName = 'timeline';
target.style.animation = "anim 10s linear timeline";
sibling.scrollTop = 50; // 50%, in [0, 100].
assert_equals(getComputedStyle(target).translate, 'none',
'Animation with an unresolved current time');
target.remove();
sibling.remove();
}, 'scroll-timeline-name on an element which is not a scroll-container');
// TODO: Add more tests which change scroll-timeline-name property.
// Those animations which use this timeline should be restyled propertly.
// -------------------------
// Test scroll-timeline-axis
// -------------------------
test(t => {
let [scroller, target] = createScrollerAndTarget(t);
scroller.style.writingMode = 'vertical-lr';
document.body.appendChild(scroller);
document.body.appendChild(target);
scroller.style.scrollTimeline = 'timeline block';
target.style.animation = "anim 10s linear timeline";
scroller.scrollLeft = 50;
assert_equals(getComputedStyle(target).translate, '100px');
}, 'scroll-timeline-axis is block');
test(t => {
let [scroller, target] = createScrollerAndTarget(t);
scroller.style.writingMode = 'vertical-lr';
document.body.appendChild(scroller);
document.body.appendChild(target);
scroller.style.scrollTimeline = 'timeline inline';
target.style.animation = "anim 10s linear timeline";
scroller.scrollTop = 50;
assert_equals(getComputedStyle(target).translate, '100px');
}, 'scroll-timeline-axis is inline');
test(t => {
let [scroller, target] = createScrollerAndTarget(t);
scroller.style.writingMode = 'vertical-lr';
document.body.appendChild(scroller);
document.body.appendChild(target);
scroller.style.scrollTimeline = 'timeline horizontal';
target.style.animation = "anim 10s linear timeline";
scroller.scrollLeft = 50;
assert_equals(getComputedStyle(target).translate, '100px');
}, 'scroll-timeline-axis is horizontal');
test(t => {
let [scroller, target] = createScrollerAndTarget(t);
scroller.style.writingMode = 'vertical-lr';
document.body.appendChild(scroller);
document.body.appendChild(target);
scroller.style.scrollTimeline = 'timeline vertical';
target.style.animation = "anim 10s linear timeline";
scroller.scrollTop = 50;
assert_equals(getComputedStyle(target).translate, '100px');
}, 'scroll-timeline-axis is vertical');
// TODO: Add more tests which change scroll-timeline-axis property.
// Those animations which use this timeline should be restyled properly.
</script>
</body>