-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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 <[email protected]> Reviewed-by: Kevin Ellis <[email protected]> Reviewed-by: Fredrik Söderquist <[email protected]> Reviewed-by: Rune Lillesveen <[email protected]> Cr-Commit-Position: refs/heads/master@{#821724}
- Loading branch information
1 parent
6770e23
commit 326f137
Showing
2 changed files
with
395 additions
and
0 deletions.
There are no files selected for viewing
208 changes: 208 additions & 0 deletions
208
scroll-animations/css/at-scroll-timeline-offset-invalidation.tentative.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
<!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> |
187 changes: 187 additions & 0 deletions
187
scroll-animations/css/at-scroll-timeline-source-invalidation.tentative.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
<!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> |