Unverified Commit af61c6c7 authored by Shanmugam's avatar Shanmugam Committed by GitHub

Support for dismissDirection property in SnackBarTheme (#139068)

This PR introduces a new feature that allows users to configure the 'dismissDirection' in SnackBarTheme. This enhancement provides users with the flexibility to set the 'dismissDirection' property in the ThemeData, rather than having to apply it each time when initializing a snack bar. This streamlines the process and makes it more convenient for users to manage and customize the behavior of snack bars within their applications.

Fixes #139012
parent 88d65375
...@@ -289,7 +289,7 @@ class SnackBar extends StatefulWidget { ...@@ -289,7 +289,7 @@ class SnackBar extends StatefulWidget {
this.duration = _snackBarDisplayDuration, this.duration = _snackBarDisplayDuration,
this.animation, this.animation,
this.onVisible, this.onVisible,
this.dismissDirection = DismissDirection.down, this.dismissDirection,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
assert(width == null || margin == null, assert(width == null || margin == null,
...@@ -463,8 +463,10 @@ class SnackBar extends StatefulWidget { ...@@ -463,8 +463,10 @@ class SnackBar extends StatefulWidget {
/// The direction in which the SnackBar can be dismissed. /// The direction in which the SnackBar can be dismissed.
/// ///
/// Defaults to [DismissDirection.down]. /// If this property is null, then [SnackBarThemeData.dismissDirection] of
final DismissDirection dismissDirection; /// [ThemeData.snackBarTheme] is used. If that is null, then the default is
/// [DismissDirection.down].
final DismissDirection? dismissDirection;
/// {@macro flutter.material.Material.clipBehavior} /// {@macro flutter.material.Material.clipBehavior}
/// ///
...@@ -737,6 +739,7 @@ class _SnackBarState extends State<SnackBar> { ...@@ -737,6 +739,7 @@ class _SnackBarState extends State<SnackBar> {
final double elevation = widget.elevation ?? snackBarTheme.elevation ?? defaults.elevation!; final double elevation = widget.elevation ?? snackBarTheme.elevation ?? defaults.elevation!;
final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? defaults.backgroundColor!; final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? defaults.backgroundColor!;
final ShapeBorder? shape = widget.shape ?? snackBarTheme.shape ?? (isFloatingSnackBar ? defaults.shape : null); final ShapeBorder? shape = widget.shape ?? snackBarTheme.shape ?? (isFloatingSnackBar ? defaults.shape : null);
final DismissDirection dismissDirection = widget.dismissDirection ?? snackBarTheme.dismissDirection ?? DismissDirection.down;
snackBar = Material( snackBar = Material(
shape: shape, shape: shape,
...@@ -783,7 +786,7 @@ class _SnackBarState extends State<SnackBar> { ...@@ -783,7 +786,7 @@ class _SnackBarState extends State<SnackBar> {
}, },
child: Dismissible( child: Dismissible(
key: const Key('dismissible'), key: const Key('dismissible'),
direction: widget.dismissDirection, direction: dismissDirection,
resizeDuration: null, resizeDuration: null,
behavior: widget.hitTestBehavior ?? (widget.margin != null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque), behavior: widget.hitTestBehavior ?? (widget.margin != null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque),
onDismissed: (DismissDirection direction) { onDismissed: (DismissDirection direction) {
......
...@@ -70,7 +70,8 @@ class SnackBarThemeData with Diagnosticable { ...@@ -70,7 +70,8 @@ class SnackBarThemeData with Diagnosticable {
this.closeIconColor, this.closeIconColor,
this.actionOverflowThreshold, this.actionOverflowThreshold,
this.actionBackgroundColor, this.actionBackgroundColor,
this.disabledActionBackgroundColor this.disabledActionBackgroundColor,
this.dismissDirection,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
assert(width == null || identical(behavior, SnackBarBehavior.floating), assert(width == null || identical(behavior, SnackBarBehavior.floating),
'Width can only be set if behaviour is SnackBarBehavior.floating'), 'Width can only be set if behaviour is SnackBarBehavior.floating'),
...@@ -158,6 +159,11 @@ class SnackBarThemeData with Diagnosticable { ...@@ -158,6 +159,11 @@ class SnackBarThemeData with Diagnosticable {
/// If null, [SnackBarAction] falls back to [Colors.transparent]. /// If null, [SnackBarAction] falls back to [Colors.transparent].
final Color? disabledActionBackgroundColor; final Color? disabledActionBackgroundColor;
/// Overrides the default value for [SnackBar.dismissDirection].
///
/// If null, [SnackBar] will default to [DismissDirection.down].
final DismissDirection? dismissDirection;
/// 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({
...@@ -175,6 +181,7 @@ class SnackBarThemeData with Diagnosticable { ...@@ -175,6 +181,7 @@ class SnackBarThemeData with Diagnosticable {
double? actionOverflowThreshold, double? actionOverflowThreshold,
Color? actionBackgroundColor, Color? actionBackgroundColor,
Color? disabledActionBackgroundColor, Color? disabledActionBackgroundColor,
DismissDirection? dismissDirection,
}) { }) {
return SnackBarThemeData( return SnackBarThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
...@@ -191,6 +198,7 @@ class SnackBarThemeData with Diagnosticable { ...@@ -191,6 +198,7 @@ class SnackBarThemeData with Diagnosticable {
actionOverflowThreshold: actionOverflowThreshold ?? this.actionOverflowThreshold, actionOverflowThreshold: actionOverflowThreshold ?? this.actionOverflowThreshold,
actionBackgroundColor: actionBackgroundColor ?? this.actionBackgroundColor, actionBackgroundColor: actionBackgroundColor ?? this.actionBackgroundColor,
disabledActionBackgroundColor: disabledActionBackgroundColor ?? this.disabledActionBackgroundColor, disabledActionBackgroundColor: disabledActionBackgroundColor ?? this.disabledActionBackgroundColor,
dismissDirection: dismissDirection ?? this.dismissDirection,
); );
} }
...@@ -215,6 +223,7 @@ class SnackBarThemeData with Diagnosticable { ...@@ -215,6 +223,7 @@ class SnackBarThemeData with Diagnosticable {
actionOverflowThreshold: lerpDouble(a?.actionOverflowThreshold, b?.actionOverflowThreshold, t), actionOverflowThreshold: lerpDouble(a?.actionOverflowThreshold, b?.actionOverflowThreshold, t),
actionBackgroundColor: Color.lerp(a?.actionBackgroundColor, b?.actionBackgroundColor, t), actionBackgroundColor: Color.lerp(a?.actionBackgroundColor, b?.actionBackgroundColor, t),
disabledActionBackgroundColor: Color.lerp(a?.disabledActionBackgroundColor, b?.disabledActionBackgroundColor, t), disabledActionBackgroundColor: Color.lerp(a?.disabledActionBackgroundColor, b?.disabledActionBackgroundColor, t),
dismissDirection: t < 0.5 ? a?.dismissDirection : b?.dismissDirection,
); );
} }
...@@ -233,7 +242,8 @@ class SnackBarThemeData with Diagnosticable { ...@@ -233,7 +242,8 @@ class SnackBarThemeData with Diagnosticable {
closeIconColor, closeIconColor,
actionOverflowThreshold, actionOverflowThreshold,
actionBackgroundColor, actionBackgroundColor,
disabledActionBackgroundColor disabledActionBackgroundColor,
dismissDirection,
); );
@override @override
...@@ -258,7 +268,8 @@ class SnackBarThemeData with Diagnosticable { ...@@ -258,7 +268,8 @@ class SnackBarThemeData with Diagnosticable {
&& other.closeIconColor == closeIconColor && other.closeIconColor == closeIconColor
&& other.actionOverflowThreshold == actionOverflowThreshold && other.actionOverflowThreshold == actionOverflowThreshold
&& other.actionBackgroundColor == actionBackgroundColor && other.actionBackgroundColor == actionBackgroundColor
&& other.disabledActionBackgroundColor == disabledActionBackgroundColor; && other.disabledActionBackgroundColor == disabledActionBackgroundColor
&& other.dismissDirection == dismissDirection;
} }
@override @override
...@@ -278,5 +289,6 @@ class SnackBarThemeData with Diagnosticable { ...@@ -278,5 +289,6 @@ class SnackBarThemeData with Diagnosticable {
properties.add(DoubleProperty('actionOverflowThreshold', actionOverflowThreshold, defaultValue: null)); properties.add(DoubleProperty('actionOverflowThreshold', actionOverflowThreshold, defaultValue: null));
properties.add(ColorProperty('actionBackgroundColor', actionBackgroundColor, defaultValue: null)); properties.add(ColorProperty('actionBackgroundColor', actionBackgroundColor, defaultValue: null));
properties.add(ColorProperty('disabledActionBackgroundColor', disabledActionBackgroundColor, defaultValue: null)); properties.add(ColorProperty('disabledActionBackgroundColor', disabledActionBackgroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<DismissDirection>('dismissDirection', dismissDirection, defaultValue: null));
} }
} }
...@@ -265,6 +265,101 @@ void main() { ...@@ -265,6 +265,101 @@ void main() {
); );
}); });
testWidgetsWithLeakTracking('SnackBar dismissDirection can be customised from SnackBarThemeData', (WidgetTester tester) async {
const Key tapTarget = Key('tap-target');
late double width;
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
snackBarTheme: const SnackBarThemeData(
dismissDirection: DismissDirection.startToEnd,
)
),
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
width = MediaQuery.sizeOf(context).width;
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('swipe ltr'),
duration: Duration(seconds: 2),
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
expect(find.text('swipe ltr'), findsNothing);
await tester.tap(find.byKey(tapTarget));
expect(find.text('swipe ltr'), findsNothing);
await tester.pump(); // schedule animation for snack bar
expect(find.text('swipe ltr'), findsOneWidget);
await tester.pump(); // begin animation
expect(find.text('swipe ltr'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750));
await tester.drag(find.text('swipe ltr'), Offset(width, 0.0));
await tester.pump(); // snack bar dismissed
expect(find.text('swipe ltr'), findsNothing);
});
testWidgetsWithLeakTracking('dismissDirection from SnackBar should be preferred over SnackBarThemeData', (WidgetTester tester) async {
const Key tapTarget = Key('tap-target');
late double width;
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
snackBarTheme: const SnackBarThemeData(
dismissDirection: DismissDirection.startToEnd,
)
),
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
width = MediaQuery.sizeOf(context).width;
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('swipe rtl'),
duration: Duration(seconds: 2),
dismissDirection: DismissDirection.endToStart,
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
expect(find.text('swipe rtl'), findsNothing);
await tester.tap(find.byKey(tapTarget));
expect(find.text('swipe rtl'), findsNothing);
await tester.pump(); // schedule animation for snack bar
expect(find.text('swipe rtl'), findsOneWidget);
await tester.pump(); // begin animation
expect(find.text('swipe rtl'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750));
await tester.drag(find.text('swipe rtl'), Offset(-width, 0.0));
await tester.pump(); // snack bar dismissed
expect(find.text('swipe rtl'), findsNothing);
});
testWidgetsWithLeakTracking('SnackBar cannot be tapped twice', (WidgetTester tester) async { testWidgetsWithLeakTracking('SnackBar cannot be tapped twice', (WidgetTester tester) async {
int tapCount = 0; int tapCount = 0;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
......
...@@ -74,6 +74,7 @@ void main() { ...@@ -74,6 +74,7 @@ void main() {
showCloseIcon: false, showCloseIcon: false,
closeIconColor: Color(0xFF0000AA), closeIconColor: Color(0xFF0000AA),
actionOverflowThreshold: 0.5, actionOverflowThreshold: 0.5,
dismissDirection: DismissDirection.down,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -94,6 +95,7 @@ void main() { ...@@ -94,6 +95,7 @@ void main() {
'showCloseIcon: false', 'showCloseIcon: false',
'closeIconColor: Color(0xff0000aa)', 'closeIconColor: Color(0xff0000aa)',
'actionOverflowThreshold: 0.5', 'actionOverflowThreshold: 0.5',
'dismissDirection: DismissDirection.down',
]); ]);
}); });
...@@ -618,7 +620,8 @@ SnackBarThemeData _createSnackBarTheme({ ...@@ -618,7 +620,8 @@ SnackBarThemeData _createSnackBarTheme({
ShapeBorder? shape, ShapeBorder? shape,
SnackBarBehavior? behavior, SnackBarBehavior? behavior,
Color? actionBackgroundColor, Color? actionBackgroundColor,
Color? disabledActionBackgroundColor Color? disabledActionBackgroundColor,
DismissDirection? dismissDirection
}) { }) {
return SnackBarThemeData( return SnackBarThemeData(
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
...@@ -629,7 +632,8 @@ SnackBarThemeData _createSnackBarTheme({ ...@@ -629,7 +632,8 @@ SnackBarThemeData _createSnackBarTheme({
shape: shape, shape: shape,
behavior: behavior, behavior: behavior,
actionBackgroundColor: actionBackgroundColor, actionBackgroundColor: actionBackgroundColor,
disabledActionBackgroundColor: disabledActionBackgroundColor disabledActionBackgroundColor: disabledActionBackgroundColor,
dismissDirection: dismissDirection
); );
} }
......
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