Skip to content

Commit 73bebc0

Browse files
Dagurmeta-codesync[bot]
authored andcommitted
Layout breaks when the first element has a different minWidth than the rest (#2021)
Summary: fixes #2006 `distributeFreeSpaceFirstPass` decrements `totalFlexGrowFactors` / `totalFlexShrinkScaledFactors` as it freezes items, but only reduces `remainingFreeSpace` after the loop. Items after the first frozen one get an inflated fair share and freeze spuriously. When the first item clamps, the whole line freezes and `remainingFreeSpace` drains to 0, so the second pass has nothing to distribute and everything falls back to its flex basis — a 540px row of three `flexGrow: 1` items with `maxWidth: 180` and minWidths 60/30/30 lays out as 60/30/30 instead of 180/180/180. Snapshot both totals before the loop and divide by the snapshot. The first pass is then iteration 1 of CSS Flexbox §9.7, and independent of child order. Layout only changes for configs that opt in: this sits behind `Errata::FlexFirstPassUsesRunningTotals`, which new configs set by default, so existing geometry is untouched until a config clears the bit. Same shape as `MinSizeUndefinedInsteadOfAuto`, which gates the CSS §4.5 auto-min floor. Changelog: [General][Fixed] - Fix a flex line collapsing to its minimum sizes when the first item clamps to its min or max main size, behind the new `FlexFirstPassUsesRunningTotals` errata (set by default) Pull Request resolved: #2021 Test Plan: `buck2 test fbsource//xplat/yoga:tests` — 854 passed, 0 failed: https://www.internalfb.com/intern/testinfra/testrun/1125900433633637 `errata_preserves_prefix_geometry_by_default` pins the default (legacy) path at 60/30/30; the other `YGFirstChildMinWidth` cases clear the bit and assert 180/180/180. `YGConfigTest` needed the new bit cleared too, since it asserts exact errata equality. Without the gate, FDS `MediaWrapper` screenshot tests shifted — the footer button went from hug-content (~130px) to stretched (~305px) in all three variants (normal, dark, RTL). With the bit default-on those fixtures are unchanged. `arc lint` clean. `enums.py` re-runs with no drift. All vendored copies verified byte-identical to `xplat/yoga`: ReactCommon C++ (5 files) and ReactAndroid `YogaErrata.kt`. C++ stable API snapshots: `js1 build cxx-api` cannot run on macOS (the generator depends on `third-party/doxygen`, which is Linux-only), so the nine `.api` files were updated by hand to match the exact output `react-native-cxx-stable-api-snapshot-validation` reported. The single added line and its position in every file match the CI diff exactly; the job regenerates and re-verifies, so it is the check on this. Reviewed By: javache Differential Revision: D119142409 Pulled By: pasqualeanatriello fbshipit-source-id: 9e5ad19e2ca818a5cccdcac68cfb8905a6f40894
1 parent 9830084 commit 73bebc0

10 files changed

Lines changed: 182 additions & 6 deletions

File tree

‎enums.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@
9292
# default on new configs to preserve pre-§4.5 Yoga shrink behavior.
9393
# Clear this bit to opt into the spec-correct CSS §4.5 floor.
9494
("MinSizeUndefinedInsteadOfAuto", 1 << 3),
95+
# In the first free-space distribution pass, size each item against
96+
# the running flex totals, which shrink as earlier items freeze,
97+
# rather than against the totals captured before the pass began. This
98+
# inflates the share offered to later items so they freeze
99+
# spuriously, and makes the pass depend on child order. Set by
100+
# default on new configs to preserve the pre-fix geometry. Clear this
101+
# bit to opt into the spec-correct single distribution.
102+
("FlexFirstPassUsesRunningTotals", 1 << 4),
95103
# Enable all incorrect behavior (preserve compatibility)
96104
("All", 0x7FFFFFFF),
97105
# Enable all errata except for "StretchFlexBasis" (Defaults behavior

‎java/com/facebook/yoga/YogaErrata.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public enum class YogaErrata(public val intValue: Int) {
1515
ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING(2),
1616
ABSOLUTE_PERCENT_AGAINST_INNER_SIZE(4),
1717
MIN_SIZE_UNDEFINED_INSTEAD_OF_AUTO(8),
18+
FLEX_FIRST_PASS_USES_RUNNING_TOTALS(16),
1819
ALL(2147483647),
1920
CLASSIC(2147483646);
2021

@@ -29,6 +30,7 @@ public enum class YogaErrata(public val intValue: Int) {
2930
2 -> ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING
3031
4 -> ABSOLUTE_PERCENT_AGAINST_INNER_SIZE
3132
8 -> MIN_SIZE_UNDEFINED_INSTEAD_OF_AUTO
33+
16 -> FLEX_FIRST_PASS_USES_RUNNING_TOTALS
3234
2147483647 -> ALL
3335
2147483646 -> CLASSIC
3436
else -> throw IllegalArgumentException("Unknown enum value: $value")

‎javascript/src/generated/YGEnums.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export enum Errata {
6262
AbsolutePositionWithoutInsetsExcludesPadding = 2,
6363
AbsolutePercentAgainstInnerSize = 4,
6464
MinSizeUndefinedInsteadOfAuto = 8,
65+
FlexFirstPassUsesRunningTotals = 16,
6566
All = 2147483647,
6667
Classic = 2147483646,
6768
}
@@ -190,6 +191,7 @@ const constants = {
190191
ERRATA_ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING: Errata.AbsolutePositionWithoutInsetsExcludesPadding,
191192
ERRATA_ABSOLUTE_PERCENT_AGAINST_INNER_SIZE: Errata.AbsolutePercentAgainstInnerSize,
192193
ERRATA_MIN_SIZE_UNDEFINED_INSTEAD_OF_AUTO: Errata.MinSizeUndefinedInsteadOfAuto,
194+
ERRATA_FLEX_FIRST_PASS_USES_RUNNING_TOTALS: Errata.FlexFirstPassUsesRunningTotals,
193195
ERRATA_ALL: Errata.All,
194196
ERRATA_CLASSIC: Errata.Classic,
195197
EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS: ExperimentalFeature.WebFlexBasis,

‎tests/YGConfigTest.cpp‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,12 @@ TEST(YogaTest, config_update_invalidates_layout_detects_each_property) {
200200
TEST(YogaTest, config_errata_bitmask_add_remove_operations) {
201201
auto* config = static_cast<yoga::Config*>(YGConfigNew());
202202

203-
// Default config carries MinSizeUndefinedInsteadOfAuto (legacy preservation
204-
// for CSS §4.5 auto-min). Clear it for this test so we can assert exact
205-
// equality at the end.
203+
// Default configs carry the errata that preserve legacy geometry
204+
// (MinSizeUndefinedInsteadOfAuto for CSS §4.5 auto-min,
205+
// FlexFirstPassUsesRunningTotals for free-space distribution). Clear them
206+
// for this test so we can assert exact equality at the end.
206207
config->removeErrata(yoga::Errata::MinSizeUndefinedInsteadOfAuto);
208+
config->removeErrata(yoga::Errata::FlexFirstPassUsesRunningTotals);
207209

208210
// Initially no errata
209211
ASSERT_FALSE(config->hasErrata(yoga::Errata::StretchFlexBasis));

‎tests/YGFirstChildMinWidthTest.cpp‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
// Regression test for https://github.com/react/yoga/issues/2006
9+
// A row of three growable children (flexGrow/flexShrink 1, maxWidth 180) inside
10+
// a 540px container. When the *first* child has a larger minWidth than the
11+
// rest, the first free-space pass used to freeze every item at its max while
12+
// also draining the remaining free space to exactly zero, leaving the second
13+
// pass with nothing to distribute. The children then collapsed to their
14+
// minWidths (60/30/30) instead of growing to fill the row (180/180/180).
15+
//
16+
// The fix is gated on clearing YGErrataFlexFirstPassUsesRunningTotals, which
17+
// new configs set by default, so both behaviors are pinned here.
18+
19+
#include <array>
20+
21+
#include <gtest/gtest.h>
22+
#include <yoga/Yoga.h>
23+
24+
namespace {
25+
26+
// A config opted into the spec-correct single distribution.
27+
YGConfigRef makeFixedConfig() {
28+
YGConfigRef config = YGConfigNew();
29+
const YGErrata errata = YGConfigGetErrata(config);
30+
YGConfigSetErrata(
31+
config,
32+
static_cast<YGErrata>(errata & ~YGErrataFlexFirstPassUsesRunningTotals));
33+
return config;
34+
}
35+
36+
// Lay out a 540px row containing three children, each with flexGrow/
37+
// flexShrink of 1, maxWidth of 180 and the given minWidths, and return the
38+
// resolved width of each child.
39+
std::array<float, 3> layoutRow(
40+
YGConfigRef config,
41+
float minWidth0,
42+
float minWidth1,
43+
float minWidth2) {
44+
YGNodeRef root = YGNodeNewWithConfig(config);
45+
YGNodeStyleSetWidth(root, 540.0f);
46+
YGNodeStyleSetFlexDirection(root, YGFlexDirectionRow);
47+
48+
const float minWidths[] = {minWidth0, minWidth1, minWidth2};
49+
for (size_t i = 0; i < 3; i++) {
50+
YGNodeRef child = YGNodeNewWithConfig(config);
51+
YGNodeStyleSetFlexGrow(child, 1.0f);
52+
YGNodeStyleSetFlexShrink(child, 1.0f);
53+
YGNodeStyleSetMaxWidth(child, 180.0f);
54+
YGNodeStyleSetHeight(child, 30.0f);
55+
YGNodeStyleSetMinWidth(child, minWidths[i]);
56+
YGNodeInsertChild(root, child, i);
57+
}
58+
59+
YGNodeCalculateLayout(root, 540.0f, 30.0f, YGDirectionLTR);
60+
61+
std::array<float, 3> widths{};
62+
for (size_t i = 0; i < 3; i++) {
63+
widths[i] = YGNodeLayoutGetWidth(YGNodeGetChild(root, i));
64+
}
65+
66+
YGNodeFreeRecursive(root);
67+
return widths;
68+
}
69+
70+
// Assert every child grows to maxWidth (the row exactly fills the container).
71+
void expectAllGrowToMax(float minWidth0, float minWidth1, float minWidth2) {
72+
YGConfigRef config = makeFixedConfig();
73+
const auto widths = layoutRow(config, minWidth0, minWidth1, minWidth2);
74+
75+
for (size_t i = 0; i < 3; i++) {
76+
EXPECT_NEAR(180.0f, widths[i], 1e-3f)
77+
<< "child[" << i
78+
<< "] should grow to maxWidth, not collapse to its minWidth";
79+
}
80+
81+
YGConfigFree(config);
82+
}
83+
84+
} // namespace
85+
86+
TEST(YGFirstChildMinWidth, first_child_larger_minwidth_row) {
87+
// The originally reported case: the first child has a larger minWidth than
88+
// the other two, which used to break the whole row.
89+
expectAllGrowToMax(60.0f, 30.0f, 30.0f);
90+
}
91+
92+
TEST(YGFirstChildMinWidth, other_positions_still_work) {
93+
// Sanity: placing the outlier minWidth on the second or third child already
94+
// worked and must keep working.
95+
expectAllGrowToMax(30.0f, 60.0f, 30.0f);
96+
expectAllGrowToMax(30.0f, 30.0f, 60.0f);
97+
}
98+
99+
TEST(YGFirstChildMinWidth, uniform_minwidth_row) {
100+
// All children share the same minWidth: no issue either way.
101+
expectAllGrowToMax(30.0f, 30.0f, 30.0f);
102+
}
103+
104+
TEST(YGFirstChildMinWidth, errata_preserves_prefix_geometry_by_default) {
105+
// A default config keeps the pre-fix collapse, so existing layouts do not
106+
// shift when this change lands. Removing the errata bit from the defaults is
107+
// what makes this test fail.
108+
YGConfigRef config = YGConfigNew();
109+
ASSERT_NE(
110+
YGConfigGetErrata(config) & YGErrataFlexFirstPassUsesRunningTotals, 0);
111+
112+
const auto widths = layoutRow(config, 60.0f, 30.0f, 30.0f);
113+
EXPECT_NEAR(60.0f, widths[0], 1e-3f);
114+
EXPECT_NEAR(30.0f, widths[1], 1e-3f);
115+
EXPECT_NEAR(30.0f, widths[2], 1e-3f);
116+
117+
YGConfigFree(config);
118+
}
119+
120+
TEST(YGFirstChildMinWidth, errata_bit_round_trips) {
121+
YGConfigRef config = YGConfigNew();
122+
EXPECT_NE(
123+
YGConfigGetErrata(config) & YGErrataFlexFirstPassUsesRunningTotals, 0);
124+
125+
YGConfigSetErrata(
126+
config,
127+
static_cast<YGErrata>(
128+
YGConfigGetErrata(config) & ~YGErrataFlexFirstPassUsesRunningTotals));
129+
EXPECT_EQ(
130+
YGConfigGetErrata(config) & YGErrataFlexFirstPassUsesRunningTotals, 0);
131+
132+
YGConfigFree(config);
133+
}

‎yoga/YGEnums.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ const char* YGErrataToString(const YGErrata value) {
119119
return "absolute-percent-against-inner-size";
120120
case YGErrataMinSizeUndefinedInsteadOfAuto:
121121
return "min-size-undefined-instead-of-auto";
122+
case YGErrataFlexFirstPassUsesRunningTotals:
123+
return "flex-first-pass-uses-running-totals";
122124
case YGErrataAll:
123125
return "all";
124126
case YGErrataClassic:

‎yoga/YGEnums.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ YG_ENUM_DECL(
6868
YGErrataAbsolutePositionWithoutInsetsExcludesPadding = 2,
6969
YGErrataAbsolutePercentAgainstInnerSize = 4,
7070
YGErrataMinSizeUndefinedInsteadOfAuto = 8,
71+
YGErrataFlexFirstPassUsesRunningTotals = 16,
7172
YGErrataAll = 2147483647,
7273
YGErrataClassic = 2147483646)
7374
YG_DEFINE_ENUM_FLAG_OPERATORS(YGErrata)

‎yoga/algorithm/CalculateLayout.cpp‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,7 @@ static float distributeFreeSpaceSecondPass(
12671267
// whose min and max constraints are triggered, those flex item's clamped size
12681268
// is removed from the remaingfreespace.
12691269
static void distributeFreeSpaceFirstPass(
1270+
yoga::Node* const node,
12701271
FlexLine& flexLine,
12711272
const Direction direction,
12721273
const FlexDirection mainAxis,
@@ -1280,6 +1281,25 @@ static void distributeFreeSpaceFirstPass(
12801281
float boundMainSize = 0;
12811282
float deltaFreeSpace = 0;
12821283

1284+
// The first pass performs a single distribution of the free space over all
1285+
// of the line's flexible items, so every item's tentative size must be
1286+
// computed against the *original* totals. The totals are still reduced as
1287+
// items get frozen below (so the second pass can redistribute), but those
1288+
// reduced values must not feed back into the fair-share calculation for the
1289+
// remaining items: doing so inflates their tentative size and can freeze
1290+
// items that should still be able to grow/shrink (see
1291+
// https://github.com/react/yoga/issues/2006).
1292+
//
1293+
// Dividing by the running totals is the pre-fix behavior, preserved for
1294+
// existing layouts behind an errata bit that is set on new configs by
1295+
// default.
1296+
const bool useRunningTotals =
1297+
node->hasErrata(Errata::FlexFirstPassUsesRunningTotals);
1298+
const float originalTotalFlexGrowFactors =
1299+
flexLine.layout.totalFlexGrowFactors;
1300+
const float originalTotalFlexShrinkScaledFactors =
1301+
flexLine.layout.totalFlexShrinkScaledFactors;
1302+
12831303
for (auto currentLineChild : flexLine.itemsInFlow) {
12841304
float childFlexBasis = boundAxisWithinMinAndMax(
12851305
currentLineChild,
@@ -1299,7 +1319,8 @@ static void distributeFreeSpaceFirstPass(
12991319
flexShrinkScaledFactor != 0) {
13001320
baseMainSize = childFlexBasis +
13011321
flexLine.layout.remainingFreeSpace /
1302-
flexLine.layout.totalFlexShrinkScaledFactors *
1322+
(useRunningTotals ? flexLine.layout.totalFlexShrinkScaledFactors
1323+
: originalTotalFlexShrinkScaledFactors) *
13031324
flexShrinkScaledFactor;
13041325
boundMainSize = boundAxisWithAutoMin(
13051326
currentLineChild,
@@ -1329,7 +1350,9 @@ static void distributeFreeSpaceFirstPass(
13291350
if (yoga::isDefined(flexGrowFactor) && flexGrowFactor != 0) {
13301351
baseMainSize = childFlexBasis +
13311352
flexLine.layout.remainingFreeSpace /
1332-
flexLine.layout.totalFlexGrowFactors * flexGrowFactor;
1353+
(useRunningTotals ? flexLine.layout.totalFlexGrowFactors
1354+
: originalTotalFlexGrowFactors) *
1355+
flexGrowFactor;
13331356
boundMainSize = boundAxis(
13341357
currentLineChild,
13351358
mainAxis,
@@ -1419,6 +1442,7 @@ static void resolveFlexibleLength(
14191442

14201443
// First pass: detect the flex items whose min/max constraints trigger
14211444
distributeFreeSpaceFirstPass(
1445+
node,
14221446
flexLine,
14231447
direction,
14241448
mainAxis,

‎yoga/config/Config.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ class YG_EXPORT Config : public ::YGConfig {
7676

7777
uint32_t version_ = 0;
7878
ExperimentalFeatureSet experimentalFeatures_{};
79-
Errata errata_ = Errata::MinSizeUndefinedInsteadOfAuto;
79+
Errata errata_ = Errata::MinSizeUndefinedInsteadOfAuto |
80+
Errata::FlexFirstPassUsesRunningTotals;
8081
float pointScaleFactor_ = 1.0f;
8182
void* context_ = nullptr;
8283
};

‎yoga/enums/Errata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ enum class Errata : uint32_t {
2121
AbsolutePositionWithoutInsetsExcludesPadding = YGErrataAbsolutePositionWithoutInsetsExcludesPadding,
2222
AbsolutePercentAgainstInnerSize = YGErrataAbsolutePercentAgainstInnerSize,
2323
MinSizeUndefinedInsteadOfAuto = YGErrataMinSizeUndefinedInsteadOfAuto,
24+
FlexFirstPassUsesRunningTotals = YGErrataFlexFirstPassUsesRunningTotals,
2425
All = YGErrataAll,
2526
Classic = YGErrataClassic,
2627
};

0 commit comments

Comments
 (0)