From 9b2b06ad8cb2a2ae8d9610208f14c4320b4e3bcc Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Tue, 31 Mar 2026 21:07:21 +0300 Subject: [PATCH 1/2] fix: prevent Flexible wrapping on shrink-0 children in justify-between rows (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container-level wrapping logic incorrectly wrapped all non-flex children with Flexible when using justify-between, breaking space distribution when combined with flex-1. Add _hasShrinkZero check to skip wrapping shrink-0 children and remove incorrect shrink-0 → FlexFit.tight parser mapping — shrink-0 now correctly preserves intrinsic size. --- CHANGELOG.md | 5 + doc/layout/flexbox.md | 4 +- .../parser/parsers/flexbox_grid_parser.dart | 1 - lib/src/widgets/w_div.dart | 13 +++ .../parsers/flexbox_grid_parser_test.dart | 17 ++- test/widgets/w_div/flex_shrink_test.dart | 106 ++++++++++++++++++ 6 files changed, 137 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5cd927..fadb58c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0. ## [Unreleased] +### 🐛 Bug Fixes + +- Fixed: flex-1 + justify-between space distribution broken by incorrect Flexible wrapping (#45) +- Fixed: shrink-0 no longer creates Flexible wrapper — correctly preserves intrinsic size (#45) + --- ## [1.0.0-alpha.4] - 2026-03-24 diff --git a/doc/layout/flexbox.md b/doc/layout/flexbox.md index 40f16e2..193b82a 100644 --- a/doc/layout/flexbox.md +++ b/doc/layout/flexbox.md @@ -61,7 +61,7 @@ WDiv( | `items-{alignment}` | `align-items: ...` | `CrossAxisAlignment` | | `gap-{n}` | `gap: {n}` | `SizedBox` (spacer) | | `flex-1` | `flex: 1` | `Expanded()` | -| `shrink-0` | `flex-shrink: 0` | `Flexible(fit: FlexFit.tight)` | +| `shrink-0` | `flex-shrink: 0` | No wrapper — preserves intrinsic size | ## Flex Direction @@ -190,7 +190,7 @@ Control how individual children resize to fill available space. | `flex-grow` | Alias for `flex-1`. | | `flex-{n}` | Specific flex factor (e.g., `flex-2`). | | `shrink` | Allow child to shrink if needed (`FlexFit.loose`). | -| `shrink-0` | Prevent child from shrinking (`FlexFit.tight`). | +| `shrink-0` | Preserve intrinsic size — no Flexible wrapper, child keeps its natural dimensions. | | `flex-none` | Do not grow or shrink. | diff --git a/lib/src/parser/parsers/flexbox_grid_parser.dart b/lib/src/parser/parsers/flexbox_grid_parser.dart index c9d6600..551d54d 100644 --- a/lib/src/parser/parsers/flexbox_grid_parser.dart +++ b/lib/src/parser/parsers/flexbox_grid_parser.dart @@ -109,7 +109,6 @@ class FlexboxGridParser implements WindParserInterface { /// Maps flex child properties to `FlexFit` static const _flexFitMap = { 'shrink': FlexFit.loose, // flex-shrink: 1 (can shrink) - 'shrink-0': FlexFit.tight, // flex-shrink: 0 (don't shrink) 'flex-auto': FlexFit.loose, 'flex-initial': FlexFit.loose, 'flex-shrink': FlexFit.loose, diff --git a/lib/src/widgets/w_div.dart b/lib/src/widgets/w_div.dart index e49b7fe..09d658f 100644 --- a/lib/src/widgets/w_div.dart +++ b/lib/src/widgets/w_div.dart @@ -417,6 +417,13 @@ class WDiv extends StatelessWidget { if (child is WText && _hasFlexClass(child.className)) { return child; } + // Skip shrink-0 children (should not shrink — keep intrinsic size) + if (child is WDiv && _hasShrinkZero(child.className)) { + return child; + } + if (child is WText && _hasShrinkZero(child.className)) { + return child; + } return Flexible(child: child); }).toList() : gappedChildren; @@ -432,6 +439,12 @@ class WDiv extends StatelessWidget { } } + /// Checks if a className contains shrink-0 class that should preserve intrinsic size + static bool _hasShrinkZero(String? className) { + if (className == null) return false; + return className.contains('shrink-0'); + } + /// Checks if a className contains flex-N classes that produce Expanded widgets static bool _hasFlexClass(String? className) { if (className == null) return false; diff --git a/test/parser/parsers/flexbox_grid_parser_test.dart b/test/parser/parsers/flexbox_grid_parser_test.dart index 15b3a36..0830b06 100644 --- a/test/parser/parsers/flexbox_grid_parser_test.dart +++ b/test/parser/parsers/flexbox_grid_parser_test.dart @@ -155,7 +155,7 @@ void main() { test('parses shrink-0 class', () { final styles = parser.parse(WindStyle(), ['shrink-0'], context); - expect(styles.flexFit, FlexFit.tight); + expect(styles.flexFit, isNull); }); test('parses items-baseline class', () { @@ -167,7 +167,12 @@ void main() { test('applies last-class-wins for shrink', () { final styles = parser.parse(WindStyle(), ['shrink', 'shrink-0'], context); - expect(styles.flexFit, FlexFit.tight); + expect(styles.flexFit, FlexFit.loose); + }); + + test('shrink-0 should not set flexFit', () { + final styles = parser.parse(WindStyle(), ['shrink-0'], context); + expect(styles.flexFit, isNull); }); test('returns unchanged styles when classes is null', () { @@ -183,9 +188,9 @@ void main() { expect(styles.flexFit, FlexFit.loose); }); - test('shrink-0 -> FlexFit.tight', () { + test('shrink-0 does not set flexFit', () { final styles = parser.parse(WindStyle(), ['shrink-0'], context); - expect(styles.flexFit, FlexFit.tight); + expect(styles.flexFit, isNull); }); test( @@ -197,10 +202,10 @@ void main() { }); test('last-class-wins override logic', () { - // Flex shrink overrides + // Flex shrink overrides — shrink-0 no longer sets flexFit expect( parser.parse(WindStyle(), ['shrink', 'shrink-0'], context).flexFit, - FlexFit.tight, + FlexFit.loose, ); expect( parser.parse(WindStyle(), ['shrink-0', 'shrink'], context).flexFit, diff --git a/test/widgets/w_div/flex_shrink_test.dart b/test/widgets/w_div/flex_shrink_test.dart index c22fd8e..691956a 100644 --- a/test/widgets/w_div/flex_shrink_test.dart +++ b/test/widgets/w_div/flex_shrink_test.dart @@ -249,6 +249,112 @@ void main() { }, ); + testWidgets( + 'flex-row justify-between with flex-1 child should not wrap shrink-0 child with Flexible', + (tester) async { + // Bug: when needsSpaceDistribution is true, ALL non-flex children + // get wrapped with Flexible — including shrink-0 children that should + // keep their intrinsic size. The shrink-0 child must NOT be Flexible. + + await tester.pumpWidget( + MaterialApp( + home: WindTheme( + data: WindThemeData(), + child: SizedBox( + width: 300, + child: const WDiv( + className: 'flex flex-row justify-between', + children: [ + WDiv( + className: 'flex-1', + child: WText('Grows to fill space'), + ), + WDiv( + className: 'shrink-0', + child: WText('Fixed size'), + ), + ], + ), + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + + final row = tester.widget(find.byType(Row).first); + + // The shrink-0 child should NOT be wrapped with Flexible. + // Only non-flex, non-shrink-0 children may receive Flexible wrapping. + // Count Flexible wrappers that are NOT Expanded (i.e. flex:1 children). + final flexibleCount = row.children + .where((child) => child is Flexible && child is! Expanded) + .length; + + expect( + flexibleCount, + 0, + reason: + 'shrink-0 child must not be wrapped with Flexible when justify-between is used', + ); + }, + ); + + testWidgets( + 'flex-row justify-between with flex-1 child preserves non-flex child intrinsic size', + (tester) async { + // Bug: the shrink-0 child gets wrapped in Flexible, which forces + // Flutter to give it a flex allocation instead of its natural size. + // After the fix, the non-flex child must render at its intrinsic width. + + const fixedText = 'Badge'; + + await tester.pumpWidget( + MaterialApp( + home: WindTheme( + data: WindThemeData(), + child: SizedBox( + width: 400, + child: const WDiv( + className: 'flex flex-row justify-between', + children: [ + WDiv( + className: 'flex-1', + child: WText('Title that grows'), + ), + WDiv( + className: 'shrink-0 px-2', + child: WText(fixedText), + ), + ], + ), + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + + // The shrink-0 WDiv must NOT be a Flexible descendant directly inside + // the Row. Find the Row and inspect its direct children. + final row = tester.widget(find.byType(Row).first); + + // None of the direct Row children that correspond to a shrink-0 WDiv + // should be a Flexible widget. We verify this by asserting zero + // non-Expanded Flexible wrappers exist at the Row's children level. + final nonExpandedFlexible = row.children + .where((child) => child is Flexible && child is! Expanded) + .toList(); + + expect( + nonExpandedFlexible, + isEmpty, + reason: + 'shrink-0 child must retain intrinsic size — no Flexible wrapping allowed', + ); + }, + ); + testWidgets( 'flex-col should NOT auto-wrap children with Flexible', (tester) async { From 0bdbcb1c8ca1fabe26b60c5f900a5ec8f35615df Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Tue, 31 Mar 2026 21:26:56 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20token-based=20matching,=20dedup=20tests,=20RenderBo?= =?UTF-8?q?x=20assertion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _hasShrinkZero now tokenizes by whitespace and matches full tokens (bare shrink-0 and prefixed variants like md:shrink-0) - Removed duplicate shrink-0 parser test - Widget test now verifies intrinsic size via RenderBox instead of re-checking Flexible wrapping --- lib/src/widgets/w_div.dart | 14 ++++++-- .../parsers/flexbox_grid_parser_test.dart | 5 --- test/widgets/w_div/flex_shrink_test.dart | 36 +++++++++++-------- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/lib/src/widgets/w_div.dart b/lib/src/widgets/w_div.dart index 09d658f..fb37d09 100644 --- a/lib/src/widgets/w_div.dart +++ b/lib/src/widgets/w_div.dart @@ -439,10 +439,18 @@ class WDiv extends StatelessWidget { } } - /// Checks if a className contains shrink-0 class that should preserve intrinsic size + /// Checks if a className contains shrink-0 token that should preserve + /// intrinsic size. Uses token-based matching to avoid false positives + /// from substring matches (e.g. a hypothetical `no-shrink-0`). + /// Matches both bare `shrink-0` and prefixed variants like `md:shrink-0`. static bool _hasShrinkZero(String? className) { - if (className == null) return false; - return className.contains('shrink-0'); + if (className == null || className.isEmpty) return false; + for (final token in className.split(' ')) { + if (token == 'shrink-0' || token.endsWith(':shrink-0')) { + return true; + } + } + return false; } /// Checks if a className contains flex-N classes that produce Expanded widgets diff --git a/test/parser/parsers/flexbox_grid_parser_test.dart b/test/parser/parsers/flexbox_grid_parser_test.dart index 0830b06..25c5700 100644 --- a/test/parser/parsers/flexbox_grid_parser_test.dart +++ b/test/parser/parsers/flexbox_grid_parser_test.dart @@ -170,11 +170,6 @@ void main() { expect(styles.flexFit, FlexFit.loose); }); - test('shrink-0 should not set flexFit', () { - final styles = parser.parse(WindStyle(), ['shrink-0'], context); - expect(styles.flexFit, isNull); - }); - test('returns unchanged styles when classes is null', () { final initialStyles = WindStyle(); final styles = parser.parse(initialStyles, null, context); diff --git a/test/widgets/w_div/flex_shrink_test.dart b/test/widgets/w_div/flex_shrink_test.dart index 691956a..aed9916 100644 --- a/test/widgets/w_div/flex_shrink_test.dart +++ b/test/widgets/w_div/flex_shrink_test.dart @@ -335,22 +335,30 @@ void main() { expect(tester.takeException(), isNull); - // The shrink-0 WDiv must NOT be a Flexible descendant directly inside - // the Row. Find the Row and inspect its direct children. - final row = tester.widget(find.byType(Row).first); - - // None of the direct Row children that correspond to a shrink-0 WDiv - // should be a Flexible widget. We verify this by asserting zero - // non-Expanded Flexible wrappers exist at the Row's children level. - final nonExpandedFlexible = row.children - .where((child) => child is Flexible && child is! Expanded) - .toList(); - + // Verify the shrink-0 child is not forced into equal flex allocation. + // If it were wrapped in Flexible(flex: 1) alongside Expanded(flex: 1), + // both children would split the 400px container equally → 200px each. + // The shrink-0 child should retain its intrinsic width instead. + final badgeFinder = find.text(fixedText); + expect(badgeFinder, findsOneWidget); + + final badgeRenderBox = tester.renderObject(badgeFinder); + final badgeWidth = badgeRenderBox.size.width; + + expect(badgeWidth, greaterThan(0)); + // Must not equal the 50% split that Flexible(flex:1) would produce + expect( + badgeWidth, + isNot(equals(200.0)), + reason: + 'shrink-0 child must retain intrinsic size — not equal-flex split', + ); + // Must not fill the entire container expect( - nonExpandedFlexible, - isEmpty, + badgeWidth, + lessThan(400), reason: - 'shrink-0 child must retain intrinsic size — no Flexible wrapping allowed', + 'shrink-0 child must not expand to fill entire container width', ); }, );