layout: Let stretch on flex item cross size stretch to the line (#38526)

We were instead stretching to the containing block, which implied that
the behaviors of a `stretch` size and `stretch` alignment weren't
consistent.

As resolved by the CSSWG, the behavior will now be:
 - If the cross size of the line is known, stretch to the line.
 - Otherwise, stretch to the containing block.

See https://github.com/w3c/csswg-drafts/issues/11784

This aligns us with Blink, which has already shipped this new behavior.

Testing: Improves existing WPT and adds a new test.

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Oriol Brufau 2025-08-08 08:45:30 -07:00 committed by GitHub
parent 9866ca7e59
commit 93c9fc14f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 164 additions and 180 deletions

View file

@ -58,9 +58,7 @@ struct FlexContext<'a> {
struct FlexItem<'a> {
box_: &'a FlexItemBox,
/// The preferred, min and max inner cross sizes. If the flex container is single-line
/// and [`Self::cross_size_stretches_to_line`] is true, then the preferred cross size
/// is set to [`Size::Stretch`].
/// The preferred, min and max inner cross sizes.
content_cross_sizes: Sizes,
padding: FlexRelativeSides<Au>,
@ -99,17 +97,10 @@ struct FlexItem<'a> {
/// <https://drafts.csswg.org/css-sizing-4/#preferred-aspect-ratio>
preferred_aspect_ratio: Option<AspectRatio>,
/// Whether the preferred cross size of the item stretches to fill the flex line.
/// This happens when the size computes to `auto`, the used value of `align-self`
/// is `stretch`, and neither of the cross-axis margins are `auto`.
/// <https://drafts.csswg.org/css-flexbox-1/#stretched>
///
/// Note the following sizes are not sufficient:
/// - A size that only behaves as `auto` (like a cyclic percentage).
/// The computed value needs to be `auto` too.
/// - A `stretch` size. It stretches to the containing block, not to the line
/// (under discussion in <https://github.com/w3c/csswg-drafts/issues/11784>).
cross_size_stretches_to_line: bool,
/// The automatic size in the cross axis.
/// <https://drafts.csswg.org/css-sizing-3/#automatic-size>
automatic_cross_size: Size<Au>,
automatic_cross_size_for_intrinsic_sizing: Size<Au>,
}
/// Child of a FlexContainer. Can either be absolutely positioned, or not. If not,
@ -1543,41 +1534,45 @@ impl InitialFlexLineLayout<'_> {
FlexAxis::Column => Direction::Inline,
};
let layout = &mut item.layout_result;
let used_cross_size = if item.item.cross_size_stretches_to_line {
let content_size = match cross_axis {
Direction::Block => layout.content_block_size,
Direction::Inline => layout.containing_block_size.inline,
};
item.item.content_cross_sizes.resolve(
cross_axis,
Size::Stretch,
Au::zero,
Some(Au::zero().max(final_line_cross_size - item.item.pbm_auto_is_zero.cross)),
|| content_size.into(),
// Tables have a special sizing in the block axis in that handles collapsed rows,
// but it would prevent stretching. So we only recognize tables in the inline axis.
// The interaction of collapsed table tracks and the flexbox algorithms is unclear,
// see https://github.com/w3c/csswg-drafts/issues/11408.
item.item.box_.independent_formatting_context.is_table() &&
cross_axis == Direction::Inline,
)
} else {
layout.hypothetical_cross_size
let get_content_size = || match cross_axis {
Direction::Block => layout.content_block_size.into(),
Direction::Inline => item
.item
.inline_content_sizes(flex_context, item.used_main_size),
};
let used_cross_size = item.item.content_cross_sizes.resolve(
cross_axis,
item.item.automatic_cross_size,
Au::zero,
Some(Au::zero().max(final_line_cross_size - item.item.pbm_auto_is_zero.cross)),
get_content_size,
// Tables have a special sizing in the block axis in that handles collapsed rows,
// but it would prevent stretching. So we only recognize tables in the inline axis.
// The interaction of collapsed table tracks and the flexbox algorithms is unclear,
// see https://github.com/w3c/csswg-drafts/issues/11408.
item.item.box_.independent_formatting_context.is_table() &&
cross_axis == Direction::Inline,
);
item_used_cross_sizes.push(used_cross_size);
// “If the flex item has `align-self: stretch`, redo layout for its contents,
// treating this used size as its definite cross size so that percentage-sized
// children can be resolved.”
let needs_new_layout = item.item.cross_size_stretches_to_line &&
match cross_axis {
Direction::Block => {
SizeConstraint::Definite(used_cross_size) !=
layout.containing_block_size.block &&
layout.depends_on_block_constraints
},
Direction::Inline => used_cross_size != layout.containing_block_size.inline,
};
// However, as resolved in https://github.com/w3c/csswg-drafts/issues/11784,
// we do that when the cross size is `stretch`. We also need to do it if the
// inline size changes, which may happen with a `fit-content` cross size.
let needs_new_layout = match cross_axis {
Direction::Block => {
(match item.item.content_cross_sizes.preferred {
Size::Initial => item.item.automatic_cross_size == Size::Stretch,
Size::Stretch => true,
_ => false,
}) && SizeConstraint::Definite(used_cross_size) !=
layout.containing_block_size.block &&
layout.depends_on_block_constraints
},
Direction::Inline => used_cross_size != layout.containing_block_size.inline,
};
if needs_new_layout {
#[cfg(feature = "tracing")]
tracing::warn!(
@ -1759,9 +1754,6 @@ impl FlexItem<'_> {
let cross_size = match used_cross_size_override {
Some(s) => SizeConstraint::Definite(s),
None => {
// This means that an auto size with stretch alignment will behave different than
// a stretch size. That's not what the spec says, but matches other browsers.
// To be discussed in https://github.com/w3c/csswg-drafts/issues/11784.
let stretch_size = containing_block
.size
.block
@ -1792,22 +1784,12 @@ impl FlexItem<'_> {
let cross_size = used_cross_size_override.unwrap_or_else(|| {
let stretch_size =
Au::zero().max(containing_block.size.inline - self.pbm_auto_is_zero.cross);
let get_content_size = || {
let constraint_space = ConstraintSpace::new(
SizeConstraint::Definite(used_main_size),
item_writing_mode,
self.preferred_aspect_ratio,
);
independent_formatting_context
.inline_content_sizes(flex_context.layout_context, &constraint_space)
.sizes
};
self.content_cross_sizes.resolve(
Direction::Inline,
Size::FitContent,
Au::zero,
Some(stretch_size),
get_content_size,
|| self.inline_content_sizes(flex_context, used_main_size),
is_table,
)
});
@ -1841,9 +1823,6 @@ impl FlexItem<'_> {
} else if let Some(cross_size) = used_cross_size_override {
cross_size.into()
} else {
// This means that an auto size with stretch alignment will behave different than
// a stretch size. That's not what the spec says, but matches other browsers.
// To be discussed in https://github.com/w3c/csswg-drafts/issues/11784.
let stretch_size = containing_block
.size
.block
@ -2050,6 +2029,15 @@ impl FlexItem<'_> {
};
outer_cross_start + margin.cross_start + self.border.cross_start + self.padding.cross_start
}
#[inline]
fn inline_content_sizes(&self, flex_context: &FlexContext, block_size: Au) -> ContentSizes {
self.box_.inline_content_sizes(
flex_context,
SizeConstraint::Definite(block_size),
self.preferred_aspect_ratio,
)
}
}
impl FlexItemBox {
@ -2094,28 +2082,31 @@ impl FlexItemBox {
main: padding_border.main,
cross: padding_border.cross,
} + margin_auto_is_zero.sum_by_axis();
let (content_main_sizes, mut content_cross_sizes, cross_size_computes_to_auto) =
match flex_axis {
FlexAxis::Row => (
&content_box_sizes.inline,
content_box_sizes.block.clone(),
preferred_size_computes_to_auto.block,
),
FlexAxis::Column => (
&content_box_sizes.block,
content_box_sizes.inline.clone(),
preferred_size_computes_to_auto.inline,
),
};
let cross_size_stretches_to_line = cross_size_computes_to_auto &&
item_with_auto_cross_size_stretches_to_line_size(align_self, &margin);
if cross_size_stretches_to_line && config.container_is_single_line {
// <https://drafts.csswg.org/css-flexbox-1/#definite-sizes>
// > If a single-line flex container has a definite cross size, the automatic preferred
// > outer cross size of any stretched flex items is the flex containers inner cross size.
// Therefore, set it to `stretch`, which has the desired behavior.
content_cross_sizes.preferred = Size::Stretch;
}
let (content_main_sizes, content_cross_sizes, cross_size_computes_to_auto) = match flex_axis
{
FlexAxis::Row => (
&content_box_sizes.inline,
&content_box_sizes.block,
preferred_size_computes_to_auto.block,
),
FlexAxis::Column => (
&content_box_sizes.block,
&content_box_sizes.inline,
preferred_size_computes_to_auto.inline,
),
};
let automatic_cross_size = if cross_size_computes_to_auto &&
item_with_auto_cross_size_stretches_to_line_size(align_self, &margin)
{
Size::Stretch
} else {
Size::FitContent
};
let automatic_cross_size_for_intrinsic_sizing = if config.container_is_single_line {
automatic_cross_size
} else {
Size::FitContent
};
let containing_block_size = flex_axis.vec2_to_flex_relative(containing_block.size);
let stretch_size = FlexRelativeVec2 {
main: containing_block_size
@ -2136,7 +2127,7 @@ impl FlexItemBox {
let (preferred_cross_size, min_cross_size, max_cross_size) =
if let Some(cross_content_size) = tentative_cross_content_size {
let (preferred, min, max) = content_cross_sizes.resolve_each(
Size::FitContent,
automatic_cross_size_for_intrinsic_sizing,
Au::zero,
stretch_size.cross,
|| cross_content_size,
@ -2145,7 +2136,7 @@ impl FlexItemBox {
(Some(preferred), min, max)
} else {
content_cross_sizes.resolve_each_extrinsic(
Size::FitContent,
automatic_cross_size_for_intrinsic_sizing,
Au::zero(),
stretch_size.cross,
)
@ -2210,7 +2201,7 @@ impl FlexItemBox {
&pbm_auto_is_zero,
content_box_sizes,
preferred_aspect_ratio,
content_cross_sizes.preferred == Size::Stretch,
automatic_cross_size_for_intrinsic_sizing,
IntrinsicSizingMode::Size,
)
.into()
@ -2279,7 +2270,7 @@ impl FlexItemBox {
FlexItem {
box_: self,
content_cross_sizes,
content_cross_sizes: content_cross_sizes.clone(),
padding,
border,
margin: config.sides_to_flex_relative(pbm.margin),
@ -2293,7 +2284,8 @@ impl FlexItemBox {
align_self,
depends_on_block_constraints: *depends_on_block_constraints,
preferred_aspect_ratio,
cross_size_stretches_to_line,
automatic_cross_size,
automatic_cross_size_for_intrinsic_sizing,
}
}
@ -2312,12 +2304,12 @@ impl FlexItemBox {
// TODO: when laying out a column container with an indefinite main size,
// we compute the base sizes of the items twice. We should consider caching.
let FlexItem {
content_cross_sizes,
flex_base_size,
content_min_main_size,
content_max_main_size,
pbm_auto_is_zero,
preferred_aspect_ratio,
automatic_cross_size_for_intrinsic_sizing,
..
} = self.to_flex_item(
layout_context,
@ -2344,7 +2336,7 @@ impl FlexItemBox {
layout_context,
containing_block,
&auto_minimum,
content_cross_sizes.preferred == Size::Stretch,
automatic_cross_size_for_intrinsic_sizing == Size::Stretch,
);
(sizes, depends_on_block_constraints)
},
@ -2354,7 +2346,7 @@ impl FlexItemBox {
&pbm_auto_is_zero,
&content_box_sizes_and_pbm.content_box_sizes,
preferred_aspect_ratio,
content_cross_sizes.preferred == Size::Stretch,
automatic_cross_size_for_intrinsic_sizing,
IntrinsicSizingMode::Contribution,
);
(size.into(), true)
@ -2511,7 +2503,7 @@ impl FlexItemBox {
pbm_auto_is_zero: &FlexRelativeVec2<Au>,
content_box_sizes: &LogicalVec2<Sizes>,
preferred_aspect_ratio: Option<AspectRatio>,
cross_size_stretches_to_container_size: bool,
automatic_inline_size: Size<Au>,
intrinsic_sizing_mode: IntrinsicSizingMode,
) -> Au {
let content_block_size = || {
@ -2526,26 +2518,18 @@ impl FlexItemBox {
// TODO: This is wrong if the item writing mode is different from the flex
// container's writing mode.
let inline_size = {
let initial_behavior = if cross_size_stretches_to_container_size {
Size::Stretch
} else {
Size::FitContent
};
let stretch_size =
flex_context.containing_block.size.inline - pbm_auto_is_zero.cross;
let get_content_size = || {
let constraint_space = ConstraintSpace::new(
self.inline_content_sizes(
flex_context,
tentative_block_size,
style.writing_mode,
preferred_aspect_ratio,
);
self.independent_formatting_context
.inline_content_sizes(flex_context.layout_context, &constraint_space)
.sizes
)
};
content_box_sizes.inline.resolve(
Direction::Inline,
initial_behavior,
automatic_inline_size,
Au::zero,
Some(stretch_size),
get_content_size,
@ -2597,4 +2581,18 @@ impl FlexItemBox {
IntrinsicSizingMode::Size => content_block_size(),
}
}
fn inline_content_sizes(
&self,
flex_context: &FlexContext,
block_size: SizeConstraint,
preferred_aspect_ratio: Option<AspectRatio>,
) -> ContentSizes {
let writing_mode = self.independent_formatting_context.style().writing_mode;
let constraint_space =
ConstraintSpace::new(block_size, writing_mode, preferred_aspect_ratio);
self.independent_formatting_context
.inline_content_sizes(flex_context.layout_context, &constraint_space)
.sizes
}
}

View file

@ -993,7 +993,7 @@ impl From<Option<Au>> for SizeConstraint {
}
}
#[derive(Clone, Default)]
#[derive(Clone, Debug, Default)]
pub(crate) struct Sizes {
/// <https://drafts.csswg.org/css-sizing-3/#preferred-size-properties>
pub preferred: Size<Au>,

View file

@ -471,7 +471,9 @@ impl ReplacedContents {
collapsible_margins_in_children: CollapsedBlockMargins::zero(),
content_block_size,
content_inline_size_for_table: None,
depends_on_block_constraints: false,
// The result doesn't depend on `containing_block_for_children.size.block`,
// but it depends on `lazy_block_size`, which is probably tied to that.
depends_on_block_constraints: true,
fragments: self.make_fragments(layout_context, &base.style, size),
specific_layout_info: None,
}

View file

@ -172605,6 +172605,19 @@
{}
]
],
"align-self-016.html": [
"4b2bc6d2efc9f4b1c68ad33961ec97b5a7e7405b",
[
null,
[
[
"/css/reference/ref-filled-green-200px-square.html",
"=="
]
],
{}
]
],
"alignment": {
"flex-align-baseline-column-rtl-direction.html": [
"3be57a2b34bf4c68f0041b874fbfdcecd797a20a",

View file

@ -1,27 +0,0 @@
[keyword-sizes-on-flex-item-001.html]
[.test 57]
expected: FAIL
[.test 58]
expected: FAIL
[.test 59]
expected: FAIL
[.test 60]
expected: FAIL
[.test 61]
expected: FAIL
[.test 62]
expected: FAIL
[.test 63]
expected: FAIL
[.test 64]
expected: FAIL
[.test 65]
expected: FAIL

View file

@ -1,3 +0,0 @@
[flex-line-002.html]
[.stretch 3]
expected: FAIL

View file

@ -1,3 +0,0 @@
[flex-line-003.html]
[.stretch 3]
expected: FAIL

View file

@ -1,3 +0,0 @@
[flex-line-004.html]
[.stretch 3]
expected: FAIL

View file

@ -1,3 +0,0 @@
[flex-line-005.html]
[.stretch 1]
expected: FAIL

View file

@ -4,9 +4,3 @@
[[data-expected-height\] 37]
expected: FAIL
[[data-expected-height\] 21]
expected: FAIL
[[data-expected-height\] 44]
expected: FAIL

View file

@ -4,9 +4,3 @@
[[data-expected-width\] 37]
expected: FAIL
[[data-expected-width\] 21]
expected: FAIL
[[data-expected-width\] 44]
expected: FAIL

View file

@ -10,9 +10,3 @@
[[data-expected-height\]:not([skip-second-pass\]) 45]
expected: FAIL
[[data-expected-height\] 21]
expected: FAIL
[[data-expected-height\]:not([skip-second-pass\]) 44]
expected: FAIL

View file

@ -10,9 +10,3 @@
[[data-expected-width\]:not([skip-second-pass\]) 45]
expected: FAIL
[[data-expected-width\] 21]
expected: FAIL
[[data-expected-width\]:not([skip-second-pass\]) 44]
expected: FAIL

View file

@ -16,9 +16,3 @@
[[data-expected-height\] 46]
expected: FAIL
[[data-expected-height\] 21]
expected: FAIL
[[data-expected-height\] 44]
expected: FAIL

View file

@ -16,9 +16,3 @@
[[data-expected-width\] 46]
expected: FAIL
[[data-expected-width\] 21]
expected: FAIL
[[data-expected-width\] 44]
expected: FAIL

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Flexbox: shrink-to-fit item in multi-line column container</title>
<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#propdef-align-self">
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/11784">
<link rel="match" href="../reference/ref-filled-green-200px-square.html">
<meta assert="
#item has `align-self: start`, which sizes it as `fit-content`.
Therefore, we initially shrink-to-fit it into the container (100px),
thus getting a cross size of 100px and a main size of 200px.
However, we have another item which is 200px wide, so the flex line
grows to that size.
Once we know the final cross size of the line, we shrink-to-fit #item
into the line (200px), thus getting a final cross size of 200px.
Now both floats fit side by side into these 200px, so vertically they
only need 100px, but the main size of #item remains as 200px.
">
<style>
#container {
display: flex;
flex-flow: column wrap;
width: 100px;
border-right: 100px solid red;
}
#item{
align-self: start;
background: linear-gradient(to bottom, red 50%, green 50%);
}
.float {
float: left;
width: 100px;
height: 100px;
background: green;
}
</style>
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div id="container">
<div id="item">
<div class="float"></div>
<div class="float"></div>
</div>
<div style="width: 200px"></div>
</div>