Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions doc/layout/flexbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

<a name="flex-direction"></a>
## Flex Direction
Expand Down Expand Up @@ -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. |

<x-preview path="layout/flex_grow" size="md" source="example/lib/pages/layout/flex_grow.dart"></x-preview>
Expand Down
1 change: 0 additions & 1 deletion lib/src/parser/parsers/flexbox_grid_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ class FlexboxGridParser implements WindParserInterface {
/// Maps flex child properties to `FlexFit`
static const _flexFitMap = <String, FlexFit>{
'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,
Expand Down
21 changes: 21 additions & 0 deletions lib/src/widgets/w_div.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions test/parser/parsers/flexbox_grid_parser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand All @@ -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', () {
Expand All @@ -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(
Expand All @@ -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,
Expand Down
114 changes: 114 additions & 0 deletions test/widgets/w_div/flex_shrink_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Row>(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<RenderBox>(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 {
Expand Down
Loading