Unverified Commit 17b4c70f authored by Eilidh Southren's avatar Eilidh Southren Committed by GitHub

[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
parent b0edf582
...@@ -67,6 +67,12 @@ class _${blockName}DefaultsM3 extends SnackBarThemeData { ...@@ -67,6 +67,12 @@ class _${blockName}DefaultsM3 extends SnackBarThemeData {
@override @override
bool get showCloseIcon => false; bool get showCloseIcon => false;
@override
Color? get closeIconColor => ${componentColor("$tokenGroup.icon")};
@override
double get actionOverflowThreshold => 0.25;
} }
'''; ''';
} }
...@@ -43,19 +43,22 @@ class _SnackBarExampleState extends State<SnackBarExample> { ...@@ -43,19 +43,22 @@ class _SnackBarExampleState extends State<SnackBarExample> {
bool _withAction = true; bool _withAction = true;
bool _multiLine = false; bool _multiLine = false;
bool _longActionLabel = false; bool _longActionLabel = false;
double _sliderValue = 0.25;
Padding _configRow(List<Widget> children) => Padding( Padding _padRow(List<Widget> children) => Padding(
padding: const EdgeInsets.all(8.0), child: Row(children: children)); padding: const EdgeInsets.all(8.0),
child: Row(children: children),
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding(padding: const EdgeInsets.only(left: 50.0), child: Column( return Padding(padding: const EdgeInsets.only(left: 50.0), child: Column(
children: <Widget>[ children: <Widget>[
_configRow(<Widget>[ _padRow(<Widget>[
Text('Snack Bar configuration', Text('Snack Bar configuration',
style: Theme.of(context).textTheme.bodyLarge), style: Theme.of(context).textTheme.bodyLarge),
]), ]),
_configRow( _padRow(
<Widget>[ <Widget>[
const Text('Fixed'), const Text('Fixed'),
Radio<SnackBarBehavior>( Radio<SnackBarBehavior>(
...@@ -79,7 +82,7 @@ class _SnackBarExampleState extends State<SnackBarExample> { ...@@ -79,7 +82,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
), ),
], ],
), ),
_configRow( _padRow(
<Widget>[ <Widget>[
const Text('Include Icon '), const Text('Include Icon '),
Switch( Switch(
...@@ -92,7 +95,7 @@ class _SnackBarExampleState extends State<SnackBarExample> { ...@@ -92,7 +95,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
), ),
], ],
), ),
_configRow( _padRow(
<Widget>[ <Widget>[
const Text('Include Action '), const Text('Include Action '),
Switch( Switch(
...@@ -117,7 +120,7 @@ class _SnackBarExampleState extends State<SnackBarExample> { ...@@ -117,7 +120,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
), ),
], ],
), ),
_configRow( _padRow(
<Widget>[ <Widget>[
const Text('Multi Line Text'), const Text('Multi Line Text'),
Switch( Switch(
...@@ -130,6 +133,21 @@ class _SnackBarExampleState extends State<SnackBarExample> { ...@@ -130,6 +133,21 @@ class _SnackBarExampleState extends State<SnackBarExample> {
), ),
], ],
), ),
_padRow(
<Widget>[
const Text('Action new-line overflow threshold'),
Slider(
value: _sliderValue,
divisions: 20,
label: _sliderValue.toStringAsFixed(2),
onChanged: _snackBarBehavior == SnackBarBehavior.fixed ? null : (double value) {
setState(() {
_sliderValue = value;
});
},
),
]
),
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
ElevatedButton( ElevatedButton(
child: const Text('Show Snackbar'), child: const Text('Show Snackbar'),
...@@ -163,6 +181,7 @@ class _SnackBarExampleState extends State<SnackBarExample> { ...@@ -163,6 +181,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
behavior: _snackBarBehavior, behavior: _snackBarBehavior,
action: action, action: action,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
actionOverflowThreshold: _sliderValue,
); );
} }
} }
...@@ -239,6 +239,7 @@ class SnackBar extends StatefulWidget { ...@@ -239,6 +239,7 @@ class SnackBar extends StatefulWidget {
this.shape, this.shape,
this.behavior, this.behavior,
this.action, this.action,
this.actionOverflowThreshold,
this.showCloseIcon, this.showCloseIcon,
this.closeIconColor, this.closeIconColor,
this.duration = _snackBarDisplayDuration, this.duration = _snackBarDisplayDuration,
...@@ -247,10 +248,11 @@ class SnackBar extends StatefulWidget { ...@@ -247,10 +248,11 @@ class SnackBar extends StatefulWidget {
this.dismissDirection = DismissDirection.down, this.dismissDirection = DismissDirection.down,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
assert( assert(width == null || margin == null,
width == null || margin == null,
'Width and margin can not be used together', 'Width and margin can not be used together',
); ),
assert(actionOverflowThreshold == null || (actionOverflowThreshold >= 0 && actionOverflowThreshold <= 1),
'Action overflow threshold must be between 0 and 1 inclusive');
/// The primary content of the snack bar. /// The primary content of the snack bar.
/// ///
...@@ -358,6 +360,18 @@ class SnackBar extends StatefulWidget { ...@@ -358,6 +360,18 @@ class SnackBar extends StatefulWidget {
/// The action should not be "dismiss" or "cancel". /// The action should not be "dismiss" or "cancel".
final SnackBarAction? action; final SnackBarAction? action;
/// (optional) The percentage threshold for action widget's width before it overflows
/// to a new line.
///
/// Must be between 0 and 1. If the width of the snackbar's [content] is greater
/// than this percentage of the width of the snackbar less the width of its [action],
/// then the [action] will appear below the [content].
///
/// At a value of 0, the action will not overflow to a new line.
///
/// Defaults to 0.25.
final double? actionOverflowThreshold;
/// (optional) Whether to include a "close" icon widget. /// (optional) Whether to include a "close" icon widget.
/// ///
/// Tapping the icon will close the snack bar. /// Tapping the icon will close the snack bar.
...@@ -431,6 +445,7 @@ class SnackBar extends StatefulWidget { ...@@ -431,6 +445,7 @@ class SnackBar extends StatefulWidget {
shape: shape, shape: shape,
behavior: behavior, behavior: behavior,
action: action, action: action,
actionOverflowThreshold: actionOverflowThreshold,
showCloseIcon: showCloseIcon, showCloseIcon: showCloseIcon,
closeIconColor: closeIconColor, closeIconColor: closeIconColor,
duration: duration, duration: duration,
...@@ -601,10 +616,11 @@ class _SnackBarState extends State<SnackBar> { ...@@ -601,10 +616,11 @@ class _SnackBarState extends State<SnackBar> {
final EdgeInsets margin = widget.margin?.resolve(TextDirection.ltr) ?? snackBarTheme.insetPadding ?? defaults.insetPadding!; final EdgeInsets margin = widget.margin?.resolve(TextDirection.ltr) ?? snackBarTheme.insetPadding ?? defaults.insetPadding!;
final double snackBarWidth = widget.width ?? MediaQuery.sizeOf(context).width - (margin.left + margin.right); final double snackBarWidth = widget.width ?? MediaQuery.sizeOf(context).width - (margin.left + margin.right);
// Action and Icon will overflow to a new line if their width is greater final double actionOverflowThreshold = widget.actionOverflowThreshold
// than one quarter of the total Snack Bar width. ?? snackBarTheme.actionOverflowThreshold
final bool actionLineOverflow = ?? defaults.actionOverflowThreshold!;
actionAndIconWidth / snackBarWidth > 0.25;
final bool willOverflowAction = actionAndIconWidth / snackBarWidth > actionOverflowThreshold;
final List<Widget> maybeActionAndIcon = <Widget>[ final List<Widget> maybeActionAndIcon = <Widget>[
if (widget.action != null) if (widget.action != null)
...@@ -645,18 +661,17 @@ class _SnackBarState extends State<SnackBar> { ...@@ -645,18 +661,17 @@ class _SnackBarState extends State<SnackBar> {
), ),
), ),
), ),
if(!actionLineOverflow) ...maybeActionAndIcon, if (!willOverflowAction) ...maybeActionAndIcon,
if(actionLineOverflow) SizedBox(width: snackBarWidth*0.4), if (willOverflowAction) SizedBox(width: snackBarWidth * 0.4),
], ],
), ),
if(actionLineOverflow) Padding( if (willOverflowAction)
padding: const EdgeInsets.only(bottom: _singleLineVerticalPadding), Padding(
child: Row(mainAxisAlignment: MainAxisAlignment.end, padding: const EdgeInsets.only(bottom: _singleLineVerticalPadding),
children: maybeActionAndIcon), child: Row(mainAxisAlignment: MainAxisAlignment.end, children: maybeActionAndIcon),
), ),
], ],
),
),
); );
if (!isFloatingSnackBar) { if (!isFloatingSnackBar) {
...@@ -820,6 +835,9 @@ class _SnackbarDefaultsM2 extends SnackBarThemeData { ...@@ -820,6 +835,9 @@ class _SnackbarDefaultsM2 extends SnackBarThemeData {
@override @override
Color get closeIconColor => _colors.onSurface; Color get closeIconColor => _colors.onSurface;
@override
double get actionOverflowThreshold => 0.25;
} }
// BEGIN GENERATED TOKEN PROPERTIES - Snackbar // BEGIN GENERATED TOKEN PROPERTIES - Snackbar
...@@ -884,6 +902,12 @@ class _SnackbarDefaultsM3 extends SnackBarThemeData { ...@@ -884,6 +902,12 @@ class _SnackbarDefaultsM3 extends SnackBarThemeData {
@override @override
bool get showCloseIcon => false; bool get showCloseIcon => false;
@override
Color? get closeIconColor => _colors.onInverseSurface;
@override
double get actionOverflowThreshold => 0.25;
} }
// END GENERATED TOKEN PROPERTIES - Snackbar // END GENERATED TOKEN PROPERTIES - Snackbar
...@@ -64,11 +64,13 @@ class SnackBarThemeData with Diagnosticable { ...@@ -64,11 +64,13 @@ class SnackBarThemeData with Diagnosticable {
this.insetPadding, this.insetPadding,
this.showCloseIcon, this.showCloseIcon,
this.closeIconColor, this.closeIconColor,
this.actionOverflowThreshold,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
assert( assert(width == null || identical(behavior, SnackBarBehavior.floating),
width == null || 'Width can only be set if behaviour is SnackBarBehavior.floating'),
(identical(behavior, SnackBarBehavior.floating)), assert(actionOverflowThreshold == null || (actionOverflowThreshold >= 0 && actionOverflowThreshold <= 1),
'Width can only be set if behaviour is SnackBarBehavior.floating'); 'Action overflow threshold must be between 0 and 1 inclusive');
/// Overrides the default value for [SnackBar.backgroundColor]. /// Overrides the default value for [SnackBar.backgroundColor].
/// ///
/// If null, [SnackBar] defaults to dark grey: `Color(0xFF323232)`. /// If null, [SnackBar] defaults to dark grey: `Color(0xFF323232)`.
...@@ -133,6 +135,11 @@ class SnackBarThemeData with Diagnosticable { ...@@ -133,6 +135,11 @@ class SnackBarThemeData with Diagnosticable {
/// This value is only used if [showCloseIcon] is true. /// This value is only used if [showCloseIcon] is true.
final Color? closeIconColor; final Color? closeIconColor;
/// Overrides the default value for [SnackBar.actionOverflowThreshold].
///
/// Must be a value between 0 and 1, if present.
final double? actionOverflowThreshold;
/// Creates a copy of this object with the given fields replaced with the /// Creates a copy of this object with the given fields replaced with the
/// new values. /// new values.
SnackBarThemeData copyWith({ SnackBarThemeData copyWith({
...@@ -147,6 +154,7 @@ class SnackBarThemeData with Diagnosticable { ...@@ -147,6 +154,7 @@ class SnackBarThemeData with Diagnosticable {
EdgeInsets? insetPadding, EdgeInsets? insetPadding,
bool? showCloseIcon, bool? showCloseIcon,
Color? closeIconColor, Color? closeIconColor,
double? actionOverflowThreshold,
}) { }) {
return SnackBarThemeData( return SnackBarThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
...@@ -160,6 +168,7 @@ class SnackBarThemeData with Diagnosticable { ...@@ -160,6 +168,7 @@ class SnackBarThemeData with Diagnosticable {
insetPadding: insetPadding ?? this.insetPadding, insetPadding: insetPadding ?? this.insetPadding,
showCloseIcon: showCloseIcon ?? this.showCloseIcon, showCloseIcon: showCloseIcon ?? this.showCloseIcon,
closeIconColor: closeIconColor ?? this.closeIconColor, closeIconColor: closeIconColor ?? this.closeIconColor,
actionOverflowThreshold: actionOverflowThreshold ?? this.actionOverflowThreshold,
); );
} }
...@@ -180,6 +189,7 @@ class SnackBarThemeData with Diagnosticable { ...@@ -180,6 +189,7 @@ class SnackBarThemeData with Diagnosticable {
width: lerpDouble(a?.width, b?.width, t), width: lerpDouble(a?.width, b?.width, t),
insetPadding: EdgeInsets.lerp(a?.insetPadding, b?.insetPadding, t), insetPadding: EdgeInsets.lerp(a?.insetPadding, b?.insetPadding, t),
closeIconColor: Color.lerp(a?.closeIconColor, b?.closeIconColor, t), closeIconColor: Color.lerp(a?.closeIconColor, b?.closeIconColor, t),
actionOverflowThreshold: lerpDouble(a?.actionOverflowThreshold, b?.actionOverflowThreshold, t),
); );
} }
...@@ -196,6 +206,7 @@ class SnackBarThemeData with Diagnosticable { ...@@ -196,6 +206,7 @@ class SnackBarThemeData with Diagnosticable {
insetPadding, insetPadding,
showCloseIcon, showCloseIcon,
closeIconColor, closeIconColor,
actionOverflowThreshold,
); );
@override @override
...@@ -217,7 +228,8 @@ class SnackBarThemeData with Diagnosticable { ...@@ -217,7 +228,8 @@ class SnackBarThemeData with Diagnosticable {
&& other.width == width && other.width == width
&& other.insetPadding == insetPadding && other.insetPadding == insetPadding
&& other.showCloseIcon == showCloseIcon && other.showCloseIcon == showCloseIcon
&& other.closeIconColor == closeIconColor; && other.closeIconColor == closeIconColor
&& other.actionOverflowThreshold == actionOverflowThreshold;
} }
@override @override
...@@ -234,5 +246,6 @@ class SnackBarThemeData with Diagnosticable { ...@@ -234,5 +246,6 @@ class SnackBarThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<EdgeInsets>('insetPadding', insetPadding, defaultValue: null)); properties.add(DiagnosticsProperty<EdgeInsets>('insetPadding', insetPadding, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('showCloseIcon', showCloseIcon, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('showCloseIcon', showCloseIcon, defaultValue: null));
properties.add(ColorProperty('closeIconColor', closeIconColor, defaultValue: null)); properties.add(ColorProperty('closeIconColor', closeIconColor, defaultValue: null));
properties.add(DoubleProperty('actionOverflowThreshold', actionOverflowThreshold, defaultValue: null));
} }
} }
...@@ -2317,6 +2317,7 @@ void main() { ...@@ -2317,6 +2317,7 @@ void main() {
required SnackBarBehavior? behavior, required SnackBarBehavior? behavior,
EdgeInsetsGeometry? margin, EdgeInsetsGeometry? margin,
double? width, double? width,
double? actionOverflowThreshold,
}) { }) {
return MaterialApp( return MaterialApp(
home: Scaffold( home: Scaffold(
...@@ -2335,6 +2336,7 @@ void main() { ...@@ -2335,6 +2336,7 @@ void main() {
content: const Text('I am a snack bar.'), content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}), action: SnackBarAction(label: 'ACTION', onPressed: () {}),
actionOverflowThreshold: actionOverflowThreshold,
)); ));
}, },
child: const Text('X'), child: const Text('X'),
...@@ -2413,6 +2415,22 @@ void main() { ...@@ -2413,6 +2415,22 @@ void main() {
); );
}); });
for (final double overflowThreshold in <double>[-1.0, -.0001, 1.000001, 5]) {
testWidgets('SnackBar will assert for actionOverflowThreshold outside of 0-1 range', (WidgetTester tester) async {
await tester.pumpWidget(doBuildApp(
actionOverflowThreshold: overflowThreshold,
behavior: SnackBarBehavior.fixed,
));
await tester.tap(find.text('X'));
await tester.pump(); // start animation
await tester.pump(const Duration(milliseconds: 750));
final AssertionError exception = tester.takeException() as AssertionError;
expect(exception.message, 'Action overflow threshold must be between 0 and 1 inclusive');
});
}
testWidgets('Snackbar by default clips BackdropFilter', (WidgetTester tester) async { testWidgets('Snackbar by default clips BackdropFilter', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/98205 // Regression test for https://github.com/flutter/flutter/issues/98205
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
...@@ -2556,7 +2574,7 @@ void main() { ...@@ -2556,7 +2574,7 @@ void main() {
matchesGoldenFile('snack_bar.goldenTest.floatingWithIcon.png')); matchesGoldenFile('snack_bar.goldenTest.floatingWithIcon.png'));
}); });
testWidgets('Fixed multi-line snackbar with icon is aligned correctly', (WidgetTester tester) async { testWidgets('Floating multi-line snackbar with icon is aligned correctly', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp( await tester.pumpWidget(const MaterialApp(
home: Scaffold( home: Scaffold(
bottomSheet: SizedBox( bottomSheet: SizedBox(
...@@ -2583,6 +2601,33 @@ void main() { ...@@ -2583,6 +2601,33 @@ void main() {
matchesGoldenFile('snack_bar.goldenTest.multiLineWithIcon.png')); matchesGoldenFile('snack_bar.goldenTest.multiLineWithIcon.png'));
}); });
testWidgets('Floating multi-line snackbar with icon and actionOverflowThreshold=1 is aligned correctly', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
bottomSheet: SizedBox(
width: 200,
height: 50,
child: ColoredBox(
color: Colors.pink,
),
),
),
));
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
scaffoldMessengerState.showSnackBar(const SnackBar(
content: Text('This is a really long snackbar message. So long, it spans across more than one line!'),
duration: Duration(seconds: 2),
showCloseIcon: true,
behavior: SnackBarBehavior.floating,
actionOverflowThreshold: 1,
));
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
await expectLater(find.byType(MaterialApp),
matchesGoldenFile('snack_bar.goldenTest.multiLineWithIconWithZeroActionOverflowThreshold.png'));
});
testWidgets( testWidgets(
'ScaffoldMessenger will alert for snackbars that cannot be presented', (WidgetTester tester) async { 'ScaffoldMessenger will alert for snackbars that cannot be presented', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/103004 // Regression test for https://github.com/flutter/flutter/issues/103004
......
...@@ -25,6 +25,7 @@ void main() { ...@@ -25,6 +25,7 @@ void main() {
expect(snackBarTheme.insetPadding, null); expect(snackBarTheme.insetPadding, null);
expect(snackBarTheme.showCloseIcon, null); expect(snackBarTheme.showCloseIcon, null);
expect(snackBarTheme.closeIconColor, null); expect(snackBarTheme.closeIconColor, null);
expect(snackBarTheme.actionOverflowThreshold, null);
}); });
test( test(
...@@ -65,6 +66,7 @@ void main() { ...@@ -65,6 +66,7 @@ void main() {
insetPadding: EdgeInsets.all(10.0), insetPadding: EdgeInsets.all(10.0),
showCloseIcon: false, showCloseIcon: false,
closeIconColor: Color(0xFF0000AA), closeIconColor: Color(0xFF0000AA),
actionOverflowThreshold: 0.5,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -84,6 +86,7 @@ void main() { ...@@ -84,6 +86,7 @@ void main() {
'insetPadding: EdgeInsets.all(10.0)', 'insetPadding: EdgeInsets.all(10.0)',
'showCloseIcon: false', 'showCloseIcon: false',
'closeIconColor: Color(0xff0000aa)', 'closeIconColor: Color(0xff0000aa)',
'actionOverflowThreshold: 0.5',
]); ]);
}); });
...@@ -313,10 +316,14 @@ void main() { ...@@ -313,10 +316,14 @@ void main() {
required SnackBarBehavior themedBehavior, required SnackBarBehavior themedBehavior,
EdgeInsetsGeometry? margin, EdgeInsetsGeometry? margin,
double? width, double? width,
double? themedActionOverflowThreshold,
}) { }) {
return MaterialApp( return MaterialApp(
theme: ThemeData( theme: ThemeData(
snackBarTheme: SnackBarThemeData(behavior: themedBehavior), snackBarTheme: SnackBarThemeData(
behavior: themedBehavior,
actionOverflowThreshold: themedActionOverflowThreshold,
),
), ),
home: Scaffold( home: Scaffold(
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
...@@ -372,6 +379,16 @@ void main() { ...@@ -372,6 +379,16 @@ void main() {
); );
}); });
for (final double overflowThreshold in <double>[-1.0, -.0001, 1.000001, 5]) {
test('SnackBar theme will assert for actionOverflowThreshold outside of 0-1 range', () {
expect(
() => SnackBarThemeData(
actionOverflowThreshold: overflowThreshold,
),
throwsAssertionError);
});
}
testWidgets('SnackBar theme behavior will assert properly for width use', (WidgetTester tester) async { testWidgets('SnackBar theme behavior will assert properly for width use', (WidgetTester tester) async {
// SnackBarBehavior.floating set in theme does not assert with width // SnackBarBehavior.floating set in theme does not assert with width
await tester.pumpWidget(buildApp( await tester.pumpWidget(buildApp(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment