mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-12-05 01:10:24 +00:00
LibWeb: Correctly sort animations returned by getAnimations()
This commit is contained in:
committed by
Jelle Raaijmakers
parent
2c389ae96c
commit
b7a71ca950
Notes:
github-actions[bot]
2025-11-10 17:30:34 +00:00
Author: https://github.com/Psychpsyo Commit: https://github.com/LadybirdBrowser/ladybird/commit/b7a71ca9503 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/6777 Reviewed-by: https://github.com/gmta ✅
@@ -627,11 +627,12 @@ void KeyframeEffect::generate_initial_and_final_frames(RefPtr<KeyFrameSet> keyfr
|
||||
// https://www.w3.org/TR/web-animations-1/#animation-composite-order
|
||||
int KeyframeEffect::composite_order(GC::Ref<KeyframeEffect> a, GC::Ref<KeyframeEffect> b)
|
||||
{
|
||||
// 1. Let the associated animation of an animation effect be the animation associated with the animation effect.
|
||||
// The relative composite order of any two keyframe effects A and B within an effect stack is established by
|
||||
// comparing their properties as follows:
|
||||
auto a_animation = a->associated_animation();
|
||||
auto b_animation = b->associated_animation();
|
||||
|
||||
// 2. Sort A and B by applying the following conditions in turn until the order is resolved,
|
||||
// 1. Sort A and B by applying the following conditions in turn until the order is resolved,
|
||||
|
||||
// 1. If A and B’s associated animations differ by class, sort by any inter-class composite order defined for
|
||||
// the corresponding classes.
|
||||
|
||||
@@ -5472,12 +5472,7 @@ void Document::remove_replaced_animations()
|
||||
|
||||
WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> Document::get_animations()
|
||||
{
|
||||
Vector<GC::Ref<Animations::Animation>> relevant_animations;
|
||||
TRY(for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
|
||||
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
|
||||
return IterationDecision::Continue;
|
||||
}));
|
||||
return relevant_animations;
|
||||
return calculate_get_animations(*this);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/Animations/Animation.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/Utils.h>
|
||||
|
||||
@@ -47,4 +48,32 @@ GC::Ptr<Element> calculate_active_element(T& self)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/web-animations-1/#dom-documentorshadowroot-getanimations
|
||||
template<DocumentOrShadowRoot T>
|
||||
WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> calculate_get_animations(T& self)
|
||||
{
|
||||
// Returns the set of relevant animations for a subtree for the document or shadow root on which this
|
||||
// method is called.
|
||||
Vector<GC::Ref<Animations::Animation>> relevant_animations;
|
||||
TRY(self.template for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
|
||||
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
|
||||
return IterationDecision::Continue;
|
||||
}));
|
||||
|
||||
// The returned list is sorted using the composite order described for the associated animations of
|
||||
// effects in § 5.4.2 The effect stack.
|
||||
quick_sort(relevant_animations, [](GC::Ref<Animations::Animation>& a, GC::Ref<Animations::Animation>& b) {
|
||||
auto& a_effect = as<Animations::KeyframeEffect>(*a->effect());
|
||||
auto& b_effect = as<Animations::KeyframeEffect>(*b->effect());
|
||||
return Animations::KeyframeEffect::composite_order(a_effect, b_effect) < 0;
|
||||
});
|
||||
|
||||
// Calling this method triggers a style change event for the document. As a result, the returned list
|
||||
// reflects the state after applying any pending style changes to animation such as changes to
|
||||
// animation-related style properties that have yet to be processed.
|
||||
// FIXME: Implement this.
|
||||
|
||||
return relevant_animations;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -198,12 +198,7 @@ void ShadowRoot::for_each_css_style_sheet(Function<void(CSS::CSSStyleSheet&)>&&
|
||||
|
||||
WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> ShadowRoot::get_animations()
|
||||
{
|
||||
Vector<GC::Ref<Animations::Animation>> relevant_animations;
|
||||
TRY(for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
|
||||
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
|
||||
return IterationDecision::Continue;
|
||||
}));
|
||||
return relevant_animations;
|
||||
return calculate_get_animations(*this);
|
||||
}
|
||||
|
||||
ElementByIdMap& ShadowRoot::element_by_id() const
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
Harness status: OK
|
||||
|
||||
Found 11 tests
|
||||
|
||||
11 Pass
|
||||
Pass Document.getAnimations() returns an empty sequence for non-animated content
|
||||
Pass Document.getAnimations() returns script-generated animations
|
||||
Pass Document.getAnimations() returns script-generated animations in the order they were created
|
||||
Pass Document.getAnimations() does not return a disconnected node
|
||||
Pass Document.getAnimations() does not return an animation with a null target
|
||||
Pass Document.getAnimations() returns animations on elements inside same-origin iframes
|
||||
Pass iframe.contentDocument.getAnimations() returns animations on elements inside same-origin Document
|
||||
Pass ShadowRoot.getAnimations() return all animations in the shadow tree
|
||||
Pass Document.getAnimations() does NOT return animations in shadow trees
|
||||
Pass ShadowRoot.getAnimations() does NOT return animations in parent document
|
||||
Pass Document.getAnimations() triggers a style change event
|
||||
@@ -0,0 +1,234 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<title>DocumentOrShadowRoot.getAnimations</title>
|
||||
<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-documentorshadowroot-getanimations">
|
||||
<script src="../../../resources/testharness.js"></script>
|
||||
<script src="../../../resources/testharnessreport.js"></script>
|
||||
<script src="../../testcommon.js"></script>
|
||||
<body>
|
||||
<div id="log"></div>
|
||||
<div id="target"></div>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
const gKeyFrames = { 'marginLeft': ['100px', '200px'] };
|
||||
|
||||
test(t => {
|
||||
assert_equals(document.getAnimations().length, 0,
|
||||
'getAnimations returns an empty sequence for a document ' +
|
||||
'with no animations');
|
||||
}, 'Document.getAnimations() returns an empty sequence for non-animated'
|
||||
+ ' content');
|
||||
|
||||
test(t => {
|
||||
const div = createDiv(t);
|
||||
const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
|
||||
const anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
|
||||
assert_equals(document.getAnimations().length, 2,
|
||||
'getAnimation returns running animations');
|
||||
|
||||
anim1.finish();
|
||||
anim2.finish();
|
||||
assert_equals(document.getAnimations().length, 0,
|
||||
'getAnimation only returns running animations');
|
||||
}, 'Document.getAnimations() returns script-generated animations')
|
||||
|
||||
test(t => {
|
||||
const div = createDiv(t);
|
||||
const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
|
||||
const anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
|
||||
assert_array_equals(document.getAnimations(),
|
||||
[ anim1, anim2 ],
|
||||
'getAnimations() returns running animations');
|
||||
}, 'Document.getAnimations() returns script-generated animations in the order'
|
||||
+ ' they were created')
|
||||
|
||||
test(t => {
|
||||
// This element exists but is not a descendent of any document, so isn't
|
||||
// picked up by getAnimations.
|
||||
const div = document.createElement('div');
|
||||
const anim = div.animate(gKeyFrames, 100 * MS_PER_SEC);
|
||||
assert_equals(document.getAnimations().length, 0);
|
||||
|
||||
// Now connect the div; it should appear in the list of animations.
|
||||
document.body.appendChild(div);
|
||||
t.add_cleanup(() => { div.remove(); });
|
||||
assert_equals(document.getAnimations().length, 1);
|
||||
}, 'Document.getAnimations() does not return a disconnected node');
|
||||
|
||||
test(t => {
|
||||
const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
|
||||
const anim = new Animation(effect, document.timeline);
|
||||
anim.play();
|
||||
|
||||
assert_equals(document.getAnimations().length, 0,
|
||||
'document.getAnimations() only returns animations targeting ' +
|
||||
'elements in this document');
|
||||
}, 'Document.getAnimations() does not return an animation with a null target');
|
||||
|
||||
promise_test(async t => {
|
||||
const iframe = document.createElement('iframe');
|
||||
await insertFrameAndAwaitLoad(t, iframe, document)
|
||||
|
||||
const div = createDiv(t, iframe.contentDocument)
|
||||
const effect = new KeyframeEffect(div, null, 100 * MS_PER_SEC);
|
||||
const anim = new Animation(effect, document.timeline);
|
||||
anim.play();
|
||||
|
||||
// The animation's timeline is from the main document, but the effect's
|
||||
// target element is part of the iframe document and that is what matters
|
||||
// for getAnimations.
|
||||
assert_equals(document.getAnimations().length, 0);
|
||||
assert_equals(iframe.contentDocument.getAnimations().length, 1);
|
||||
anim.finish();
|
||||
}, 'Document.getAnimations() returns animations on elements inside same-origin'
|
||||
+ ' iframes');
|
||||
|
||||
promise_test(async t => {
|
||||
const iframe1 = document.createElement('iframe');
|
||||
const iframe2 = document.createElement('iframe');
|
||||
|
||||
await insertFrameAndAwaitLoad(t, iframe1, document);
|
||||
await insertFrameAndAwaitLoad(t, iframe2, document);
|
||||
|
||||
const div_frame1 = createDiv(t, iframe1.contentDocument)
|
||||
const div_main_frame = createDiv(t)
|
||||
const effect1 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
|
||||
const anim1 = new Animation(effect1, document.timeline);
|
||||
anim1.play();
|
||||
// Animation of div_frame1 is in iframe with main timeline.
|
||||
// The animation's timeline is from the iframe, but the effect's target
|
||||
// element is part of the iframe's document.
|
||||
assert_equals(document.getAnimations().length, 0);
|
||||
assert_equals(iframe1.contentDocument.getAnimations().length, 1);
|
||||
anim1.finish();
|
||||
|
||||
// animation of div_frame1 in iframe1 with iframe timeline
|
||||
const effect2 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
|
||||
const anim2 = new Animation(effect2, iframe1.contentDocument.timeline);
|
||||
anim2.play();
|
||||
assert_equals(document.getAnimations().length, 0);
|
||||
assert_equals(iframe1.contentDocument.getAnimations().length, 1);
|
||||
anim2.finish();
|
||||
|
||||
//animation of div_main_frame in main frame with iframe timeline
|
||||
const effect3 = new KeyframeEffect(div_main_frame, null, 100 * MS_PER_SEC);
|
||||
const anim3 = new Animation(effect3, iframe1.contentDocument.timeline);
|
||||
anim3.play();
|
||||
assert_equals(document.getAnimations().length, 1);
|
||||
assert_equals(iframe1.contentDocument.getAnimations().length, 0);
|
||||
anim3.finish();
|
||||
|
||||
//animation of div_frame1 in iframe1 with another iframe's timeline
|
||||
const effect4 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
|
||||
const anim4 = new Animation(effect4, iframe2.contentDocument.timeline);
|
||||
anim4.play();
|
||||
assert_equals(document.getAnimations().length, 0);
|
||||
assert_equals(iframe1.contentDocument.getAnimations().length, 1);
|
||||
assert_equals(iframe2.contentDocument.getAnimations().length, 0);
|
||||
anim4.finish();
|
||||
}, 'iframe.contentDocument.getAnimations() returns animations on elements '
|
||||
+ 'inside same-origin Document');
|
||||
|
||||
test(t => {
|
||||
const div = createDiv(t);
|
||||
const shadow = div.attachShadow({ mode: 'open' });
|
||||
|
||||
// Create a tree with the following structure
|
||||
//
|
||||
// div
|
||||
// |
|
||||
// (ShadowRoot)
|
||||
// / \
|
||||
// childA childB
|
||||
// (*anim2) |
|
||||
// grandChild
|
||||
// (*anim1)
|
||||
//
|
||||
// This lets us test that:
|
||||
//
|
||||
// a) All children of the ShadowRoot are included
|
||||
// b) Descendants of the children are included
|
||||
// c) The result is sorted by composite order (since we fire anim1 before
|
||||
// anim2 despite childA appearing first in tree order)
|
||||
|
||||
const childA = createDiv(t);
|
||||
shadow.append(childA);
|
||||
|
||||
const childB = createDiv(t);
|
||||
shadow.append(childB);
|
||||
|
||||
const grandChild = createDiv(t);
|
||||
childB.append(grandChild);
|
||||
|
||||
const anim1 = grandChild.animate(gKeyFrames, 100 * MS_PER_SEC)
|
||||
const anim2 = childA.animate(gKeyFrames, 100 * MS_PER_SEC)
|
||||
|
||||
assert_array_equals(
|
||||
div.shadowRoot.getAnimations(),
|
||||
[ anim1, anim2 ],
|
||||
'getAnimations() called on ShadowRoot returns expected animations'
|
||||
);
|
||||
}, 'ShadowRoot.getAnimations() return all animations in the shadow tree');
|
||||
|
||||
test(t => {
|
||||
const div = createDiv(t);
|
||||
const shadow = div.attachShadow({ mode: 'open' });
|
||||
|
||||
const child = createDiv(t);
|
||||
shadow.append(child);
|
||||
|
||||
child.animate(gKeyFrames, 100 * MS_PER_SEC)
|
||||
|
||||
assert_array_equals(
|
||||
document.getAnimations(),
|
||||
[],
|
||||
'getAnimations() called on Document does not return animations from shadow'
|
||||
+ ' trees'
|
||||
);
|
||||
}, 'Document.getAnimations() does NOT return animations in shadow trees');
|
||||
|
||||
test(t => {
|
||||
const div = createDiv(t);
|
||||
const shadow = div.attachShadow({ mode: 'open' });
|
||||
|
||||
div.animate(gKeyFrames, 100 * MS_PER_SEC)
|
||||
|
||||
assert_array_equals(
|
||||
div.shadowRoot.getAnimations(),
|
||||
[],
|
||||
'getAnimations() called on ShadowRoot does not return animations from'
|
||||
+ ' Document'
|
||||
);
|
||||
}, 'ShadowRoot.getAnimations() does NOT return animations in parent document');
|
||||
|
||||
promise_test(async t => {
|
||||
const div = createDiv(t);
|
||||
const watcher = EventWatcher(t, div, 'transitionrun');
|
||||
|
||||
// Create a covering animation to prevent transitions from firing after
|
||||
// calling getAnimations().
|
||||
const coveringAnimation = new Animation(
|
||||
new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
|
||||
);
|
||||
|
||||
// Setup transition start point.
|
||||
div.style.transition = 'opacity 100s';
|
||||
getComputedStyle(div).opacity;
|
||||
|
||||
// Update specified style but don't flush style.
|
||||
div.style.opacity = '0.5';
|
||||
|
||||
// Fetch animations
|
||||
document.getAnimations();
|
||||
|
||||
// Play the covering animation to ensure that only the call to
|
||||
// getAnimations() has a chance to trigger transitions.
|
||||
coveringAnimation.play();
|
||||
|
||||
// If getAnimations() flushed style, we should get a transitionrun event.
|
||||
await watcher.wait_for('transitionrun');
|
||||
}, 'Document.getAnimations() triggers a style change event');
|
||||
|
||||
</script>
|
||||
</body>
|
||||
Reference in New Issue
Block a user