Bug 1866566 - If there is change in over-all relevancy, update HiddenByContentVisibility for animations, r=emilio,hiro

Differential Revision: https://phabricator.services.mozilla.com/D195009
This commit is contained in:
Cathie Chen 2023-12-06 13:36:59 +00:00
parent 1c04160996
commit e88b28e248
24 changed files with 335 additions and 18 deletions

View File

@ -1953,6 +1953,22 @@ void Animation::SetHiddenByContentVisibility(bool hidden) {
GetTimeline()->NotifyAnimationContentVisibilityChanged(this, !hidden);
}
void Animation::UpdateHiddenByContentVisibility() {
// To be consistent with nsIFrame::UpdateAnimationVisibility, here we only
// deal with CSSAnimation and CSSTransition.
if (!AsCSSAnimation() && !AsCSSTransition()) {
return;
}
NonOwningAnimationTarget target = GetTargetForAnimation();
if (!target) {
return;
}
if (auto* frame = target.mElement->GetPrimaryFrame()) {
SetHiddenByContentVisibility(
frame->IsHiddenByContentVisibilityOnAnyAncestor());
}
}
StickyTimeDuration Animation::IntervalStartTime(
const StickyTimeDuration& aActiveDuration) const {
MOZ_ASSERT(AsCSSTransition() || AsCSSAnimation(),

View File

@ -399,6 +399,7 @@ class Animation : public DOMEventTargetHelper,
bool IsHiddenByContentVisibility() const {
return mHiddenByContentVisibility;
}
void UpdateHiddenByContentVisibility();
DocGroup* GetDocGroup();
void SetSyncWithGeometricAnimations() { mSyncWithGeometricAnimations = true; }

View File

@ -112,4 +112,10 @@ void AnimationTimeline::NotifyAnimationContentVisibilityChanged(
}
}
void AnimationTimeline::UpdateHiddenByContentVisibility() {
for (Animation* animation : mAnimations) {
animation->UpdateHiddenByContentVisibility();
}
}
} // namespace mozilla::dom

View File

@ -113,6 +113,7 @@ class AnimationTimeline : public nsISupports, public nsWrapperCache {
virtual void RemoveAnimation(Animation* aAnimation);
virtual void NotifyAnimationContentVisibilityChanged(Animation* aAnimation,
bool aIsVisible);
void UpdateHiddenByContentVisibility();
virtual Document* GetDocument() const = 0;

View File

@ -263,6 +263,17 @@ const nsIScrollableFrame* ScrollTimeline::GetScrollFrame() const {
return nullptr;
}
void ScrollTimeline::NotifyAnimationContentVisibilityChanged(
Animation* aAnimation, bool aIsVisible) {
AnimationTimeline::NotifyAnimationContentVisibilityChanged(aAnimation,
aIsVisible);
if (mAnimationOrder.isEmpty()) {
UnregisterFromScrollSource();
} else {
RegisterWithScrollSource();
}
}
// ------------------------------------
// Methods of ProgressTimelineScheduler
// ------------------------------------

View File

@ -197,6 +197,9 @@ class ScrollTimeline : public AnimationTimeline {
PseudoStyleType aPseudoType,
const StyleScrollTimeline& aNew);
void NotifyAnimationContentVisibilityChanged(Animation* aAnimation,
bool aIsVisible) override;
protected:
virtual ~ScrollTimeline() { Teardown(); }
ScrollTimeline() = delete;

View File

@ -18983,4 +18983,9 @@ RadioGroupContainer& Document::OwnedRadioGroupContainer() {
return *mRadioGroupContainer;
}
void Document::UpdateHiddenByContentVisibilityForAnimations() {
for (AnimationTimeline* timeline : Timelines()) {
timeline->UpdateHiddenByContentVisibility();
}
}
} // namespace mozilla::dom

View File

@ -3034,6 +3034,7 @@ class Document : public nsINode,
DocumentTimeline* Timeline();
LinkedList<DocumentTimeline>& Timelines() { return mTimelines; }
void UpdateHiddenByContentVisibilityForAnimations();
SVGSVGElement* GetSVGRootElement() const;

View File

@ -11950,8 +11950,15 @@ void PresShell::UpdateRelevancyOfContentVisibilityAutoFrames() {
return;
}
bool isRelevantContentChanged = false;
for (nsIFrame* frame : mContentVisibilityAutoFrames) {
frame->UpdateIsRelevantContent(mContentVisibilityRelevancyToUpdate);
isRelevantContentChanged |=
frame->UpdateIsRelevantContent(mContentVisibilityRelevancyToUpdate);
}
if (isRelevantContentChanged) {
if (nsPresContext* presContext = GetPresContext()) {
presContext->UpdateHiddenByContentVisibilityForAnimations();
}
}
mContentVisibilityRelevancyToUpdate.clear();
@ -11985,6 +11992,7 @@ PresShell::ProximityToViewportResult PresShell::DetermineProximityToViewport() {
auto input = DOMIntersectionObserver::ComputeInput(
*mDocument, /* aRoot = */ nullptr, &rootMargin);
bool isRelevantContentChanged = false;
for (nsIFrame* frame : mContentVisibilityAutoFrames) {
auto* element = frame->GetContent()->AsElement();
result.mAnyScrollIntoViewFlag |=
@ -12005,7 +12013,8 @@ PresShell::ProximityToViewportResult PresShell::DetermineProximityToViewport() {
.Intersects();
element->SetVisibleForContentVisibility(intersects);
if (oldVisibility.isNothing() || *oldVisibility != intersects) {
frame->UpdateIsRelevantContent(ContentRelevancyReason::Visible);
isRelevantContentChanged |=
frame->UpdateIsRelevantContent(ContentRelevancyReason::Visible);
}
// 14.2.3.3
@ -12013,6 +12022,11 @@ PresShell::ProximityToViewportResult PresShell::DetermineProximityToViewport() {
result.mHadInitialDetermination = true;
}
}
if (isRelevantContentChanged) {
if (nsPresContext* presContext = GetPresContext()) {
presContext->UpdateHiddenByContentVisibilityForAnimations();
}
}
return result;
}

View File

@ -3077,6 +3077,11 @@ PerformanceMainThread* nsPresContext::GetPerformanceMainThread() const {
return nullptr;
}
void nsPresContext::UpdateHiddenByContentVisibilityForAnimations() {
mDocument->UpdateHiddenByContentVisibilityForAnimations();
TimelineManager()->UpdateHiddenByContentVisibilityForAnimations();
}
#ifdef DEBUG
void nsPresContext::ValidatePresShellAndDocumentReleation() const {

View File

@ -1079,6 +1079,8 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
return mFontPaletteValueSet;
}
void UpdateHiddenByContentVisibilityForAnimations();
protected:
friend class nsRunnableMethod<nsPresContext>;
void ThemeChangedInternal();

View File

@ -7003,7 +7003,7 @@ bool nsIFrame::IsDescendantOfTopLayerElement() const {
return false;
}
void nsIFrame::UpdateIsRelevantContent(
bool nsIFrame::UpdateIsRelevantContent(
const ContentRelevancy& aRelevancyToUpdate) {
MOZ_ASSERT(StyleDisplay()->ContentVisibility(*this) ==
StyleContentVisibility::Auto);
@ -7053,7 +7053,7 @@ void nsIFrame::UpdateIsRelevantContent(
}
if (!overallRelevancyChanged) {
return;
return false;
}
HandleLastRememberedSize();
@ -7075,6 +7075,7 @@ void nsIFrame::UpdateIsRelevantContent(
new AsyncEventDispatcher(element, event.forget());
DebugOnly<nsresult> rv = asyncDispatcher->PostDOMEvent();
NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncEventDispatcher failed to dispatch");
return true;
}
nsresult nsIFrame::CharacterDataChanged(const CharacterDataChangeInfo&) {

View File

@ -3303,8 +3303,10 @@ class nsIFrame : public nsQueryFrame {
* Update the whether or not this frame is considered relevant content for the
* purposes of `content-visibility: auto` according to the rules specified in
* https://drafts.csswg.org/css-contain-2/#relevant-to-the-user.
* Returns true if the over-all relevancy changed.
*/
void UpdateIsRelevantContent(const ContentRelevancy& aRelevancyToUpdate);
[[nodiscard]] bool UpdateIsRelevantContent(
const ContentRelevancy& aRelevancyToUpdate);
/**
* Get the "type" of the frame.

View File

@ -55,6 +55,7 @@ class TimelineCollection final
// if it does not already exist.
static TimelineCollection* Get(const dom::Element* aElement,
PseudoStyleType aPseudoType);
const TimelineMap& Timelines() const { return mTimelines; }
private:
// The element. Weak reference is fine since it owns us.

View File

@ -168,4 +168,20 @@ void TimelineManager::DoUpdateTimelines(
// siblings when mutating {scroll|view}-timeline-name.
}
void TimelineManager::UpdateHiddenByContentVisibilityForAnimations() {
for (auto* scrollTimelineCollection : mScrollTimelineCollections) {
for (ScrollTimeline* timeline :
scrollTimelineCollection->Timelines().Values()) {
timeline->UpdateHiddenByContentVisibility();
}
}
for (auto* viewTimelineCollection : mViewTimelineCollections) {
for (ViewTimeline* timeline :
viewTimelineCollection->Timelines().Values()) {
timeline->UpdateHiddenByContentVisibility();
}
}
}
} // namespace mozilla

View File

@ -51,6 +51,8 @@ class TimelineManager {
const ComputedStyle* aComputedStyle,
ProgressTimelineType aType);
void UpdateHiddenByContentVisibilityForAnimations();
private:
template <typename StyleType, typename TimelineType>
void DoUpdateTimelines(nsPresContext* aPresContext, dom::Element* aElement,

View File

@ -1,2 +0,0 @@
[content-visibility-animation-and-scroll.html]
expected: TIMEOUT

View File

@ -1,13 +1,4 @@
[content-visibility-animation-in-auto-subtree.html]
expected: TIMEOUT
[Animation events do not fire for a CSS animation running in a content visibility subtree]
expected: TIMEOUT
[The finished promise does not resolve due to the normal passage of time for a CSS animation in a content visibility subtree]
expected: NOTRUN
[The finished promise does not resolve due to the normal passage of time for a CSS transition in a content visibility subtree]
expected: NOTRUN
[Events and promises are handled normally for animations without an owning element]
expected: NOTRUN
expected: TIMEOUT

View File

@ -0,0 +1,2 @@
[content-visibility-animation-with-scroll-timeline-in-auto-subtree.html]
prefs: [layout.css.scroll-driven-animations.enabled:true]

View File

@ -0,0 +1,2 @@
[content-visibility-animation-with-scroll-timeline-in-hidden-subtree.html]
prefs: [layout.css.scroll-driven-animations.enabled:true]

View File

@ -14,7 +14,7 @@
to { opacity: 0; }
}
#target {
background: 'green';
background: green;
height: 100px;
width: 100px;
}

View File

@ -0,0 +1,82 @@
<!DOCTYPE html>
<meta charset=utf8>
<title>Test getComputedStyle on a CSS animation with scroll timeline in a content visibility subtree using content-visibility: auto</title>
<link rel="help" href="https://drafts.csswg.org/css-contain-2/">
<script src="/web-animations/testcommon.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
#container {
content-visibility: auto;
}
#scrollContainer {
height: 100vh;
overflow-y: scroll;
scroll-timeline-name: --targetTimeline;
}
#innerspacer {
height: 300vh;
}
@keyframes fade {
from { opacity: 1; }
to { opacity: 0; }
}
#target {
background: green;
height: 100px;
width: 100px;
}
.animate {
animation-name: fade;
animation-duration: 1ms;
animation-direction: alternate;
animation-timeline: --targetTimeline;
}
</style>
<body>
<div id="log"></div>
<div id="spacer"></div>
<div id="scrollContainer">
<div id="container"></div>
<div id="innerspacer"></div>
</div>
</body>
<script>
"use strict";
function createAnimatingElement(test, name) {
const container = document.getElementById('container');
const target = document.createElement('div');
container.appendChild(target);
target.id = 'target';
target.className = name;
return target;
}
promise_test(async t => {
const container = document.getElementById('container');
const target = createAnimatingElement(t, 'animate');
scrollContainer.scrollTop = 10000;
const animation = target.getAnimations()[0];
await animation.ready;
await waitForAnimationFrames(1);
let expectedOpacity = parseFloat(getComputedStyle(target).opacity);
assert_approx_equals(expectedOpacity, 0, 0.1, 'scrollContainer scrolls to bottom, so the opacity should be 0');
document.getElementById('spacer').style.height = '300vh';
await waitForAnimationFrames(1);
assert_equals(parseFloat(getComputedStyle(target).opacity), expectedOpacity, 'Opacity does not change when it is hidden by c-v');
scrollContainer.scrollTop = 0;
assert_equals(parseFloat(getComputedStyle(target).opacity), expectedOpacity, 'The animation is hidden by c-v, so opacity does not change even if scrollTop changes');
await waitForAnimationFrames(2);
document.getElementById('spacer').style.height = '0vh';
await waitForAnimationFrames(2);
assert_equals(getComputedStyle(target).opacity, '1', 'Now that the animation is visible, opacity should be updated');
}, 'Animation with scroll-timeline should be affected c-v');
</script>

View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<meta charset=utf8>
<title>Test getComputedStyle on a CSS animation with scroll-timeline in a content-visibility subtree</title>
<link rel="help" href="https://drafts.csswg.org/css-contain-2/">
<script src="/web-animations/testcommon.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
#container {
content-visibility: visible;
}
#scrollContainer {
height: 100vh;
overflow-y: scroll;
scroll-timeline-name: --targetTimeline;
}
#innerspacer {
height: 300vh;
}
@keyframes fade {
from { opacity: 1; }
to { opacity: 0; }
}
#target {
background: green;
height: 100px;
width: 100px;
}
.animate {
animation-name: fade;
animation-duration: 1ms;
animation-direction: alternate;
animation-timeline: --targetTimeline;
}
</style>
<body>
<div id="log"></div>
<div id="scrollContainer">
<div id="container"></div>
<div id="innerspacer"></div>
</div>
</body>
<script>
"use strict";
function createAnimatingElement(test, name) {
const container = document.getElementById('container');
const target = document.createElement('div');
container.appendChild(target);
target.id = 'target';
target.className = name;
return target;
}
promise_test(async t => {
const container = document.getElementById('container');
const target = createAnimatingElement(t, 'animate');
scrollContainer.scrollTop = 10000;
const animation = target.getAnimations()[0];
await animation.ready;
await waitForAnimationFrames(1);
let expectedOpacity = parseFloat(getComputedStyle(target).opacity);
assert_approx_equals(expectedOpacity, 0, 0.1, 'scrollContainer scrolls to bottom, so the opacity should be 0');
document.getElementById('container').style.contentVisibility = 'hidden';
await waitForAnimationFrames(1);
assert_equals(parseFloat(getComputedStyle(target).opacity), expectedOpacity, 'Opacity does not change when it is hidden by c-v');
scrollContainer.scrollTop = 0;
assert_equals(parseFloat(getComputedStyle(target).opacity), expectedOpacity, 'The animation is hidden by c-v, so opacity does not change even if scrollTop changes');
await waitForAnimationFrames(2);
document.getElementById('container').style.contentVisibility = 'visible';
await waitForAnimationFrames(2);
assert_approx_equals(parseFloat(getComputedStyle(target).opacity), 1, 0.1, 'Now that the animation is visible, opacity should be updated');
}, 'Animation with scroll-timeline should be affected c-v');
</script>

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<meta charset=utf8>
<title>Web Animation does not stop even if target is hidden by c-v</title>
<link rel="help" href="https://drafts.csswg.org/css-contain-2/">
<script src="/web-animations/testcommon.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
#container {
content-visibility: auto;
}
@keyframes fade {
from { opacity: 1; }
to { opacity: 0; }
}
#target {
background: green;
height: 100px;
width: 100px;
}
.animate {
animation: fade 1s linear 2 alternate;
}
.transition {
transition: opacity 1s linear;
}
</style>
<body>
<div id="spacer"></div>
<div id="container"></div>
</body>
<script>
"use strict";
function createElementWithWebAnimation(test) {
const container = document.getElementById('container');
const target = document.createElement('div');
container.appendChild(target);
target.id = 'target';
const keyframes = [
{ opacity: 1 },
{ opacity: 0 },
];
const options = {
duration: 2000,
iterations: 1,
easing: 'linear',
direction: 'alternate',
};
target.animate(keyframes, options);
return target;
}
promise_test(async t => {
// Make sure the target is hidden from the beginning.
document.getElementById("spacer").style.height = "300vh";
const target = createElementWithWebAnimation(t);
const animation = target.getAnimations()[0];
let animationFinishEvent = false;
animation.addEventListener('finish', () => {
animationFinishEvent = true;
});
animation.currentTime = 1999;
await animation.ready;
await waitForAnimationFrames(2);
assert_true(animationFinishEvent,
'Web Animation event should keep going even if target is hidden by c-v');
}, 'Web Animation does not stop even if target is hidden by c-v');
</script>