LibWeb/CSS: Parse the container-type property

This applies size, inline-size, and style containment in some cases.
There are other WPT tests for that, but we seem to not implement enough
of containment for this to have an effect so I've not imported those.

Gets us 35 WPT subtests.
This commit is contained in:
Sam Atkins 2025-09-30 14:48:55 +01:00 committed by Tim Ledbetter
commit 3916e33276
Notes: github-actions[bot] 2025-09-30 21:07:12 +00:00
17 changed files with 355 additions and 7 deletions

View file

@ -1887,6 +1887,38 @@ Containment ComputedProperties::contain() const
return containment;
}
ContainerType ComputedProperties::container_type() const
{
ContainerType container_type {};
auto const& value = property(PropertyID::Contain);
if (value.to_keyword() == Keyword::Normal)
return container_type;
if (value.is_value_list()) {
auto& values = value.as_value_list().values();
for (auto const& item : values) {
switch (item->to_keyword()) {
case Keyword::Size:
container_type.is_size_container = true;
break;
case Keyword::InlineSize:
container_type.is_inline_size_container = true;
break;
case Keyword::ScrollState:
container_type.is_scroll_state_container = true;
break;
default:
dbgln("`{}` is not supported in `container-type` (yet?)", item->to_string(SerializationMode::Normal));
break;
}
}
}
return container_type;
}
MixBlendMode ComputedProperties::mix_blend_mode() const
{
auto const& value = property(PropertyID::MixBlendMode);

View file

@ -183,6 +183,7 @@ public:
Isolation isolation() const;
TouchActionData touch_action() const;
Containment contain() const;
ContainerType container_type() const;
MixBlendMode mix_blend_mode() const;
Optional<FlyString> view_transition_name() const;

View file

@ -82,6 +82,14 @@ struct Containment {
bool is_empty() const { return !(size_containment || inline_size_containment || layout_containment || style_containment || paint_containment); }
};
struct ContainerType {
bool is_size_container : 1 { false };
bool is_inline_size_container : 1 { false };
bool is_scroll_state_container : 1 { false };
bool is_empty() const { return !(is_size_container || is_inline_size_container || is_scroll_state_container); }
};
struct ScrollbarColorData {
Color thumb_color { Color::Transparent };
Color track_color { Color::Transparent };
@ -242,6 +250,7 @@ public:
static UserSelect user_select() { return UserSelect::Auto; }
static Isolation isolation() { return Isolation::Auto; }
static Containment contain() { return {}; }
static ContainerType container_type() { return {}; }
static MixBlendMode mix_blend_mode() { return MixBlendMode::Normal; }
static Optional<int> z_index() { return OptionalNone(); }
@ -563,6 +572,7 @@ public:
UserSelect user_select() const { return m_noninherited.user_select; }
Isolation isolation() const { return m_noninherited.isolation; }
Containment const& contain() const { return m_noninherited.contain; }
ContainerType const& container_type() const { return m_noninherited.container_type; }
MixBlendMode mix_blend_mode() const { return m_noninherited.mix_blend_mode; }
Optional<FlyString> view_transition_name() const { return m_noninherited.view_transition_name; }
TouchActionData touch_action() const { return m_noninherited.touch_action; }
@ -841,6 +851,7 @@ protected:
UserSelect user_select { InitialValues::user_select() };
Isolation isolation { InitialValues::isolation() };
Containment contain { InitialValues::contain() };
ContainerType container_type { InitialValues::container_type() };
MixBlendMode mix_blend_mode { InitialValues::mix_blend_mode() };
WhiteSpaceTrimData white_space_trim;
Optional<FlyString> view_transition_name;
@ -1046,6 +1057,7 @@ public:
void set_user_select(UserSelect value) { m_noninherited.user_select = value; }
void set_isolation(Isolation value) { m_noninherited.isolation = value; }
void set_contain(Containment value) { m_noninherited.contain = move(value); }
void set_container_type(ContainerType value) { m_noninherited.container_type = move(value); }
void set_mix_blend_mode(MixBlendMode value) { m_noninherited.mix_blend_mode = value; }
void set_view_transition_name(Optional<FlyString> value) { m_noninherited.view_transition_name = move(value); }
void set_touch_action(TouchActionData value) { m_noninherited.touch_action = value; }

View file

@ -456,6 +456,7 @@
"screen",
"scroll",
"scroll-position",
"scroll-state",
"scrollbar",
"se-resize",
"searchfield",

View file

@ -395,6 +395,7 @@ private:
RefPtr<PositionStyleValue const> parse_position_value(TokenStream<ComponentValue>&, PositionParsingMode = PositionParsingMode::Normal);
RefPtr<StyleValue const> parse_filter_value_list_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_contain_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_container_type_value(TokenStream<ComponentValue>&);
RefPtr<StringStyleValue const> parse_opentype_tag_value(TokenStream<ComponentValue>&);
RefPtr<FontSourceStyleValue const> parse_font_source_value(TokenStream<ComponentValue>&);

View file

@ -537,6 +537,14 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue const>> Parser::parse_css_value(Pr
if (auto parsed_value = parse_columns_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
return ParseError::SyntaxError;
case PropertyID::Contain:
if (auto parsed_value = parse_contain_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
return ParseError::SyntaxError;
case PropertyID::ContainerType:
if (auto parsed_value = parse_container_type_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
return ParseError::SyntaxError;
case PropertyID::Content:
if (auto parsed_value = parse_content_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
@ -815,10 +823,6 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue const>> Parser::parse_css_value(Pr
if (auto parsed_value = parse_scale_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
return ParseError::SyntaxError;
case PropertyID::Contain:
if (auto parsed_value = parse_contain_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
return ParseError::SyntaxError;
case PropertyID::WhiteSpace:
if (auto parsed_value = parse_white_space_shorthand(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
@ -6073,6 +6077,59 @@ RefPtr<StyleValue const> Parser::parse_contain_value(TokenStream<ComponentValue>
return StyleValueList::create(move(containment_values), StyleValueList::Separator::Space);
}
// https://drafts.csswg.org/css-conditional-5/#propdef-container-type
RefPtr<StyleValue const> Parser::parse_container_type_value(TokenStream<ComponentValue>& tokens)
{
// normal | [ [ size | inline-size ] || scroll-state ]
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
// normal
if (auto none = parse_all_as_single_keyword_value(tokens, Keyword::Normal)) {
transaction.commit();
return none;
}
// [ [ size | inline-size ] || scroll-state ]
if (!tokens.has_next_token())
return {};
RefPtr<StyleValue const> size_value;
RefPtr<StyleValue const> scroll_state_value;
while (tokens.has_next_token()) {
auto keyword_value = parse_keyword_value(tokens);
if (!keyword_value)
return {};
switch (keyword_value->to_keyword()) {
case Keyword::Size:
case Keyword::InlineSize:
if (size_value)
return {};
size_value = move(keyword_value);
break;
case Keyword::ScrollState:
if (scroll_state_value)
return {};
scroll_state_value = move(keyword_value);
break;
default:
return {};
}
tokens.discard_whitespace();
}
StyleValueVector containment_values;
if (size_value)
containment_values.append(size_value.release_nonnull());
if (scroll_state_value)
containment_values.append(scroll_state_value.release_nonnull());
transaction.commit();
return StyleValueList::create(move(containment_values), StyleValueList::Separator::Space);
}
// https://www.w3.org/TR/css-text-4/#white-space-trim
RefPtr<StyleValue const> Parser::parse_white_space_trim_value(TokenStream<ComponentValue>& tokens)
{

View file

@ -1343,6 +1343,18 @@
"contain"
]
},
"container-type": {
"affects-stacking-context": true,
"animation-type": "none",
"inherited": false,
"initial": "normal",
"valid-identifiers": [
"normal",
"size",
"inline-size",
"scroll-state"
]
},
"content": {
"animation-type": "discrete",
"inherited": false,

View file

@ -898,6 +898,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
computed_values.set_mix_blend_mode(computed_style.mix_blend_mode());
computed_values.set_view_transition_name(computed_style.view_transition_name());
computed_values.set_contain(computed_style.contain());
computed_values.set_container_type(computed_style.container_type());
computed_values.set_shape_rendering(computed_values.shape_rendering());
computed_values.set_will_change(computed_style.will_change());
@ -1226,6 +1227,9 @@ bool Node::has_size_containment() const
if (computed_values().contain().size_containment)
return true;
if (computed_values().container_type().is_size_container)
return true;
return false;
}
// https://drafts.csswg.org/css-contain-2/#containment-inline-size
@ -1250,6 +1254,9 @@ bool Node::has_inline_size_containment() const
if (computed_values().contain().inline_size_containment)
return true;
if (computed_values().container_type().is_inline_size_container)
return true;
return false;
}
// https://drafts.csswg.org/css-contain-2/#containment-layout
@ -1289,6 +1296,9 @@ bool Node::has_style_containment() const
if (computed_values().contain().style_containment)
return true;
if (computed_values().container_type().is_size_container || computed_values().container_type().is_inline_size_container)
return true;
// https://drafts.csswg.org/css-contain-2/#valdef-content-visibility-auto
// Changes the used value of the 'contain' property so as to turn on layout containment, style containment, and
// paint containment for the element.

View file

@ -149,6 +149,7 @@ All properties associated with getComputedStyle(document.body):
"column-span",
"column-width",
"contain",
"container-type",
"content",
"content-visibility",
"counter-increment",

View file

@ -358,6 +358,8 @@ All supported properties and their default values exposed from CSSStylePropertie
'column-width': 'auto'
'columns': 'auto'
'contain': 'none'
'containerType': 'normal'
'container-type': 'normal'
'content': 'normal'
'contentVisibility': 'visible'
'content-visibility': 'visible'

View file

@ -147,6 +147,7 @@ column-height: auto
column-span: none
column-width: auto
contain: none
container-type: normal
content: normal
content-visibility: visible
counter-increment: none
@ -174,7 +175,7 @@ grid-row-start: auto
grid-template-areas: none
grid-template-columns: none
grid-template-rows: none
height: 2640px
height: 2655px
inline-size: 784px
inset-block-end: auto
inset-block-start: auto

View file

@ -1,8 +1,8 @@
Harness status: OK
Found 265 tests
Found 266 tests
259 Pass
260 Pass
6 Fail
Pass accent-color
Pass border-collapse
@ -147,6 +147,7 @@ Pass column-gap
Pass column-span
Pass column-width
Pass contain
Pass container-type
Pass content
Pass content-visibility
Pass counter-increment

View file

@ -0,0 +1,10 @@
Harness status: OK
Found 5 tests
5 Pass
Pass Property container-type value 'initial'
Pass Property container-type value 'unset'
Pass Property container-type value 'inline-size'
Pass Property container-type value 'size'
Pass Property container-type value 'normal'

View file

@ -0,0 +1,35 @@
Harness status: OK
Found 30 tests
30 Pass
Pass e.style['container-type'] = "initial" should set the property value
Pass e.style['container-type'] = "inherit" should set the property value
Pass e.style['container-type'] = "unset" should set the property value
Pass e.style['container-type'] = "revert" should set the property value
Pass e.style['container-type'] = "normal" should set the property value
Pass e.style['container-type'] = "size" should set the property value
Pass e.style['container-type'] = "inline-size" should set the property value
Pass e.style['container-type'] = "none" should not set the property value
Pass e.style['container-type'] = "auto" should not set the property value
Pass e.style['container-type'] = "block-size" should not set the property value
Pass e.style['container-type'] = "normal normal" should not set the property value
Pass e.style['container-type'] = "normal inline-size" should not set the property value
Pass e.style['container-type'] = "inline-size normal" should not set the property value
Pass e.style['container-type'] = "inline-size inline-size" should not set the property value
Pass e.style['container-type'] = "inline-size block-size" should not set the property value
Pass e.style['container-type'] = "block-size inline-size" should not set the property value
Pass e.style['container-type'] = "size inline-size" should not set the property value
Pass e.style['container-type'] = "inline-size size" should not set the property value
Pass e.style['container-type'] = "normal, normal" should not set the property value
Pass e.style['container-type'] = "foo" should not set the property value
Pass e.style['container-type'] = "\"foo\"" should not set the property value
Pass e.style['container-type'] = "foo, bar" should not set the property value
Pass e.style['container-type'] = "#fff" should not set the property value
Pass e.style['container-type'] = "1px" should not set the property value
Pass e.style['container-type'] = "default" should not set the property value
Pass e.style['container-type'] = "size nonsense" should not set the property value
Pass e.style['container-type'] = "style" should not set the property value
Pass e.style['container-type'] = "inline-size style" should not set the property value
Pass e.style['container-type'] = "style inline-size" should not set the property value
Pass e.style['container-type'] = "style size" should not set the property value

View file

@ -0,0 +1,18 @@
<!doctype html>
<meta charset="utf-8">
<title>Computed values of container-type</title>
<link rel="help" href="https://drafts.csswg.org/css-conditional-5/#container-type">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../css/support/computed-testcommon.js"></script>
<script src="support/cq-testcommon.js"></script>
<div id="target"></div>
<script>
setup(() => assert_implements_size_container_queries());
test_computed_value('container-type', 'initial', 'normal');
test_computed_value('container-type', 'unset', 'normal');
test_computed_value('container-type', 'inline-size');
test_computed_value('container-type', 'size');
test_computed_value('container-type', 'normal');
</script>

View file

@ -0,0 +1,44 @@
<!doctype html>
<meta charset="utf-8">
<title>CSS Conditional Test: Parsing of container-type</title>
<link rel="help" href="https://drafts.csswg.org/css-conditional-5/#container-type">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../css/support/parsing-testcommon.js"></script>
<script src="support/cq-testcommon.js"></script>
<div id="target"></div>
<script>
setup(() => assert_implements_size_container_queries());
test_valid_value('container-type', 'initial');
test_valid_value('container-type', 'inherit');
test_valid_value('container-type', 'unset');
test_valid_value('container-type', 'revert');
test_valid_value('container-type', 'normal');
test_valid_value('container-type', 'size');
test_valid_value('container-type', 'inline-size');
test_invalid_value('container-type', 'none');
test_invalid_value('container-type', 'auto');
test_invalid_value('container-type', 'block-size');
test_invalid_value('container-type', 'normal normal');
test_invalid_value('container-type', 'normal inline-size');
test_invalid_value('container-type', 'inline-size normal');
test_invalid_value('container-type', 'inline-size inline-size');
test_invalid_value('container-type', 'inline-size block-size');
test_invalid_value('container-type', 'block-size inline-size');
test_invalid_value('container-type', 'size inline-size');
test_invalid_value('container-type', 'inline-size size');
test_invalid_value('container-type', 'normal, normal');
test_invalid_value('container-type', 'foo');
test_invalid_value('container-type', '"foo"');
test_invalid_value('container-type', 'foo, bar');
test_invalid_value('container-type', '#fff');
test_invalid_value('container-type', '1px');
test_invalid_value('container-type', 'default');
test_invalid_value('container-type', 'size nonsense');
test_invalid_value('container-type', 'style');
test_invalid_value('container-type', 'inline-size style', 'style inline-size');
test_invalid_value('container-type', 'style inline-size');
test_invalid_value('container-type', 'style size');
</script>

View file

@ -0,0 +1,110 @@
function assert_implements_size_container_queries() {
assert_implements(CSS.supports("container-type:size"),
"Basic support for size container queries required");
}
function assert_implements_scroll_state_container_queries() {
assert_implements(CSS.supports("container-type:scroll-state"),
"Basic support for scroll-state container queries required");
}
function assert_implements_style_container_queries() {
// TODO: Replace with CSS.supports() when/if this can be expressed with at-rule().
const sheet = new CSSStyleSheet();
// No support means the style() function is <general-enclosed> which should
// affect serialization. Although serialization for <general-enclosed> is not
// specified[1], unknown function names are unlikely to be resolved to be
// serialized lower-case. Also, keeping the case is currently interoperable.
//
// [1] https://github.com/w3c/csswg-drafts/issues/7266
sheet.replaceSync('@container STYLE(--foo: bar){}');
assert_implements(sheet.cssRules[0].containerQuery === "style(--foo: bar)",
"Basic support for style container queries required");
}
function cleanup_container_query_main() {
const main = document.querySelector("#cq-main");
while (main.firstChild)
main.firstChild.remove();
}
function set_container_query_style(text) {
let style = document.createElement('style');
style.innerText = text;
document.querySelector("#cq-main").append(style);
return style;
}
function test_cq_rule_invalid(query) {
const ruleText = `@container ${query} {}`;
test(t => {
t.add_cleanup(cleanup_container_query_main);
let style = set_container_query_style(ruleText);
assert_equals(style.sheet.rules.length, 0);
}, `@container rule should be invalid: ${ruleText} {}`);
}
function test_cq_rule_valid(query) {
const ruleText = `@container ${query} {}`;
test(t => {
t.add_cleanup(cleanup_container_query_main);
let style = set_container_query_style(`@container ${query} {}`);
assert_equals(style.sheet.rules.length, 1);
}, `@container rule should be valid: ${ruleText} {}`);
}
function test_cq_condition_invalid(condition) {
test(t => {
t.add_cleanup(cleanup_container_query_main);
let style = set_container_query_style(`@container name ${condition} {}`);
assert_equals(style.sheet.rules.length, 0);
}, `Query condition should be invalid: ${condition}`);
}
// Tests that 1) the condition parses, and 2) is either "unknown" or not, as
// specified.
function test_cq_condition_valid(condition, unknown) {
test(t => {
t.add_cleanup(cleanup_container_query_main);
let style = set_container_query_style(`
@container name ${condition} {}
@container name (${condition}) or (not (${condition})) { main { --match:true; } }
`);
assert_equals(style.sheet.rules.length, 2);
const expected = unknown ? '' : 'true';
assert_equals(getComputedStyle(document.querySelector("#cq-main")).getPropertyValue('--match'), expected);
}, `Query condition should be valid${unknown ? ' but unknown' : ''}: ${condition}`);
}
function test_cq_condition_known(condition) {
test_cq_condition_valid(condition, false /* unknown */);
}
function test_cq_condition_unknown(condition) {
test_cq_condition_valid(condition, true /* unknown */);
}
function test_container_name_invalid(container_name) {
test(t => {
t.add_cleanup(cleanup_container_query_main);
let style = set_container_query_style(`@container ${container_name} not (width) {}`);
assert_equals(style.sheet.rules.length, 0);
}, `Container name: ${container_name}`);
}
function test_container_name_valid(container_name) {
test(t => {
t.add_cleanup(cleanup_container_query_main);
let style = set_container_query_style(`@container ${container_name} not (width) {}`);
assert_equals(style.sheet.rules.length, 1);
}, `Container name: ${container_name}`);
}
function test_cq_condition_serialization(query, expected) {
test(t => {
t.add_cleanup(cleanup_container_query_main);
let style = set_container_query_style(`@container ${query} {}`);
assert_equals(style.sheet.rules.length, 1);
assert_equals(style.sheet.rules[0].conditionText, expected);
}, `@container conditionText serialization: ${query}`);
}