Skip to content

Commit 17b4c70

Browse files
author
Eilidh Southren
authored
[M3] Add customizable overflow property to Snackbar's action (#120394)
* add actionOverflowThreshold param * analyzer tings * https://www.youtube.com/watch?v=NPwyyjtxlzU * remove erroneous switch changes * rename test * remove unwanted switch.dart diff * remove redundant values * review changes
1 parent b0edf58 commit 17b4c70

6 files changed

Lines changed: 155 additions & 31 deletions

File tree

dev/tools/gen_defaults/lib/snackbar_template.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ class _${blockName}DefaultsM3 extends SnackBarThemeData {
6767
6868
@override
6969
bool get showCloseIcon => false;
70+
71+
@override
72+
Color? get closeIconColor => ${componentColor("$tokenGroup.icon")};
73+
74+
@override
75+
double get actionOverflowThreshold => 0.25;
7076
}
7177
''';
7278
}

examples/api/lib/material/snack_bar/snack_bar.2.dart

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,22 @@ class _SnackBarExampleState extends State<SnackBarExample> {
4343
bool _withAction = true;
4444
bool _multiLine = false;
4545
bool _longActionLabel = false;
46+
double _sliderValue = 0.25;
4647

47-
Padding _configRow(List<Widget> children) => Padding(
48-
padding: const EdgeInsets.all(8.0), child: Row(children: children));
48+
Padding _padRow(List<Widget> children) => Padding(
49+
padding: const EdgeInsets.all(8.0),
50+
child: Row(children: children),
51+
);
4952

5053
@override
5154
Widget build(BuildContext context) {
5255
return Padding(padding: const EdgeInsets.only(left: 50.0), child: Column(
5356
children: <Widget>[
54-
_configRow(<Widget>[
57+
_padRow(<Widget>[
5558
Text('Snack Bar configuration',
5659
style: Theme.of(context).textTheme.bodyLarge),
5760
]),
58-
_configRow(
61+
_padRow(
5962
<Widget>[
6063
const Text('Fixed'),
6164
Radio<SnackBarBehavior>(
@@ -79,7 +82,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
7982
),
8083
],
8184
),
82-
_configRow(
85+
_padRow(
8386
<Widget>[
8487
const Text('Include Icon '),
8588
Switch(
@@ -92,7 +95,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
9295
),
9396
],
9497
),
95-
_configRow(
98+
_padRow(
9699
<Widget>[
97100
const Text('Include Action '),
98101
Switch(
@@ -117,7 +120,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
117120
),
118121
],
119122
),
120-
_configRow(
123+
_padRow(
121124
<Widget>[
122125
const Text('Multi Line Text'),
123126
Switch(
@@ -130,6 +133,21 @@ class _SnackBarExampleState extends State<SnackBarExample> {
130133
),
131134
],
132135
),
136+
_padRow(
137+
<Widget>[
138+
const Text('Action new-line overflow threshold'),
139+
Slider(
140+
value: _sliderValue,
141+
divisions: 20,
142+
label: _sliderValue.toStringAsFixed(2),
143+
onChanged: _snackBarBehavior == SnackBarBehavior.fixed ? null : (double value) {
144+
setState(() {
145+
_sliderValue = value;
146+
});
147+
},
148+
),
149+
]
150+
),
133151
const SizedBox(height: 16.0),
134152
ElevatedButton(
135153
child: const Text('Show Snackbar'),
@@ -163,6 +181,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
163181
behavior: _snackBarBehavior,
164182
action: action,
165183
duration: const Duration(seconds: 3),
184+
actionOverflowThreshold: _sliderValue,
166185
);
167186
}
168187
}

packages/flutter/lib/src/material/snack_bar.dart

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ class SnackBar extends StatefulWidget {
239239
this.shape,
240240
this.behavior,
241241
this.action,
242+
this.actionOverflowThreshold,
242243
this.showCloseIcon,
243244
this.closeIconColor,
244245
this.duration = _snackBarDisplayDuration,
@@ -247,10 +248,11 @@ class SnackBar extends StatefulWidget {
247248
this.dismissDirection = DismissDirection.down,
248249
this.clipBehavior = Clip.hardEdge,
249250
}) : assert(elevation == null || elevation >= 0.0),
250-
assert(
251-
width == null || margin == null,
251+
assert(width == null || margin == null,
252252
'Width and margin can not be used together',
253-
);
253+
),
254+
assert(actionOverflowThreshold == null || (actionOverflowThreshold >= 0 && actionOverflowThreshold <= 1),
255+
'Action overflow threshold must be between 0 and 1 inclusive');
254256

255257
/// The primary content of the snack bar.
256258
///
@@ -358,6 +360,18 @@ class SnackBar extends StatefulWidget {
358360
/// The action should not be "dismiss" or "cancel".
359361
final SnackBarAction? action;
360362

363+
/// (optional) The percentage threshold for action widget's width before it overflows
364+
/// to a new line.
365+
///
366+
/// Must be between 0 and 1. If the width of the snackbar's [content] is greater
367+
/// than this percentage of the width of the snackbar less the width of its [action],
368+
/// then the [action] will appear below the [content].
369+
///
370+
/// At a value of 0, the action will not overflow to a new line.
371+
///
372+
/// Defaults to 0.25.
373+
final double? actionOverflowThreshold;
374+
361375
/// (optional) Whether to include a "close" icon widget.
362376
///
363377
/// Tapping the icon will close the snack bar.
@@ -431,6 +445,7 @@ class SnackBar extends StatefulWidget {
431445
shape: shape,
432446
behavior: behavior,
433447
action: action,
448+
actionOverflowThreshold: actionOverflowThreshold,
434449
showCloseIcon: showCloseIcon,
435450
closeIconColor: closeIconColor,
436451
duration: duration,
@@ -601,10 +616,11 @@ class _SnackBarState extends State<SnackBar> {
601616
final EdgeInsets margin = widget.margin?.resolve(TextDirection.ltr) ?? snackBarTheme.insetPadding ?? defaults.insetPadding!;
602617

603618
final double snackBarWidth = widget.width ?? MediaQuery.sizeOf(context).width - (margin.left + margin.right);
604-
// Action and Icon will overflow to a new line if their width is greater
605-
// than one quarter of the total Snack Bar width.
606-
final bool actionLineOverflow =
607-
actionAndIconWidth / snackBarWidth > 0.25;
619+
final double actionOverflowThreshold = widget.actionOverflowThreshold
620+
?? snackBarTheme.actionOverflowThreshold
621+
?? defaults.actionOverflowThreshold!;
622+
623+
final bool willOverflowAction = actionAndIconWidth / snackBarWidth > actionOverflowThreshold;
608624

609625
final List<Widget> maybeActionAndIcon = <Widget>[
610626
if (widget.action != null)
@@ -645,18 +661,17 @@ class _SnackBarState extends State<SnackBar> {
645661
),
646662
),
647663
),
648-
if(!actionLineOverflow) ...maybeActionAndIcon,
649-
if(actionLineOverflow) SizedBox(width: snackBarWidth*0.4),
664+
if (!willOverflowAction) ...maybeActionAndIcon,
665+
if (willOverflowAction) SizedBox(width: snackBarWidth * 0.4),
650666
],
651667
),
652-
if(actionLineOverflow) Padding(
653-
padding: const EdgeInsets.only(bottom: _singleLineVerticalPadding),
654-
child: Row(mainAxisAlignment: MainAxisAlignment.end,
655-
children: maybeActionAndIcon),
656-
),
657-
],
658-
659-
),
668+
if (willOverflowAction)
669+
Padding(
670+
padding: const EdgeInsets.only(bottom: _singleLineVerticalPadding),
671+
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: maybeActionAndIcon),
672+
),
673+
],
674+
),
660675
);
661676

662677
if (!isFloatingSnackBar) {
@@ -820,6 +835,9 @@ class _SnackbarDefaultsM2 extends SnackBarThemeData {
820835

821836
@override
822837
Color get closeIconColor => _colors.onSurface;
838+
839+
@override
840+
double get actionOverflowThreshold => 0.25;
823841
}
824842

825843
// BEGIN GENERATED TOKEN PROPERTIES - Snackbar
@@ -884,6 +902,12 @@ class _SnackbarDefaultsM3 extends SnackBarThemeData {
884902

885903
@override
886904
bool get showCloseIcon => false;
905+
906+
@override
907+
Color? get closeIconColor => _colors.onInverseSurface;
908+
909+
@override
910+
double get actionOverflowThreshold => 0.25;
887911
}
888912

889913
// END GENERATED TOKEN PROPERTIES - Snackbar

packages/flutter/lib/src/material/snack_bar_theme.dart

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ class SnackBarThemeData with Diagnosticable {
6464
this.insetPadding,
6565
this.showCloseIcon,
6666
this.closeIconColor,
67+
this.actionOverflowThreshold,
6768
}) : assert(elevation == null || elevation >= 0.0),
68-
assert(
69-
width == null ||
70-
(identical(behavior, SnackBarBehavior.floating)),
71-
'Width can only be set if behaviour is SnackBarBehavior.floating');
69+
assert(width == null || identical(behavior, SnackBarBehavior.floating),
70+
'Width can only be set if behaviour is SnackBarBehavior.floating'),
71+
assert(actionOverflowThreshold == null || (actionOverflowThreshold >= 0 && actionOverflowThreshold <= 1),
72+
'Action overflow threshold must be between 0 and 1 inclusive');
73+
7274
/// Overrides the default value for [SnackBar.backgroundColor].
7375
///
7476
/// If null, [SnackBar] defaults to dark grey: `Color(0xFF323232)`.
@@ -133,6 +135,11 @@ class SnackBarThemeData with Diagnosticable {
133135
/// This value is only used if [showCloseIcon] is true.
134136
final Color? closeIconColor;
135137

138+
/// Overrides the default value for [SnackBar.actionOverflowThreshold].
139+
///
140+
/// Must be a value between 0 and 1, if present.
141+
final double? actionOverflowThreshold;
142+
136143
/// Creates a copy of this object with the given fields replaced with the
137144
/// new values.
138145
SnackBarThemeData copyWith({
@@ -147,6 +154,7 @@ class SnackBarThemeData with Diagnosticable {
147154
EdgeInsets? insetPadding,
148155
bool? showCloseIcon,
149156
Color? closeIconColor,
157+
double? actionOverflowThreshold,
150158
}) {
151159
return SnackBarThemeData(
152160
backgroundColor: backgroundColor ?? this.backgroundColor,
@@ -160,6 +168,7 @@ class SnackBarThemeData with Diagnosticable {
160168
insetPadding: insetPadding ?? this.insetPadding,
161169
showCloseIcon: showCloseIcon ?? this.showCloseIcon,
162170
closeIconColor: closeIconColor ?? this.closeIconColor,
171+
actionOverflowThreshold: actionOverflowThreshold ?? this.actionOverflowThreshold,
163172
);
164173
}
165174

@@ -180,6 +189,7 @@ class SnackBarThemeData with Diagnosticable {
180189
width: lerpDouble(a?.width, b?.width, t),
181190
insetPadding: EdgeInsets.lerp(a?.insetPadding, b?.insetPadding, t),
182191
closeIconColor: Color.lerp(a?.closeIconColor, b?.closeIconColor, t),
192+
actionOverflowThreshold: lerpDouble(a?.actionOverflowThreshold, b?.actionOverflowThreshold, t),
183193
);
184194
}
185195

@@ -196,6 +206,7 @@ class SnackBarThemeData with Diagnosticable {
196206
insetPadding,
197207
showCloseIcon,
198208
closeIconColor,
209+
actionOverflowThreshold,
199210
);
200211

201212
@override
@@ -217,7 +228,8 @@ class SnackBarThemeData with Diagnosticable {
217228
&& other.width == width
218229
&& other.insetPadding == insetPadding
219230
&& other.showCloseIcon == showCloseIcon
220-
&& other.closeIconColor == closeIconColor;
231+
&& other.closeIconColor == closeIconColor
232+
&& other.actionOverflowThreshold == actionOverflowThreshold;
221233
}
222234

223235
@override
@@ -234,5 +246,6 @@ class SnackBarThemeData with Diagnosticable {
234246
properties.add(DiagnosticsProperty<EdgeInsets>('insetPadding', insetPadding, defaultValue: null));
235247
properties.add(DiagnosticsProperty<bool>('showCloseIcon', showCloseIcon, defaultValue: null));
236248
properties.add(ColorProperty('closeIconColor', closeIconColor, defaultValue: null));
249+
properties.add(DoubleProperty('actionOverflowThreshold', actionOverflowThreshold, defaultValue: null));
237250
}
238251
}

packages/flutter/test/material/snack_bar_test.dart

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2317,6 +2317,7 @@ void main() {
23172317
required SnackBarBehavior? behavior,
23182318
EdgeInsetsGeometry? margin,
23192319
double? width,
2320+
double? actionOverflowThreshold,
23202321
}) {
23212322
return MaterialApp(
23222323
home: Scaffold(
@@ -2335,6 +2336,7 @@ void main() {
23352336
content: const Text('I am a snack bar.'),
23362337
duration: const Duration(seconds: 2),
23372338
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
2339+
actionOverflowThreshold: actionOverflowThreshold,
23382340
));
23392341
},
23402342
child: const Text('X'),
@@ -2413,6 +2415,22 @@ void main() {
24132415
);
24142416
});
24152417

2418+
for (final double overflowThreshold in <double>[-1.0, -.0001, 1.000001, 5]) {
2419+
testWidgets('SnackBar will assert for actionOverflowThreshold outside of 0-1 range', (WidgetTester tester) async {
2420+
await tester.pumpWidget(doBuildApp(
2421+
actionOverflowThreshold: overflowThreshold,
2422+
behavior: SnackBarBehavior.fixed,
2423+
));
2424+
await tester.tap(find.text('X'));
2425+
await tester.pump(); // start animation
2426+
await tester.pump(const Duration(milliseconds: 750));
2427+
2428+
final AssertionError exception = tester.takeException() as AssertionError;
2429+
expect(exception.message, 'Action overflow threshold must be between 0 and 1 inclusive');
2430+
});
2431+
}
2432+
2433+
24162434
testWidgets('Snackbar by default clips BackdropFilter', (WidgetTester tester) async {
24172435
// Regression test for https://github.com/flutter/flutter/issues/98205
24182436
await tester.pumpWidget(MaterialApp(
@@ -2556,7 +2574,7 @@ void main() {
25562574
matchesGoldenFile('snack_bar.goldenTest.floatingWithIcon.png'));
25572575
});
25582576

2559-
testWidgets('Fixed multi-line snackbar with icon is aligned correctly', (WidgetTester tester) async {
2577+
testWidgets('Floating multi-line snackbar with icon is aligned correctly', (WidgetTester tester) async {
25602578
await tester.pumpWidget(const MaterialApp(
25612579
home: Scaffold(
25622580
bottomSheet: SizedBox(
@@ -2583,6 +2601,33 @@ void main() {
25832601
matchesGoldenFile('snack_bar.goldenTest.multiLineWithIcon.png'));
25842602
});
25852603

2604+
testWidgets('Floating multi-line snackbar with icon and actionOverflowThreshold=1 is aligned correctly', (WidgetTester tester) async {
2605+
await tester.pumpWidget(const MaterialApp(
2606+
home: Scaffold(
2607+
bottomSheet: SizedBox(
2608+
width: 200,
2609+
height: 50,
2610+
child: ColoredBox(
2611+
color: Colors.pink,
2612+
),
2613+
),
2614+
),
2615+
));
2616+
2617+
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
2618+
scaffoldMessengerState.showSnackBar(const SnackBar(
2619+
content: Text('This is a really long snackbar message. So long, it spans across more than one line!'),
2620+
duration: Duration(seconds: 2),
2621+
showCloseIcon: true,
2622+
behavior: SnackBarBehavior.floating,
2623+
actionOverflowThreshold: 1,
2624+
));
2625+
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
2626+
2627+
await expectLater(find.byType(MaterialApp),
2628+
matchesGoldenFile('snack_bar.goldenTest.multiLineWithIconWithZeroActionOverflowThreshold.png'));
2629+
});
2630+
25862631
testWidgets(
25872632
'ScaffoldMessenger will alert for snackbars that cannot be presented', (WidgetTester tester) async {
25882633
// Regression test for https://github.com/flutter/flutter/issues/103004

0 commit comments

Comments
 (0)