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..fb37d09 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,20 @@ class WDiv extends StatelessWidget { } } + /// 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 || 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 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..25c5700 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,7 @@ 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('returns unchanged styles when classes is null', () { @@ -183,9 +183,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 +197,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..aed9916 100644 --- a/test/widgets/w_div/flex_shrink_test.dart +++ b/test/widgets/w_div/flex_shrink_test.dart @@ -249,6 +249,120 @@ 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); + + // 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( + badgeWidth, + lessThan(400), + reason: + 'shrink-0 child must not expand to fill entire container width', + ); + }, + ); + testWidgets( 'flex-col should NOT auto-wrap children with Flexible', (tester) async {