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 {
@override
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> {
bool _withAction = true;
bool _multiLine = false;
bool _longActionLabel = false;
double _sliderValue = 0.25;
Padding _configRow(List<Widget> children) => Padding(
padding: const EdgeInsets.all(8.0), child: Row(children: children));
Padding _padRow(List<Widget> children) => Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: children),
);
@override
Widget build(BuildContext context) {
return Padding(padding: const EdgeInsets.only(left: 50.0), child: Column(
children: <Widget>[
_configRow(<Widget>[
_padRow(<Widget>[
Text('Snack Bar configuration',
style: Theme.of(context).textTheme.bodyLarge),
]),
_configRow(
_padRow(
<Widget>[
const Text('Fixed'),
Radio<SnackBarBehavior>(
......@@ -79,7 +82,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
),
],
),
_configRow(
_padRow(
<Widget>[
const Text('Include Icon '),
Switch(
......@@ -92,7 +95,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
),
],
),
_configRow(
_padRow(
<Widget>[
const Text('Include Action '),
Switch(
......@@ -117,7 +120,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
),
],
),
_configRow(
_padRow(
<Widget>[
const Text('Multi Line Text'),
Switch(
......@@ -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),
ElevatedButton(
child: const Text('Show Snackbar'),
......@@ -163,6 +181,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
behavior: _snackBarBehavior,
action: action,
duration: const Duration(seconds: 3),
actionOverflowThreshold: _sliderValue,
);
}
}
......@@ -239,6 +239,7 @@ class SnackBar extends StatefulWidget {
this.shape,
this.behavior,
this.action,
this.actionOverflowThreshold,
this.showCloseIcon,
this.closeIconColor,
this.duration = _snackBarDisplayDuration,
......@@ -247,10 +248,11 @@ class SnackBar extends StatefulWidget {
this.dismissDirection = DismissDirection.down,
this.clipBehavior = Clip.hardEdge,
}) : assert(elevation == null || elevation >= 0.0),
assert(
width == null || margin == null,
assert(width == null || margin == null,
'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.
///
......@@ -358,6 +360,18 @@ class SnackBar extends StatefulWidget {
/// The action should not be "dismiss" or "cancel".
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.
///
/// Tapping the icon will close the snack bar.
......@@ -431,6 +445,7 @@ class SnackBar extends StatefulWidget {
shape: shape,
behavior: behavior,
action: action,
actionOverflowThreshold: actionOverflowThreshold,
showCloseIcon: showCloseIcon,
closeIconColor: closeIconColor,
duration: duration,
......@@ -601,10 +616,11 @@ class _SnackBarState extends State<SnackBar> {
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);
// Action and Icon will overflow to a new line if their width is greater
// than one quarter of the total Snack Bar width.
final bool actionLineOverflow =
actionAndIconWidth / snackBarWidth > 0.25;
final double actionOverflowThreshold = widget.actionOverflowThreshold
?? snackBarTheme.actionOverflowThreshold
?? defaults.actionOverflowThreshold!;
final bool willOverflowAction = actionAndIconWidth / snackBarWidth > actionOverflowThreshold;
final List<Widget> maybeActionAndIcon = <Widget>[
if (widget.action != null)
......@@ -645,17 +661,16 @@ class _SnackBarState extends State<SnackBar> {
),
),
),
if(!actionLineOverflow) ...maybeActionAndIcon,
if(actionLineOverflow) SizedBox(width: snackBarWidth*0.4),
if (!willOverflowAction) ...maybeActionAndIcon,
if (willOverflowAction) SizedBox(width: snackBarWidth * 0.4),
],
),
if(actionLineOverflow) Padding(
if (willOverflowAction)
Padding(
padding: const EdgeInsets.only(bottom: _singleLineVerticalPadding),
child: Row(mainAxisAlignment: MainAxisAlignment.end,
children: maybeActionAndIcon),
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: maybeActionAndIcon),
),
],
),
);
......@@ -820,6 +835,9 @@ class _SnackbarDefaultsM2 extends SnackBarThemeData {
@override
Color get closeIconColor => _colors.onSurface;
@override
double get actionOverflowThreshold => 0.25;
}
// BEGIN GENERATED TOKEN PROPERTIES - Snackbar
......@@ -884,6 +902,12 @@ class _SnackbarDefaultsM3 extends SnackBarThemeData {
@override
bool get showCloseIcon => false;
@override
Color? get closeIconColor => _colors.onInverseSurface;
@override
double get actionOverflowThreshold => 0.25;
}
// END GENERATED TOKEN PROPERTIES - Snackbar
......@@ -64,11 +64,13 @@ class SnackBarThemeData with Diagnosticable {
this.insetPadding,
this.showCloseIcon,
this.closeIconColor,
this.actionOverflowThreshold,
}) : assert(elevation == null || elevation >= 0.0),
assert(
width == null ||
(identical(behavior, SnackBarBehavior.floating)),
'Width can only be set if behaviour is SnackBarBehavior.floating');
assert(width == null || identical(behavior, SnackBarBehavior.floating),
'Width can only be set if behaviour is SnackBarBehavior.floating'),
assert(actionOverflowThreshold == null || (actionOverflowThreshold >= 0 && actionOverflowThreshold <= 1),
'Action overflow threshold must be between 0 and 1 inclusive');
/// Overrides the default value for [SnackBar.backgroundColor].
///
/// If null, [SnackBar] defaults to dark grey: `Color(0xFF323232)`.
......@@ -133,6 +135,11 @@ class SnackBarThemeData with Diagnosticable {
/// This value is only used if [showCloseIcon] is true.
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
/// new values.
SnackBarThemeData copyWith({
......@@ -147,6 +154,7 @@ class SnackBarThemeData with Diagnosticable {
EdgeInsets? insetPadding,
bool? showCloseIcon,
Color? closeIconColor,
double? actionOverflowThreshold,
}) {
return SnackBarThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
......@@ -160,6 +168,7 @@ class SnackBarThemeData with Diagnosticable {
insetPadding: insetPadding ?? this.insetPadding,
showCloseIcon: showCloseIcon ?? this.showCloseIcon,
closeIconColor: closeIconColor ?? this.closeIconColor,
actionOverflowThreshold: actionOverflowThreshold ?? this.actionOverflowThreshold,
);
}
......@@ -180,6 +189,7 @@ class SnackBarThemeData with Diagnosticable {
width: lerpDouble(a?.width, b?.width, t),
insetPadding: EdgeInsets.lerp(a?.insetPadding, b?.insetPadding, t),
closeIconColor: Color.lerp(a?.closeIconColor, b?.closeIconColor, t),
actionOverflowThreshold: lerpDouble(a?.actionOverflowThreshold, b?.actionOverflowThreshold, t),
);
}
......@@ -196,6 +206,7 @@ class SnackBarThemeData with Diagnosticable {
insetPadding,
showCloseIcon,
closeIconColor,
actionOverflowThreshold,
);
@override
......@@ -217,7 +228,8 @@ class SnackBarThemeData with Diagnosticable {
&& other.width == width
&& other.insetPadding == insetPadding
&& other.showCloseIcon == showCloseIcon
&& other.closeIconColor == closeIconColor;
&& other.closeIconColor == closeIconColor
&& other.actionOverflowThreshold == actionOverflowThreshold;
}
@override
......@@ -234,5 +246,6 @@ class SnackBarThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<EdgeInsets>('insetPadding', insetPadding, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('showCloseIcon', showCloseIcon, defaultValue: null));
properties.add(ColorProperty('closeIconColor', closeIconColor, defaultValue: null));
properties.add(DoubleProperty('actionOverflowThreshold', actionOverflowThreshold, defaultValue: null));
}
}
......@@ -2317,6 +2317,7 @@ void main() {
required SnackBarBehavior? behavior,
EdgeInsetsGeometry? margin,
double? width,
double? actionOverflowThreshold,
}) {
return MaterialApp(
home: Scaffold(
......@@ -2335,6 +2336,7 @@ void main() {
content: const Text('I am a snack bar.'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
actionOverflowThreshold: actionOverflowThreshold,
));
},
child: const Text('X'),
......@@ -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 {
// Regression test for https://github.com/flutter/flutter/issues/98205
await tester.pumpWidget(MaterialApp(
......@@ -2556,7 +2574,7 @@ void main() {
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(
home: Scaffold(
bottomSheet: SizedBox(
......@@ -2583,6 +2601,33 @@ void main() {
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(
'ScaffoldMessenger will alert for snackbars that cannot be presented', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/103004
......
......@@ -25,6 +25,7 @@ void main() {
expect(snackBarTheme.insetPadding, null);
expect(snackBarTheme.showCloseIcon, null);
expect(snackBarTheme.closeIconColor, null);
expect(snackBarTheme.actionOverflowThreshold, null);
});
test(
......@@ -65,6 +66,7 @@ void main() {
insetPadding: EdgeInsets.all(10.0),
showCloseIcon: false,
closeIconColor: Color(0xFF0000AA),
actionOverflowThreshold: 0.5,
).debugFillProperties(builder);
final List<String> description = builder.properties
......@@ -84,6 +86,7 @@ void main() {
'insetPadding: EdgeInsets.all(10.0)',
'showCloseIcon: false',
'closeIconColor: Color(0xff0000aa)',
'actionOverflowThreshold: 0.5',
]);
});
......@@ -313,10 +316,14 @@ void main() {
required SnackBarBehavior themedBehavior,
EdgeInsetsGeometry? margin,
double? width,
double? themedActionOverflowThreshold,
}) {
return MaterialApp(
theme: ThemeData(
snackBarTheme: SnackBarThemeData(behavior: themedBehavior),
snackBarTheme: SnackBarThemeData(
behavior: themedBehavior,
actionOverflowThreshold: themedActionOverflowThreshold,
),
),
home: Scaffold(
floatingActionButton: FloatingActionButton(
......@@ -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 {
// SnackBarBehavior.floating set in theme does not assert with width
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