Commit 0c2d7260 authored by Anders Hartvoll Ruud's avatar Anders Hartvoll Ruud Committed by Commit Bot

[scroll-animations] Handle changes to selector(#id) references

An @scroll-timeline rule may reference elements with selector(#id)
syntax. Whenever the element represented by the selector() function
changes, the "effective" CSSScrollTimeline produced by the
@scroll-timeline rule changes as well.

This CL solves this problem by registering IdTargetObservers
for the IDs a CSSScrollTimeline depends on. The IdTargetObservers
are registered when at least one Animation is attached, and
unregistered when the last Animation is detached. This is
similar to how the ScrollTimeline itself is registered/
unregistered with the resolved scroll source when Animations
attach/detach.

Marking the animation target elements for style recalc
(non-animation-style-change) means we'll re-evaluate the
@scroll-timeline rule against the current DOM state, and consider
whether or not we need a new CSSScrollTimeline for the animation.

Bug: 1074052
Change-Id: Iab0c6b8d8b57d7d63283e97355ee5d9948b831a0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2356506
Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org>
Reviewed-by: default avatarKevin Ellis <kevers@chromium.org>
Reviewed-by: default avatarFredrik Söderquist <fs@opera.com>
Reviewed-by: default avatarRune Lillesveen <futhark@chromium.org>
Cr-Commit-Position: refs/heads/master@{#821724}
parent 14227268
...@@ -2261,6 +2261,19 @@ void Animation::InvalidateKeyframeEffect(const TreeScope& tree_scope) { ...@@ -2261,6 +2261,19 @@ void Animation::InvalidateKeyframeEffect(const TreeScope& tree_scope) {
} }
} }
void Animation::InvalidateEffectTargetStyle() {
auto* keyframe_effect = DynamicTo<KeyframeEffect>(content_.Get());
if (!keyframe_effect)
return;
Element* target = keyframe_effect->EffectTarget();
if (target) {
// TODO(andruud): Should we add a new style_change_reason?
target->SetNeedsStyleRecalc(kLocalStyleChange,
StyleChangeReasonForTracing::Create(
style_change_reason::kScrollTimeline));
}
}
void Animation::ResolvePromiseMaybeAsync(AnimationPromise* promise) { void Animation::ResolvePromiseMaybeAsync(AnimationPromise* promise) {
if (ScriptForbiddenScope::IsScriptForbidden()) { if (ScriptForbiddenScope::IsScriptForbidden()) {
GetExecutionContext() GetExecutionContext()
......
...@@ -269,6 +269,7 @@ class CORE_EXPORT Animation : public EventTargetWithInlineData, ...@@ -269,6 +269,7 @@ class CORE_EXPORT Animation : public EventTargetWithInlineData,
void SetEffectSuppressed(bool); void SetEffectSuppressed(bool);
void InvalidateKeyframeEffect(const TreeScope&); void InvalidateKeyframeEffect(const TreeScope&);
void InvalidateEffectTargetStyle();
void Trace(Visitor*) const override; void Trace(Visitor*) const override;
......
...@@ -13,6 +13,7 @@ blink_core_tests_animation = [ ...@@ -13,6 +13,7 @@ blink_core_tests_animation = [
"animation_utils_test.cc", "animation_utils_test.cc",
"compositor_animations_test.cc", "compositor_animations_test.cc",
"css/css_animations_test.cc", "css/css_animations_test.cc",
"css/css_scroll_timeline_test.cc",
"css/css_transition_data_test.cc", "css/css_transition_data_test.cc",
"document_animations_test.cc", "document_animations_test.cc",
"document_timeline_test.cc", "document_timeline_test.cc",
......
...@@ -145,6 +145,57 @@ base::Optional<double> ComputeTimeRange(const CSSValue* value) { ...@@ -145,6 +145,57 @@ base::Optional<double> ComputeTimeRange(const CSSValue* value) {
return base::nullopt; return base::nullopt;
} }
class ElementReferenceObserver : public IdTargetObserver {
public:
ElementReferenceObserver(Document* document,
const AtomicString& id,
CSSScrollTimeline* timeline)
: IdTargetObserver(document->GetIdTargetObserverRegistry(), id),
timeline_(timeline) {}
void Trace(Visitor* visitor) const override {
visitor->Trace(timeline_);
IdTargetObserver::Trace(visitor);
}
private:
void IdTargetChanged() override {
if (timeline_)
timeline_->InvalidateEffectTargetStyle();
}
WeakMember<CSSScrollTimeline> timeline_;
};
HeapVector<Member<IdTargetObserver>> CreateElementReferenceObservers(
Document* document,
StyleRuleScrollTimeline* rule,
CSSScrollTimeline* timeline) {
HeapVector<Member<IdTargetObserver>> observers;
if (const auto* id = GetIdSelectorValue(rule->GetSource())) {
observers.push_back(MakeGarbageCollected<ElementReferenceObserver>(
document, id->Id(), timeline));
}
// TODO(crbug.com/1094014): The 'offsets' descriptor will replace the 'start'
// and 'end' descriptors eventually.
HeapVector<Member<const CSSValue>> offsets = {rule->GetStart(),
rule->GetEnd()};
for (const CSSValue* offset : offsets) {
const auto* element_offset =
DynamicTo<cssvalue::CSSElementOffsetValue>(offset);
if (!element_offset)
continue;
if (const auto* id = GetIdSelectorValue(element_offset->Target())) {
observers.push_back(MakeGarbageCollected<ElementReferenceObserver>(
document, id->Id(), timeline));
}
}
return observers;
}
} // anonymous namespace } // anonymous namespace
CSSScrollTimeline::Options::Options(Element* element, CSSScrollTimeline::Options::Options(Element* element,
...@@ -154,15 +205,18 @@ CSSScrollTimeline::Options::Options(Element* element, ...@@ -154,15 +205,18 @@ CSSScrollTimeline::Options::Options(Element* element,
offsets_(ComputeScrollOffsets(element->GetDocument(), offsets_(ComputeScrollOffsets(element->GetDocument(),
rule.GetStart(), rule.GetStart(),
rule.GetEnd())), rule.GetEnd())),
time_range_(ComputeTimeRange(rule.GetTimeRange())) {} time_range_(ComputeTimeRange(rule.GetTimeRange())),
rule_(&rule) {}
CSSScrollTimeline::CSSScrollTimeline(Document* document, const Options& options) CSSScrollTimeline::CSSScrollTimeline(Document* document, const Options& options)
: ScrollTimeline(document, : ScrollTimeline(document,
options.source_, options.source_,
options.direction_, options.direction_,
options.offsets_, options.offsets_,
*options.time_range_) { *options.time_range_),
rule_(options.rule_) {
DCHECK(options.IsValid()); DCHECK(options.IsValid());
DCHECK(rule_);
} }
bool CSSScrollTimeline::Matches(const Options& options) const { bool CSSScrollTimeline::Matches(const Options& options) const {
...@@ -170,7 +224,32 @@ bool CSSScrollTimeline::Matches(const Options& options) const { ...@@ -170,7 +224,32 @@ bool CSSScrollTimeline::Matches(const Options& options) const {
return (scrollSource() == options.source_) && return (scrollSource() == options.source_) &&
(GetOrientation() == options.direction_) && (GetOrientation() == options.direction_) &&
(ScrollOffsetsEqual(*options.offsets_)) && (ScrollOffsetsEqual(*options.offsets_)) &&
(GetTimeRange() == options.time_range_); (GetTimeRange() == options.time_range_) && (rule_ == options.rule_);
}
void CSSScrollTimeline::AnimationAttached(Animation* animation) {
ScrollTimeline::AnimationAttached(animation);
if (AttachedAnimationsCount() == 1)
SetObservers(CreateElementReferenceObservers(GetDocument(), rule_, this));
}
void CSSScrollTimeline::AnimationDetached(Animation* animation) {
ScrollTimeline::AnimationDetached(animation);
if (AttachedAnimationsCount() == 0)
SetObservers(HeapVector<Member<IdTargetObserver>>());
}
void CSSScrollTimeline::Trace(Visitor* visitor) const {
visitor->Trace(rule_);
visitor->Trace(observers_);
ScrollTimeline::Trace(visitor);
}
void CSSScrollTimeline::SetObservers(
HeapVector<Member<IdTargetObserver>> observers) {
for (IdTargetObserver* observer : observers_)
observer->Unregister();
observers_ = std::move(observers);
} }
} // namespace blink } // namespace blink
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#include "base/optional.h" #include "base/optional.h"
#include "third_party/blink/renderer/core/animation/scroll_timeline.h" #include "third_party/blink/renderer/core/animation/scroll_timeline.h"
#include "third_party/blink/renderer/core/dom/id_target_observer.h"
namespace blink { namespace blink {
...@@ -34,6 +35,7 @@ class CORE_EXPORT CSSScrollTimeline : public ScrollTimeline { ...@@ -34,6 +35,7 @@ class CORE_EXPORT CSSScrollTimeline : public ScrollTimeline {
ScrollTimeline::ScrollDirection direction_; ScrollTimeline::ScrollDirection direction_;
HeapVector<Member<ScrollTimelineOffset>>* offsets_; HeapVector<Member<ScrollTimelineOffset>>* offsets_;
base::Optional<double> time_range_; base::Optional<double> time_range_;
StyleRuleScrollTimeline* rule_;
}; };
CSSScrollTimeline(Document*, const Options&); CSSScrollTimeline(Document*, const Options&);
...@@ -42,6 +44,16 @@ class CORE_EXPORT CSSScrollTimeline : public ScrollTimeline { ...@@ -42,6 +44,16 @@ class CORE_EXPORT CSSScrollTimeline : public ScrollTimeline {
// AnimationTimeline implementation. // AnimationTimeline implementation.
bool IsCSSScrollTimeline() const override { return true; } bool IsCSSScrollTimeline() const override { return true; }
void AnimationAttached(Animation*) override;
void AnimationDetached(Animation*) override;
void Trace(Visitor*) const override;
private:
void SetObservers(HeapVector<Member<IdTargetObserver>>);
Member<StyleRuleScrollTimeline> rule_;
HeapVector<Member<IdTargetObserver>> observers_;
}; };
template <> template <>
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/animation/css/css_scroll_timeline.h"
#include "third_party/blink/renderer/core/dom/id_target_observer.h"
#include "third_party/blink/renderer/core/dom/id_target_observer_registry.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_style_element.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/platform/heap/thread_state.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
namespace blink {
class CSSScrollTimelineTest : public PageTestBase,
private ScopedCSSScrollTimelineForTest {
public:
CSSScrollTimelineTest() : ScopedCSSScrollTimelineForTest(true) {}
bool HasObservers(const AtomicString& id) {
return GetDocument().GetIdTargetObserverRegistry().HasObservers(id);
}
};
TEST_F(CSSScrollTimelineTest, IdObserverElementRemoval) {
ASSERT_FALSE(HasObservers("scroller"));
SetBodyInnerHTML(R"HTML(
<style>
@keyframes anim {
from { width: 100px; }
to { width: 200px; }
}
@scroll-timeline timeline {
source: selector(#scroller);
time-range: 10s;
}
div {
animation: anim 10s;
animation-timeline: timeline;
}
</style>
<div id=element1></div>
<div id=element2></div>
)HTML");
EXPECT_TRUE(HasObservers("scroller"));
Element* element1 = GetDocument().getElementById("element1");
Element* element2 = GetDocument().getElementById("element2");
ASSERT_TRUE(element1);
ASSERT_TRUE(element2);
element1->remove();
UpdateAllLifecyclePhasesForTest();
ThreadState::Current()->CollectAllGarbageForTesting();
EXPECT_TRUE(HasObservers("scroller"));
element2->remove();
UpdateAllLifecyclePhasesForTest();
ThreadState::Current()->CollectAllGarbageForTesting();
EXPECT_FALSE(HasObservers("scroller"));
}
TEST_F(CSSScrollTimelineTest, IdObserverRuleInsertion) {
ASSERT_FALSE(HasObservers("scroller1"));
ASSERT_FALSE(HasObservers("scroller2"));
ASSERT_FALSE(HasObservers("scroller3"));
ASSERT_FALSE(HasObservers("redefined"));
ASSERT_FALSE(HasObservers("offset1"));
ASSERT_FALSE(HasObservers("offset2"));
SetBodyInnerHTML(R"HTML(
<style>
@keyframes anim {
from { width: 100px; }
to { width: 200px; }
}
@scroll-timeline timeline1 {
source: selector(#scroller1);
time-range: 10s;
}
@scroll-timeline timeline2 {
source: selector(#scroller2);
time-range: 10s;
start: selector(#offset1);
}
div {
animation: anim 10s;
}
#element1 {
animation-timeline: timeline1;
}
#element2 {
animation-timeline: timeline2;
}
</style>
<div id=element1></div>
<div id=element2></div>
<div id=element3></div>
)HTML");
EXPECT_TRUE(HasObservers("scroller1"));
EXPECT_TRUE(HasObservers("scroller2"));
EXPECT_TRUE(HasObservers("offset1"));
Element* element1 = GetDocument().getElementById("element1");
Element* element2 = GetDocument().getElementById("element2");
ASSERT_TRUE(element1);
ASSERT_TRUE(element2);
// Insert a <style> element which redefines timeline2, and also
// creates an additional timeline (timeline3).
auto* style_element = MakeGarbageCollected<HTMLStyleElement>(
GetDocument(), CreateElementFlags());
style_element->setTextContent(R"CSS(
@scroll-timeline timeline2 {
source: selector(#redefined);
time-range: 10s;
start: selector(#offset2);
}
@scroll-timeline timeline3 {
source: selector(#scroller3);
time-range: 10s;
}
#element3 {
animation-timeline: timeline3;
}
)CSS");
GetDocument().body()->AppendChild(style_element);
UpdateAllLifecyclePhasesForTest();
ThreadState::Current()->CollectAllGarbageForTesting();
EXPECT_TRUE(HasObservers("scroller1"));
EXPECT_FALSE(HasObservers("scroller2"));
EXPECT_TRUE(HasObservers("scroller3"));
EXPECT_TRUE(HasObservers("redefined"));
EXPECT_FALSE(HasObservers("offset1"));
EXPECT_TRUE(HasObservers("offset2"));
// Remove the <style> element again.
style_element->remove();
UpdateAllLifecyclePhasesForTest();
ThreadState::Current()->CollectAllGarbageForTesting();
EXPECT_TRUE(HasObservers("scroller1"));
EXPECT_TRUE(HasObservers("scroller2"));
EXPECT_FALSE(HasObservers("scroller3"));
EXPECT_FALSE(HasObservers("redefined"));
EXPECT_TRUE(HasObservers("offset1"));
EXPECT_FALSE(HasObservers("offset2"));
}
} // namespace blink
...@@ -561,6 +561,11 @@ void ScrollTimeline::Invalidate(Node* node) { ...@@ -561,6 +561,11 @@ void ScrollTimeline::Invalidate(Node* node) {
} }
} }
void ScrollTimeline::InvalidateEffectTargetStyle() {
for (Animation* animation : scroll_animations_)
animation->InvalidateEffectTargetStyle();
}
CompositorAnimationTimeline* ScrollTimeline::EnsureCompositorTimeline() { CompositorAnimationTimeline* ScrollTimeline::EnsureCompositorTimeline() {
if (compositor_timeline_) if (compositor_timeline_)
return compositor_timeline_.get(); return compositor_timeline_.get();
......
...@@ -90,6 +90,10 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -90,6 +90,10 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline {
// This may lead the timeline to request a new animation frame. // This may lead the timeline to request a new animation frame.
virtual void Invalidate(); virtual void Invalidate();
// Mark every effect target of every Animation attached to this timeline
// for style recalc.
void InvalidateEffectTargetStyle();
CompositorAnimationTimeline* EnsureCompositorTimeline() override; CompositorAnimationTimeline* EnsureCompositorTimeline() override;
void UpdateCompositorTimeline() override; void UpdateCompositorTimeline() override;
...@@ -118,6 +122,7 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -118,6 +122,7 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline {
double GetTimeRange() const { return time_range_; } double GetTimeRange() const { return time_range_; }
bool ScrollOffsetsEqual( bool ScrollOffsetsEqual(
const HeapVector<Member<ScrollTimelineOffset>>& other) const; const HeapVector<Member<ScrollTimelineOffset>>& other) const;
size_t AttachedAnimationsCount() const { return scroll_animations_.size(); }
private: private:
// https://wicg.github.io/scroll-animations/#avoiding-cycles // https://wicg.github.io/scroll-animations/#avoiding-cycles
......
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_DOM_ID_TARGET_OBSERVER_REGISTRY_H_ #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_DOM_ID_TARGET_OBSERVER_REGISTRY_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_DOM_ID_TARGET_OBSERVER_REGISTRY_H_ #define THIRD_PARTY_BLINK_RENDERER_CORE_DOM_ID_TARGET_OBSERVER_REGISTRY_H_
#include "base/macros.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/platform/heap/handle.h" #include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/wtf/forward.h" #include "third_party/blink/renderer/platform/wtf/forward.h"
#include "third_party/blink/renderer/platform/wtf/hash_map.h" #include "third_party/blink/renderer/platform/wtf/hash_map.h"
...@@ -36,7 +38,7 @@ namespace blink { ...@@ -36,7 +38,7 @@ namespace blink {
class IdTargetObserver; class IdTargetObserver;
class IdTargetObserverRegistry final class CORE_EXPORT IdTargetObserverRegistry final
: public GarbageCollected<IdTargetObserverRegistry> { : public GarbageCollected<IdTargetObserverRegistry> {
friend class IdTargetObserver; friend class IdTargetObserver;
......
<!DOCTYPE html>
<title>@scroll-timeline element offset invalidation</title>
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#typedef-element-offset">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<style>
#scroller {
overflow: scroll;
width: 100px;
height: 100px;
}
#scroller > div {
height: 50px;
}
@keyframes expand {
from { width: 100px; }
to { width: 200px; }
}
@scroll-timeline timeline {
source: selector(#scroller);
time-range: 1e10s;
start: selector(#offset1) end;
end: selector(#offset2) end;
}
#element {
width: 0px;
height: 20px;
animation: expand 1e10s linear;
animation-timeline: timeline;
}
/* Ensure stable expectations if feature is not supported */
@supports not (animation-timeline:foo) {
#element { animation-play-state: paused; }
}
</style>
<div id=scroller></div>
<div id=element></div>
<p class=sibling1></p>
<p class=sibling2></p>
<script>
function setup() {
while (scroller.firstChild)
scroller.firstChild.remove();
for (let i = 0; i < 10; i++)
scroller.append(document.createElement('div'));
}
// The contents of the scroller look like this:
//
// +-------+
// | 50px | div (0)
// +-------+
// +-------+
// | 50px | div (1)
// +-------+
// +-------+
// | 50px | div (2)
// +-------+
// +-------+
// | 50px | div (3)
// +-------+
// +-------+
// | 50px | div (4)
// +-------+
// +-------+
// | 50px | div (5)
// +-------+
// +-------+
// | 50px | div (6)
// +-------+
// +-------+
// | 50px | div (7)
// +-------+
// +-------+
// | 50px | div (8)
// +-------+
// +-------+
// | 50px | div (9)
// +-------+
//
// The height of the scrollport is 100px.
function invalidation_test(func, description) {
promise_test(async (t) => {
setup();
await func();
}, description);
}
function remove(id) {
let old_element = document.getElementById(id);
if (old_element)
old_element.removeAttribute('id');
}
function reassign(id, element) {
remove(id);
element.setAttribute('id', id);
}
async function assert_element_width_at_scroll(expected_width, scroll) {
scroller.scrollTop = scroll;
await waitForNextFrame();
assert_equals(getComputedStyle(element).width, expected_width);
}
invalidation_test(async () => {
await assert_element_width_at_scroll('0px', 0);
}, 'Offsets missing');
invalidation_test(async () => {
// [50, 150]
reassign('offset1', scroller.children[3]);
reassign('offset2', scroller.children[5]);
await assert_element_width_at_scroll('150px', 100);
// [100, 150]
reassign('offset1', scroller.children[4]);
await assert_element_width_at_scroll('100px', 100);
}, 'Change first offset');
invalidation_test(async () => {
// [50, 150]
reassign('offset1', scroller.children[3]);
reassign('offset2', scroller.children[5]);
await assert_element_width_at_scroll('150px', 100);
// [50, 250]
reassign('offset2', scroller.children[7]);
await assert_element_width_at_scroll('125px', 100);
}, 'Change second offset');
invalidation_test(async () => {
// [50, 250]
reassign('offset1', scroller.children[3]);
reassign('offset2', scroller.children[7]);
await assert_element_width_at_scroll('125px', 100);
// [0, 200]
reassign('offset1', scroller.children[2]);
reassign('offset2', scroller.children[4]);
await assert_element_width_at_scroll('150px', 50);
}, 'Change both offsets');
invalidation_test(async () => {
// [50, 150]
reassign('offset1', scroller.children[3]);
reassign('offset2', scroller.children[5]);
await assert_element_width_at_scroll('150px', 100);
remove('offset1');
await assert_element_width_at_scroll('0px', 0);
}, 'Remove first offset');
invalidation_test(async () => {
// [50, 150]
reassign('offset1', scroller.children[3]);
reassign('offset2', scroller.children[5]);
await assert_element_width_at_scroll('150px', 100);
remove('offset2');
await assert_element_width_at_scroll('0px', 0);
}, 'Remove second offset');
invalidation_test(async () => {
// [50, 150]
reassign('offset1', scroller.children[3]);
reassign('offset2', scroller.children[5]);
await assert_element_width_at_scroll('150px', 100);
remove('offset1');
remove('offset2');
await assert_element_width_at_scroll('0px', 0);
}, 'Remove both offsets');
invalidation_test(async () => {
// [50, 150]
reassign('offset1', scroller.children[3]);
reassign('offset2', scroller.children[5]);
await assert_element_width_at_scroll('150px', 100);
reassign('offset1', document.querySelector('.sibling1'));
await assert_element_width_at_scroll('0px', 0);
}, 'Reassign first offset to sibling of scroller');
invalidation_test(async () => {
// [50, 150]
reassign('offset1', scroller.children[3]);
reassign('offset2', scroller.children[5]);
await assert_element_width_at_scroll('150px', 100);
reassign('offset2', document.querySelector('.sibling2'));
await assert_element_width_at_scroll('0px', 0);
}, 'Reassign second offset to sibling of scroller');
invalidation_test(async () => {
// [50, 150]
reassign('offset1', scroller.children[3]);
reassign('offset2', scroller.children[5]);
await assert_element_width_at_scroll('150px', 100);
reassign('offset1', document.querySelector('.sibling1'));
reassign('offset2', document.querySelector('.sibling2'));
await assert_element_width_at_scroll('0px', 0);
}, 'Reassign both offsets to sibling of scroller');
</script>
<!DOCTYPE html>
<title>@scroll-timeline source invalidation</title>
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-at-rule">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<style>
#scrollers {
overflow: hidden;
height: 0;
}
.scroller {
overflow: scroll;
width: 100px;
height: 100px;
}
.contents {
height: 200px;
}
@keyframes expand {
from { width: 100px; }
to { width: 200px; }
}
@scroll-timeline timeline {
source: selector(#scroller);
time-range: 1e10s;
start: 0px;
end: 100px;
}
#element {
width: 0px;
height: 20px;
animation: expand 1e10s linear;
animation-timeline: timeline;
}
/* Ensure stable expectations if feature is not supported */
@supports not (animation-timeline:foo) {
#element { animation-play-state: paused; }
}
</style>
<div id=scrollers></div>
<div id=element></div>
<script>
function createScroller() {
let scroller = document.createElement('div');
let contents = document.createElement('div');
scroller.classList.add('scroller');
contents.classList.add('contents');
scroller.append(contents);
return scroller;
}
function wrapInDiv(element) {
let div = document.createElement('div');
div.append(element);
return div;
}
function scrollerAt(n) {
return document.querySelectorAll('.scroller')[n];
}
// Resets #scrollers to a state where it has three .scroller children with
// scrollTop offsets 10, 20 and 30.
function cleanup() {
while (scrollers.firstChild)
scrollers.firstChild.remove();
for (let i = 0; i < 3; i++)
scrollers.append(createScroller());
scrollerAt(0).scrollTop = 10;
scrollerAt(1).scrollTop = 20;
scrollerAt(2).scrollTop = 30;
}
// Do an initial "cleanup" to set up the first test.
cleanup();
function invalidation_test(func, description) {
promise_test(async (t) => {
t.add_cleanup(cleanup);
await func();
}, description);
}
invalidation_test(() => {
assert_equals(getComputedStyle(element).width, '0px');
}, 'Nonexistent source');
invalidation_test(() => {
assert_equals(getComputedStyle(element).width, '0px');
scrollerAt(0).setAttribute('id', 'scroller');
assert_equals(getComputedStyle(element).width, '110px');
scrollerAt(1).setAttribute('id', 'scroller'); // No effect
assert_equals(getComputedStyle(element).width, '110px');
scrollerAt(2).setAttribute('id', 'scroller'); // No effect
assert_equals(getComputedStyle(element).width, '110px');
}, 'Setting id attribute');
invalidation_test(() => {
assert_equals(getComputedStyle(element).width, '0px');
scrollerAt(0).setAttribute('id', 'scroller');
assert_equals(getComputedStyle(element).width, '110px');
scrollerAt(0).removeAttribute('id');
assert_equals(getComputedStyle(element).width, '0px');
}, 'Removing id attribute');
invalidation_test(() => {
assert_equals(getComputedStyle(element).width, '0px');
scrollerAt(2).setAttribute('id', 'scroller');
assert_equals(getComputedStyle(element).width, '130px');
scrollerAt(1).setAttribute('id', 'scroller');
assert_equals(getComputedStyle(element).width, '120px');
scrollerAt(0).setAttribute('id', 'scroller');
assert_equals(getComputedStyle(element).width, '110px');
}, 'Setting id attribute earlier in the tree');
invalidation_test(async () => {
assert_equals(getComputedStyle(element).width, '0px');
// Appending a new element with id 'scroller' already set before
// insertion into the tree.
let scroller = createScroller();
scroller.setAttribute('id', 'scroller');
scrollers.append(scroller);
// Make sure |scroller| has a layout box.
//
// https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles
//
// TODO: Depending on the outcome of Issue 5261, the call to offsetTop
// might be unnecessary.
// https://github.com/w3c/csswg-drafts/issues/5261
scroller.offsetTop;
await waitForNextFrame();
assert_equals(getComputedStyle(element).width, '100px');
}, 'Appending a new element');
invalidation_test(async () => {
assert_equals(getComputedStyle(element).width, '0px');
let scroller = createScroller();
scroller.setAttribute('id', 'scroller');
scrollers.append(wrapInDiv(wrapInDiv(scroller)));
await waitForNextFrame();
assert_equals(getComputedStyle(element).width, '100px');
}, 'Inserting a subtree with #scroller descendant');
invalidation_test(() => {
assert_equals(getComputedStyle(element).width, '0px');
scrollerAt(0).setAttribute('id', 'scroller');
scrollerAt(1).setAttribute('id', 'scroller');
scrollerAt(2).setAttribute('id', 'scroller');
assert_equals(getComputedStyle(element).width, '110px');
scrollerAt(0).remove();
assert_equals(getComputedStyle(element).width, '120px');
scrollerAt(0).remove();
assert_equals(getComputedStyle(element).width, '130px');
scrollerAt(0).remove();
assert_equals(getComputedStyle(element).width, '0px');
}, 'Removing source element');
invalidation_test(async () => {
assert_equals(getComputedStyle(element).width, '0px');
// Create a chain: #scrollers -> div -> div -> #scroller
let scroller = createScroller();
let div = wrapInDiv(wrapInDiv(scroller));
scrollers.append(div);
scroller.setAttribute('id', 'scroller');
scroller.scrollTop = 50;
await waitForNextFrame();
assert_equals(getComputedStyle(element).width, '150px');
div.remove();
assert_equals(getComputedStyle(element).width, '0px');
}, 'Removing ancestor of source element');
</script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment