diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart index 5dea738bc0..ce3df05095 100644 --- a/packages/flutter/lib/src/material/snack_bar.dart +++ b/packages/flutter/lib/src/material/snack_bar.dart @@ -283,6 +283,7 @@ class SnackBar extends StatefulWidget { this.padding, this.width, this.shape, + this.hitTestBehavior, this.behavior, this.action, this.actionOverflowThreshold, @@ -331,6 +332,8 @@ class SnackBar extends StatefulWidget { /// If this property is null, then [SnackBarThemeData.insetPadding] of /// [ThemeData.snackBarTheme] is used. If that is also null, then the default is /// `EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0)`. + /// + /// If this property is not null and [hitTestBehavior] is null, then [hitTestBehavior] default is [HitTestBehavior.deferToChild]. final EdgeInsetsGeometry? margin; /// The amount of padding to apply to the snack bar's content and optional @@ -384,6 +387,13 @@ class SnackBar extends StatefulWidget { /// circular corner radius of 4.0. final ShapeBorder? shape; + /// Defines how the snack bar area, including margin, will behave during hit testing. + /// + /// If this property is null and [margin] is not null, then [HitTestBehavior.deferToChild] is used by default. + /// + /// Please refer to [HitTestBehavior] for a detailed explanation of every behavior. + final HitTestBehavior? hitTestBehavior; + /// This defines the behavior and location of the snack bar. /// /// Defines where a [SnackBar] should appear within a [Scaffold] and how its @@ -489,6 +499,7 @@ class SnackBar extends StatefulWidget { padding: padding, width: width, shape: shape, + hitTestBehavior: hitTestBehavior, behavior: behavior, action: action, actionOverflowThreshold: actionOverflowThreshold, @@ -776,6 +787,7 @@ class _SnackBarState extends State { key: const Key('dismissible'), direction: widget.dismissDirection, resizeDuration: null, + behavior: widget.hitTestBehavior ?? (widget.margin != null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque), onDismissed: (DismissDirection direction) { ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe); }, diff --git a/packages/flutter/test/material/snack_bar_test.dart b/packages/flutter/test/material/snack_bar_test.dart index a31753412f..0cd5479071 100644 --- a/packages/flutter/test/material/snack_bar_test.dart +++ b/packages/flutter/test/material/snack_bar_test.dart @@ -7,6 +7,7 @@ @Tags(['reduced-test-set']) library; +import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -2915,6 +2916,175 @@ testWidgets('SnackBarAction backgroundColor works as a Color', (WidgetTester tes expect(material.clipBehavior, Clip.antiAlias); }); + + testWidgets('Tap on button behind snack bar defined by width', (WidgetTester tester) async { + tester.view.physicalSize = const Size.square(200); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + const String buttonText = 'Show snackbar'; + const String snackbarContent = 'Snackbar'; + const String buttonText2 = 'Try press me'; + + final Completer completer = Completer(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + behavior: SnackBarBehavior.floating, + width: 100, + content: Text(snackbarContent), + ), + ); + }, + child: const Text(buttonText), + ), + ElevatedButton( + onPressed: () { + completer.complete(); + }, + child: const Text(buttonText2), + ), + ], + ); + }, + ), + ), + )); + + await tester.tap(find.text(buttonText)); + await tester.pumpAndSettle(); + + expect(find.text(snackbarContent), findsOneWidget); + await tester.tapAt(tester.getTopLeft(find.text(buttonText2))); + expect(find.text(snackbarContent), findsOneWidget); + + expect(completer.isCompleted, true); + }); + + + testWidgets('Tap on button behind snack bar defined by margin', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/78537. + tester.view.physicalSize = const Size.square(200); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + const String buttonText = 'Show snackbar'; + const String snackbarContent = 'Snackbar'; + const String buttonText2 = 'Try press me'; + + final Completer completer = Completer(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only(left: 100), + content: Text(snackbarContent), + ), + ); + }, + child: const Text(buttonText), + ), + ElevatedButton( + onPressed: () { + completer.complete(); + }, + child: const Text(buttonText2), + ), + ], + ); + }, + ), + ), + )); + + await tester.tap(find.text(buttonText)); + await tester.pumpAndSettle(); + + expect(find.text(snackbarContent), findsOneWidget); + await tester.tapAt(tester.getTopLeft(find.text(buttonText2))); + expect(find.text(snackbarContent), findsOneWidget); + + expect(completer.isCompleted, true); + }); + + testWidgets("Can't tap on button behind snack bar defined by margin and HitTestBehavior.opaque", (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/78537. + tester.view.physicalSize = const Size.square(200); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + const String buttonText = 'Show snackbar'; + const String snackbarContent = 'Snackbar'; + const String buttonText2 = 'Try press me'; + + final Completer completer = Completer(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + hitTestBehavior: HitTestBehavior.opaque, + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only(left: 100), + content: Text(snackbarContent), + ), + ); + }, + child: const Text(buttonText), + ), + ElevatedButton( + onPressed: () { + completer.complete(); + }, + child: const Text(buttonText2), + ), + ], + ); + }, + ), + ), + )); + + await tester.tap(find.text(buttonText)); + await tester.pumpAndSettle(); + + expect(find.text(snackbarContent), findsOneWidget); + await tester.tapAt(tester.getTopLeft(find.text(buttonText2))); + expect(find.text(snackbarContent), findsOneWidget); + + expect(completer.isCompleted, false); + }); } /// Start test for "SnackBar dismiss test".