LibWeb: Parse the scroll-timeline shorthand CSS property

The remaining failing tests in scroll-timeline-shorthand.html are due to
either:
 a) incorrect tests, see web-platform-tests/wpt#56181 or;
 b) a wider issue where we collapse coordinating value list longhand
properties to a single value when we shouldn't.
This commit is contained in:
Callum Law 2025-11-21 01:30:02 +13:00 committed by Sam Atkins
parent 992b0a4dc6
commit 13ce2d1857
Notes: github-actions[bot] 2025-11-28 13:26:23 +00:00
8 changed files with 261 additions and 3 deletions

View file

@ -499,6 +499,7 @@ private:
RefPtr<StyleValue const> parse_position_visibility_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_position_visibility_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_quotes_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_quotes_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_single_repeat_style_value(PropertyID, TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_single_repeat_style_value(PropertyID, TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_scroll_timeline_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_scrollbar_color_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_scrollbar_color_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_scrollbar_gutter_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_scrollbar_gutter_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_shadow_value(TokenStream<ComponentValue>&, ShadowStyleValue::ShadowType); RefPtr<StyleValue const> parse_shadow_value(TokenStream<ComponentValue>&, ShadowStyleValue::ShadowType);

View file

@ -745,6 +745,8 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue const>> Parser::parse_css_value(Pr
return parse_all_as(tokens, [this](auto& tokens) { return parse_translate_value(tokens); }); return parse_all_as(tokens, [this](auto& tokens) { return parse_translate_value(tokens); });
case PropertyID::Scale: case PropertyID::Scale:
return parse_all_as(tokens, [this](auto& tokens) { return parse_scale_value(tokens); }); return parse_all_as(tokens, [this](auto& tokens) { return parse_scale_value(tokens); });
case PropertyID::ScrollTimeline:
return parse_all_as(tokens, [this](auto& tokens) { return parse_scroll_timeline_value(tokens); });
case PropertyID::ScrollTimelineAxis: case PropertyID::ScrollTimelineAxis:
case PropertyID::ScrollTimelineName: case PropertyID::ScrollTimelineName:
return parse_all_as(tokens, [this, property_id](auto& tokens) { return parse_simple_comma_separated_value_list(property_id, tokens); }); return parse_all_as(tokens, [this, property_id](auto& tokens) { return parse_simple_comma_separated_value_list(property_id, tokens); });
@ -5182,6 +5184,73 @@ RefPtr<StyleValue const> Parser::parse_scale_value(TokenStream<ComponentValue>&
return TransformationStyleValue::create(PropertyID::Scale, TransformFunction::Scale3d, { maybe_x.release_nonnull(), maybe_y.release_nonnull(), maybe_z.release_nonnull() }); return TransformationStyleValue::create(PropertyID::Scale, TransformFunction::Scale3d, { maybe_x.release_nonnull(), maybe_y.release_nonnull(), maybe_z.release_nonnull() });
} }
// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand
RefPtr<StyleValue const> Parser::parse_scroll_timeline_value(TokenStream<ComponentValue>& tokens)
{
// [ <'scroll-timeline-name'> <'scroll-timeline-axis'>? ]#
StyleValueVector names;
StyleValueVector axes;
auto transaction = tokens.begin_transaction();
do {
tokens.discard_whitespace();
auto maybe_name = parse_css_value_for_property(PropertyID::ScrollTimelineName, tokens);
if (!maybe_name)
return nullptr;
names.append(maybe_name.release_nonnull());
tokens.discard_whitespace();
if (tokens.next_token().is(Token::Type::Comma)) {
axes.append(KeywordStyleValue::create(Keyword::Block));
tokens.discard_a_token();
// Disallow trailing commas
if (!tokens.has_next_token())
return nullptr;
continue;
}
if (!tokens.has_next_token()) {
axes.append(KeywordStyleValue::create(Keyword::Block));
break;
}
auto maybe_axis = parse_css_value_for_property(PropertyID::ScrollTimelineAxis, tokens);
if (!maybe_axis)
return nullptr;
axes.append(maybe_axis.release_nonnull());
tokens.discard_whitespace();
if (tokens.next_token().is(Token::Type::Comma)) {
tokens.discard_a_token();
// Disallow trailing commas
if (!tokens.has_next_token())
return nullptr;
continue;
}
if (tokens.has_next_token())
return nullptr;
} while (tokens.has_next_token());
transaction.commit();
return ShorthandStyleValue::create(PropertyID::ScrollTimeline,
{ PropertyID::ScrollTimelineName, PropertyID::ScrollTimelineAxis },
{ StyleValueList::create(move(names), StyleValueList::Separator::Comma), StyleValueList::create(move(axes), StyleValueList::Separator::Comma) });
}
// https://drafts.csswg.org/css-scrollbars/#propdef-scrollbar-color // https://drafts.csswg.org/css-scrollbars/#propdef-scrollbar-color
RefPtr<StyleValue const> Parser::parse_scrollbar_color_value(TokenStream<ComponentValue>& tokens) RefPtr<StyleValue const> Parser::parse_scrollbar_color_value(TokenStream<ComponentValue>& tokens)
{ {

View file

@ -3440,6 +3440,16 @@
"affects-layout": false, "affects-layout": false,
"affects-stacking-context": true "affects-stacking-context": true
}, },
"scroll-timeline": {
"affects-layout": false,
"inherited": false,
"initial": "none block",
"multiplicity": "coordinating-list",
"longhands": [
"scroll-timeline-name",
"scroll-timeline-axis"
]
},
"scroll-timeline-axis": { "scroll-timeline-axis": {
"affects-layout": false, "affects-layout": false,
"animation-type": "none", "animation-type": "none",

View file

@ -78,7 +78,7 @@ String ShorthandStyleValue::to_string(SerializationMode mode) const
return { value.release_nonnull() }; return { value.release_nonnull() };
}; };
auto const coordinating_value_list_shorthand_to_string = [&](StringView entry_when_all_longhands_initial) { auto const coordinating_value_list_shorthand_to_string = [&](StringView entry_when_all_longhands_initial, Vector<PropertyID> required_longhands = {}) {
auto entry_count = style_value_as_value_list(longhand(m_properties.sub_properties[0])).size(); auto entry_count = style_value_as_value_list(longhand(m_properties.sub_properties[0])).size();
// If we don't have the same number of values for each longhand, we can't serialize this shorthand. // If we don't have the same number of values for each longhand, we can't serialize this shorthand.
@ -86,10 +86,15 @@ String ShorthandStyleValue::to_string(SerializationMode mode) const
return ""_string; return ""_string;
// We should serialize a longhand if: // We should serialize a longhand if:
// - The longhand is required
// - The value is not the initial value // - The value is not the initial value
// - Another longhand value which will be included later in the serialization is valid for this longhand. // - Another longhand value which will be included later in the serialization is valid for this longhand.
auto should_serialize_longhand = [&](size_t entry_index, size_t longhand_index) { auto should_serialize_longhand = [&](size_t entry_index, size_t longhand_index) {
auto longhand_id = m_properties.sub_properties[longhand_index]; auto longhand_id = m_properties.sub_properties[longhand_index];
if (required_longhands.contains_slow(longhand_id))
return true;
auto longhand_value = style_value_as_value_list(longhand(longhand_id))[entry_index]; auto longhand_value = style_value_as_value_list(longhand(longhand_id))[entry_index];
if (!longhand_value->equals(style_value_as_value_list(property_initial_value(longhand_id))[0])) if (!longhand_value->equals(style_value_as_value_list(property_initial_value(longhand_id))[0]))
@ -815,6 +820,10 @@ String ShorthandStyleValue::to_string(SerializationMode mode) const
case PropertyID::PlaceItems: case PropertyID::PlaceItems:
case PropertyID::PlaceSelf: case PropertyID::PlaceSelf:
return positional_value_list_shorthand_to_string(m_properties.values); return positional_value_list_shorthand_to_string(m_properties.values);
case PropertyID::ScrollTimeline:
// NB: We don't need to specify a value to use when the entry is empty as all values are initial since
// scroll-timeline-name is always included
return coordinating_value_list_shorthand_to_string(""sv, { PropertyID::ScrollTimelineName });
case PropertyID::TextDecoration: { case PropertyID::TextDecoration: {
// The rule here seems to be, only print what's different from the default value, // The rule here seems to be, only print what's different from the default value,
// but if they're all default, print the line. // but if they're all default, print the line.

View file

@ -694,6 +694,8 @@ All supported properties and their default values exposed from CSSStylePropertie
'rx': 'auto' 'rx': 'auto'
'ry': 'auto' 'ry': 'auto'
'scale': 'none' 'scale': 'none'
'scrollTimeline': 'none'
'scroll-timeline': 'none'
'scrollTimelineAxis': 'block' 'scrollTimelineAxis': 'block'
'scroll-timeline-axis': 'block' 'scroll-timeline-axis': 'block'
'scrollTimelineName': 'none' 'scrollTimelineName': 'none'

View file

@ -1,8 +1,8 @@
Harness status: OK Harness status: OK
Found 272 tests Found 274 tests
266 Pass 268 Pass
6 Fail 6 Fail
Pass accent-color Pass accent-color
Pass border-collapse Pass border-collapse
@ -241,6 +241,8 @@ Pass row-gap
Pass rx Pass rx
Pass ry Pass ry
Pass scale Pass scale
Pass scroll-timeline-axis
Pass scroll-timeline-name
Pass scrollbar-color Pass scrollbar-color
Pass scrollbar-gutter Pass scrollbar-gutter
Pass scrollbar-width Pass scrollbar-width

View file

@ -0,0 +1,56 @@
Harness status: OK
Found 50 tests
47 Pass
3 Fail
Pass e.style['scroll-timeline'] = "none block" should set the property value
Pass e.style['scroll-timeline'] = "none inline" should set the property value
Pass e.style['scroll-timeline'] = "--abc x" should set the property value
Pass e.style['scroll-timeline'] = "--abc inline" should set the property value
Pass e.style['scroll-timeline'] = "--aBc inline" should set the property value
Pass e.style['scroll-timeline'] = "--inline inline" should set the property value
Pass e.style['scroll-timeline'] = "--abc" should set the property value
Pass e.style['scroll-timeline'] = "--inline block" should set the property value
Pass e.style['scroll-timeline'] = "--block block" should set the property value
Pass e.style['scroll-timeline'] = "--y block" should set the property value
Pass e.style['scroll-timeline'] = "--x block" should set the property value
Pass e.style['scroll-timeline'] = "--a, --b, --c" should set the property value
Pass e.style['scroll-timeline'] = "--a inline, --b block, --c y" should set the property value
Pass e.style['scroll-timeline'] = "--auto" should set the property value
Pass e.style['scroll-timeline'] = "" should not set the property value
Pass e.style['scroll-timeline'] = "--abc --abc" should not set the property value
Pass e.style['scroll-timeline'] = "block none" should not set the property value
Pass e.style['scroll-timeline'] = "inline --abc" should not set the property value
Pass e.style['scroll-timeline'] = "default" should not set the property value
Pass e.style['scroll-timeline'] = "," should not set the property value
Pass e.style['scroll-timeline'] = ",,block,," should not set the property value
Pass Property scroll-timeline value 'none block'
Pass Property scroll-timeline value '--abc inline'
Pass Property scroll-timeline value 'none y'
Pass Property scroll-timeline value '--abc x'
Pass Property scroll-timeline value '--y y'
Pass Property scroll-timeline value '--abc'
Pass Property scroll-timeline value '--inline block'
Pass Property scroll-timeline value '--block block'
Pass Property scroll-timeline value '--y block'
Pass Property scroll-timeline value '--x block'
Pass Property scroll-timeline value '--a, --b, --c'
Pass Property scroll-timeline value '--a inline, --b block, --c y'
Pass e.style['scroll-timeline'] = "--abc y" should set scroll-timeline-axis
Pass e.style['scroll-timeline'] = "--abc y" should set scroll-timeline-name
Pass e.style['scroll-timeline'] = "--abc y" should not set unrelated longhands
Pass e.style['scroll-timeline'] = "--inline x" should set scroll-timeline-axis
Pass e.style['scroll-timeline'] = "--inline x" should set scroll-timeline-name
Pass e.style['scroll-timeline'] = "--inline x" should not set unrelated longhands
Pass e.style['scroll-timeline'] = "--abc y, --def" should set scroll-timeline-axis
Pass e.style['scroll-timeline'] = "--abc y, --def" should set scroll-timeline-name
Pass e.style['scroll-timeline'] = "--abc y, --def" should not set unrelated longhands
Fail e.style['scroll-timeline'] = "--abc, --def" should set scroll-timeline-axis
Pass e.style['scroll-timeline'] = "--abc, --def" should set scroll-timeline-name
Pass e.style['scroll-timeline'] = "--abc, --def" should not set unrelated longhands
Pass Shorthand contraction of scroll-timeline-name:--abc:undefined;scroll-timeline-axis:inline:undefined
Pass Shorthand contraction of scroll-timeline-name:--a, --b:undefined;scroll-timeline-axis:inline, block:undefined
Pass Shorthand contraction of scroll-timeline-name:none, none:undefined;scroll-timeline-axis:block, block:undefined
Fail Shorthand contraction of scroll-timeline-name:--a, --b, --c:undefined;scroll-timeline-axis:inline, inline:undefined
Fail Shorthand contraction of scroll-timeline-name:--a, --b:undefined;scroll-timeline-axis:inline, inline, inline:undefined

View file

@ -0,0 +1,109 @@
<!DOCTYPE html>
<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../../css/support/computed-testcommon.js"></script>
<script src="../../css/support/parsing-testcommon.js"></script>
<script src="../../css/support/shorthand-testcommon.js"></script>
<div id="target"></div>
<script>
test_valid_value('scroll-timeline', 'none block', 'none');
test_valid_value('scroll-timeline', 'none inline');
test_valid_value('scroll-timeline', '--abc x');
test_valid_value('scroll-timeline', '--abc inline');
test_valid_value('scroll-timeline', '--aBc inline');
test_valid_value('scroll-timeline', '--inline inline');
test_valid_value('scroll-timeline', '--abc');
test_valid_value('scroll-timeline', '--inline block', '--inline');
test_valid_value('scroll-timeline', '--block block', '--block');
test_valid_value('scroll-timeline', '--y block', '--y');
test_valid_value('scroll-timeline', '--x block', '--x');
test_valid_value('scroll-timeline', '--a, --b, --c');
test_valid_value('scroll-timeline', '--a inline, --b block, --c y', '--a inline, --b, --c y');
test_valid_value('scroll-timeline', '--auto');
test_invalid_value('scroll-timeline', '');
test_invalid_value('scroll-timeline', '--abc --abc');
test_invalid_value('scroll-timeline', 'block none');
test_invalid_value('scroll-timeline', 'inline --abc');
test_invalid_value('scroll-timeline', 'default');
test_invalid_value('scroll-timeline', ',');
test_invalid_value('scroll-timeline', ',,block,,');
test_computed_value('scroll-timeline', 'none block', 'none');
test_computed_value('scroll-timeline', '--abc inline');
test_computed_value('scroll-timeline', 'none y');
test_computed_value('scroll-timeline', '--abc x');
test_computed_value('scroll-timeline', '--y y');
test_computed_value('scroll-timeline', '--abc');
test_computed_value('scroll-timeline', '--inline block', '--inline');
test_computed_value('scroll-timeline', '--block block', '--block');
test_computed_value('scroll-timeline', '--y block', '--y');
test_computed_value('scroll-timeline', '--x block', '--x');
test_computed_value('scroll-timeline', '--a, --b, --c');
test_computed_value('scroll-timeline', '--a inline, --b block, --c y', '--a inline, --b, --c y');
test_shorthand_value('scroll-timeline', '--abc y',
{
'scroll-timeline-name': '--abc',
'scroll-timeline-axis': 'y',
});
test_shorthand_value('scroll-timeline', '--inline x',
{
'scroll-timeline-name': '--inline',
'scroll-timeline-axis': 'x',
});
test_shorthand_value('scroll-timeline', '--abc y, --def',
{
'scroll-timeline-name': '--abc, --def',
'scroll-timeline-axis': 'y, block',
});
test_shorthand_value('scroll-timeline', '--abc, --def',
{
'scroll-timeline-name': '--abc, --def',
'scroll-timeline-axis': 'block, block',
});
function test_shorthand_contraction(shorthand, longhands, expected) {
let longhands_fmt = Object.entries(longhands).map((e) => `${e[0]}:${e[1]}:${e[2]}`).join(';');
test((t) => {
t.add_cleanup(() => {
for (let shorthand of Object.keys(longhands))
target.style.removeProperty(shorthand);
});
for (let [shorthand, value] of Object.entries(longhands))
target.style.setProperty(shorthand, value);
assert_equals(target.style.getPropertyValue(shorthand), expected, 'Declared value');
assert_equals(getComputedStyle(target).getPropertyValue(shorthand), expected, 'Computed value');
}, `Shorthand contraction of ${longhands_fmt}`);
}
test_shorthand_contraction('scroll-timeline', {
'scroll-timeline-name': '--abc',
'scroll-timeline-axis': 'inline',
}, '--abc inline');
test_shorthand_contraction('scroll-timeline', {
'scroll-timeline-name': '--a, --b',
'scroll-timeline-axis': 'inline, block',
}, '--a inline, --b');
test_shorthand_contraction('scroll-timeline', {
'scroll-timeline-name': 'none, none',
'scroll-timeline-axis': 'block, block',
}, 'none, none');
// Longhands with different lengths:
test_shorthand_contraction('scroll-timeline', {
'scroll-timeline-name': '--a, --b, --c',
'scroll-timeline-axis': 'inline, inline',
}, '--a inline, --b inline, --c inline');
test_shorthand_contraction('scroll-timeline', {
'scroll-timeline-name': '--a, --b',
'scroll-timeline-axis': 'inline, inline, inline',
}, '--a inline, --b inline');
</script>