Skip to content

Commit f90b422

Browse files
committed
fix: add row overflow strategies to prevent layout overflow
1 parent 9849689 commit f90b422

4 files changed

Lines changed: 154 additions & 4 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,26 @@ if (!result.isValid) {
147147
- `Button`
148148
- `Image`
149149

150+
`Row` overflow strategy:
151+
152+
- `overflow: "row"` (default): normal `Row`
153+
- `overflow: "wrap"`: uses `Wrap` to avoid horizontal overflow
154+
- `overflow: "scroll"`: horizontal scroll container
155+
156+
Example:
157+
158+
```json
159+
{
160+
"type": "Row",
161+
"props": {
162+
"spacing": 8,
163+
"runSpacing": 8,
164+
"overflow": "wrap"
165+
},
166+
"children": ["a", "b", "c"]
167+
}
168+
```
169+
150170
## Example App
151171

152172
A full multi-scenario showcase app is included in `/example`:

example/lib/main.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,11 @@ class _ShowcaseAppState extends State<ShowcaseApp> {
583583
},
584584
'buttons': <String, dynamic>{
585585
'type': 'Row',
586-
'props': <String, dynamic>{'spacing': 8},
586+
'props': <String, dynamic>{
587+
'spacing': 8,
588+
'runSpacing': 8,
589+
'overflow': 'wrap',
590+
},
587591
'children': <String>['dec', 'inc', 'toggle'],
588592
},
589593
'dec': <String, dynamic>{
@@ -656,7 +660,11 @@ class _ShowcaseAppState extends State<ShowcaseApp> {
656660
},
657661
'line': <String, dynamic>{
658662
'type': 'Row',
659-
'props': <String, dynamic>{'spacing': 8},
663+
'props': <String, dynamic>{
664+
'spacing': 8,
665+
'runSpacing': 8,
666+
'overflow': 'wrap',
667+
},
660668
'children': <String>[
661669
'name',
662670
'doneChip',

lib/src/components/standard_components.dart

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ final Map<String, JsonComponentDefinition> standardComponentDefinitions =
3838
description: 'Horizontal layout for children widgets.',
3939
props: <String, JsonPropDefinition>{
4040
'spacing': JsonPropDefinition(type: 'number', defaultValue: 0),
41+
'runSpacing': JsonPropDefinition(
42+
type: 'number',
43+
description: 'Used when overflow is set to "wrap".',
44+
),
45+
'overflow': JsonPropDefinition(
46+
type: 'string',
47+
enumValues: <String>['row', 'wrap', 'scroll'],
48+
defaultValue: 'row',
49+
description: 'How to handle narrow widths.',
50+
),
4151
'mainAxisAlignment': JsonPropDefinition(type: 'string'),
4252
'crossAxisAlignment': JsonPropDefinition(type: 'string'),
4353
'mainAxisSize': JsonPropDefinition(
@@ -135,14 +145,31 @@ Map<String, JsonComponentBuilder> standardComponentBuilders() {
135145
},
136146
'Row': (context) {
137147
final spacing = _toDouble(context.props['spacing']) ?? 0;
148+
final overflow = _parseRowOverflow(context.props['overflow']);
149+
150+
if (overflow == _RowOverflowBehavior.wrap) {
151+
return Wrap(
152+
spacing: spacing,
153+
runSpacing: _toDouble(context.props['runSpacing']) ?? spacing,
154+
alignment: _parseWrapAlignment(context.props['mainAxisAlignment']),
155+
crossAxisAlignment: _parseWrapCrossAlignment(
156+
context.props['crossAxisAlignment'],
157+
),
158+
children: context.children,
159+
);
160+
}
161+
138162
final children = _withSpacing(
139163
context.children,
140164
spacing: spacing,
141165
axis: Axis.horizontal,
142166
);
143167

144-
return Row(
145-
mainAxisSize: _parseMainAxisSize(context.props['mainAxisSize']),
168+
final row = Row(
169+
// Horizontal scroll views provide unconstrained width, so max is invalid.
170+
mainAxisSize: overflow == _RowOverflowBehavior.scroll
171+
? MainAxisSize.min
172+
: _parseMainAxisSize(context.props['mainAxisSize']),
146173
mainAxisAlignment: _parseMainAxisAlignment(
147174
context.props['mainAxisAlignment'],
148175
),
@@ -151,6 +178,15 @@ Map<String, JsonComponentBuilder> standardComponentBuilders() {
151178
),
152179
children: children,
153180
);
181+
182+
if (overflow == _RowOverflowBehavior.scroll) {
183+
return SingleChildScrollView(
184+
scrollDirection: Axis.horizontal,
185+
child: row,
186+
);
187+
}
188+
189+
return row;
154190
},
155191
'Container': (context) {
156192
final width = _toDouble(context.props['width']);
@@ -465,3 +501,44 @@ int? _toInt(dynamic raw) {
465501
if (raw is num) return raw.toInt();
466502
return int.tryParse(raw?.toString() ?? '');
467503
}
504+
505+
enum _RowOverflowBehavior { row, wrap, scroll }
506+
507+
_RowOverflowBehavior _parseRowOverflow(dynamic raw) {
508+
switch (raw?.toString()) {
509+
case 'wrap':
510+
return _RowOverflowBehavior.wrap;
511+
case 'scroll':
512+
return _RowOverflowBehavior.scroll;
513+
default:
514+
return _RowOverflowBehavior.row;
515+
}
516+
}
517+
518+
WrapAlignment _parseWrapAlignment(dynamic raw) {
519+
switch (raw?.toString()) {
520+
case 'end':
521+
return WrapAlignment.end;
522+
case 'center':
523+
return WrapAlignment.center;
524+
case 'spaceBetween':
525+
return WrapAlignment.spaceBetween;
526+
case 'spaceAround':
527+
return WrapAlignment.spaceAround;
528+
case 'spaceEvenly':
529+
return WrapAlignment.spaceEvenly;
530+
default:
531+
return WrapAlignment.start;
532+
}
533+
}
534+
535+
WrapCrossAlignment _parseWrapCrossAlignment(dynamic raw) {
536+
switch (raw?.toString()) {
537+
case 'end':
538+
return WrapCrossAlignment.end;
539+
case 'center':
540+
return WrapCrossAlignment.center;
541+
default:
542+
return WrapCrossAlignment.start;
543+
}
544+
}

test/flutter_json_render_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,49 @@ void main() {
142142
expect(find.text('Now Visible'), findsOneWidget);
143143
},
144144
);
145+
146+
testWidgets('Row component supports wrap overflow strategy', (tester) async {
147+
final spec = JsonRenderSpec.fromJson({
148+
'root': 'root',
149+
'elements': {
150+
'root': {
151+
'type': 'Row',
152+
'props': {'spacing': 8, 'runSpacing': 8, 'overflow': 'wrap'},
153+
'children': ['a', 'b', 'c'],
154+
},
155+
'a': {
156+
'type': 'Button',
157+
'props': {'label': 'Very long button A'},
158+
},
159+
'b': {
160+
'type': 'Button',
161+
'props': {'label': 'Very long button B'},
162+
},
163+
'c': {
164+
'type': 'Button',
165+
'props': {'label': 'Very long button C'},
166+
},
167+
},
168+
});
169+
170+
await tester.pumpWidget(
171+
MaterialApp(
172+
home: Scaffold(
173+
body: SizedBox(
174+
width: 220,
175+
child: JsonRenderer(
176+
spec: spec,
177+
registry: defineRegistry(components: standardComponentBuilders()),
178+
),
179+
),
180+
),
181+
),
182+
);
183+
184+
expect(find.byType(Wrap), findsOneWidget);
185+
expect(find.text('Very long button A'), findsOneWidget);
186+
expect(find.text('Very long button B'), findsOneWidget);
187+
expect(find.text('Very long button C'), findsOneWidget);
188+
expect(tester.takeException(), isNull);
189+
});
145190
}

0 commit comments

Comments
 (0)