mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 11:25:00 +00:00
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:
parent
f1b5d17847
commit
2b8751f919
@ -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 element’s descendants
|
||||
// 3. that element’s 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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -844,9 +844,13 @@ fn is_default<T: Default + PartialEq>(value: &T) -> bool {
|
||||
pub enum AnimationTimeline {
|
||||
/// Use default timeline. The animation’s 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(
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user