LibWeb: Don't allow "display: none" start CSS animations

This is both a correctness fix and a performance optimization.
This commit is contained in:
Aliaksandr Kalenik 2025-01-31 22:26:59 +01:00 committed by Alexander Kalenik
commit 0cfe90b59e
Notes: github-actions[bot] 2025-02-01 12:42:56 +00:00
6 changed files with 116 additions and 7 deletions

View file

@ -10,6 +10,7 @@
#include <AK/StringBuilder.h>
#include <LibUnicode/CharacterTypes.h>
#include <LibUnicode/Locale.h>
#include <LibWeb/Animations/Animation.h>
#include <LibWeb/Bindings/ElementPrototype.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/MainThreadVM.h>
@ -20,6 +21,7 @@
#include <LibWeb/CSS/SelectorEngine.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/DOM/Attr.h>
@ -56,6 +58,7 @@
#include <LibWeb/HTML/Numbers.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/Scripting/Agent.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Infra/CharacterTypes.h>
@ -2459,8 +2462,69 @@ void Element::set_cascaded_properties(Optional<CSS::Selector::PseudoElement::Typ
}
}
bool Element::has_display_none_ancestor()
{
for (auto* ancestor = parent_or_shadow_host(); ancestor; ancestor = ancestor->parent_or_shadow_host()) {
if (!ancestor->is_element())
continue;
auto const& ancestor_element = static_cast<Element&>(*ancestor);
if (ancestor_element.computed_properties() && ancestor_element.computed_properties()->display().is_none()) {
return true;
}
}
return false;
}
void Element::play_or_cancel_animations_after_display_property_change(Optional<CSS::Display> old_display, Optional<CSS::Display> new_display)
{
// https://www.w3.org/TR/css-animations-1/#animations
// Setting the display property to none will terminate any running animation applied to the element and its descendants.
// If an element has a display of none, updating display to a value other than none will start all animations applied to
// the element by the animation-name property, as well as all animations applied to descendants with display other than none.
if (has_display_none_ancestor())
return;
bool previous_display_is_none = old_display.has_value() && old_display->is_none();
bool new_display_is_none = new_display.has_value() && new_display->is_none();
if (previous_display_is_none == new_display_is_none)
return;
auto play_or_cancel_depending_on_display = [&](Animations::Animation& animation) {
if (new_display_is_none) {
animation.cancel();
} else {
HTML::TemporaryExecutionContext context(realm());
animation.play().release_value_but_fixme_should_propagate_errors();
}
};
for_each_shadow_including_inclusive_descendant([&](auto& node) {
if (!node.is_element())
return TraversalDecision::Continue;
auto const& element = static_cast<Element const&>(node);
if (auto animation = element.cached_animation_name_animation({}))
play_or_cancel_depending_on_display(*animation);
for (auto i = 0; i < to_underlying(CSS::Selector::PseudoElement::Type::KnownPseudoElementCount); i++) {
auto pseudo_element = static_cast<CSS::Selector::PseudoElement::Type>(i);
if (auto animation = element.cached_animation_name_animation(pseudo_element))
play_or_cancel_depending_on_display(*animation);
}
return TraversalDecision::Continue;
});
}
void Element::set_computed_properties(GC::Ptr<CSS::ComputedProperties> style)
{
Optional<CSS::Display> old_display;
Optional<CSS::Display> new_display;
if (m_computed_properties)
old_display = m_computed_properties->display();
if (style)
new_display = style->display();
play_or_cancel_animations_after_display_property_change(old_display, new_display);
m_computed_properties = style;
computed_properties_changed();
}