diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index d2bfceb4079..6607b751d5b 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -722,6 +722,9 @@ Future showModalBottomSheet({ /// persistent bottom sheets (see the documentation for these on [BottomSheet] /// for more details). /// +/// The [enableDrag] parameter specifies whether the bottom sheet can be +/// dragged up and down and dismissed by swiping downwards. +/// /// To rebuild the bottom sheet (e.g. if it is stateful), call /// [PersistentBottomSheetController.setState] on the controller returned by /// this method. @@ -759,6 +762,7 @@ PersistentBottomSheetController showBottomSheet({ ShapeBorder? shape, Clip? clipBehavior, BoxConstraints? constraints, + bool? enableDrag, AnimationController? transitionAnimationController, }) { assert(context != null); @@ -772,6 +776,7 @@ PersistentBottomSheetController showBottomSheet({ shape: shape, clipBehavior: clipBehavior, constraints: constraints, + enableDrag: enableDrag, transitionAnimationController: transitionAnimationController, ); } diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index a9cb134de20..41e753e9189 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -2318,6 +2318,7 @@ class ScaffoldState extends State with TickerProviderStateMixin, Resto ShapeBorder? shape, Clip? clipBehavior, BoxConstraints? constraints, + bool? enableDrag, bool shouldDisposeAnimationController = true, }) { assert(() { @@ -2367,7 +2368,7 @@ class ScaffoldState extends State with TickerProviderStateMixin, Resto bottomSheet = _StandardBottomSheet( key: bottomSheetKey, animationController: animationController, - enableDrag: !isPersistent, + enableDrag: enableDrag ?? !isPersistent, onClosing: () { if (_currentBottomSheet == null) { return; @@ -2468,6 +2469,7 @@ class ScaffoldState extends State with TickerProviderStateMixin, Resto ShapeBorder? shape, Clip? clipBehavior, BoxConstraints? constraints, + bool? enableDrag, AnimationController? transitionAnimationController, }) { assert(() { @@ -2494,6 +2496,7 @@ class ScaffoldState extends State with TickerProviderStateMixin, Resto shape: shape, clipBehavior: clipBehavior, constraints: constraints, + enableDrag: enableDrag, shouldDisposeAnimationController: transitionAnimationController == null, ); }); diff --git a/packages/flutter/test/material/bottom_sheet_test.dart b/packages/flutter/test/material/bottom_sheet_test.dart index 94fb1812c3f..0c8a2b1557d 100644 --- a/packages/flutter/test/material/bottom_sheet_test.dart +++ b/packages/flutter/test/material/bottom_sheet_test.dart @@ -88,6 +88,150 @@ void main() { await tester.pumpWidget(Container()); }); + + testWidgets('Swiping down a BottomSheet should dismiss it by default', (WidgetTester tester) async { + + final GlobalKey scaffoldKey = GlobalKey(); + bool showBottomSheetThenCalled = false; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: scaffoldKey, + body: const Center(child: Text('body')), + ), + )); + + await tester.pump(); + expect(showBottomSheetThenCalled, isFalse); + expect(find.text('BottomSheet'), findsNothing); + + scaffoldKey.currentState!.showBottomSheet((BuildContext context) { + return const SizedBox( + height: 200.0, + child: Text('BottomSheet'), + ); + }).closed.whenComplete(() { + showBottomSheetThenCalled = true; + }); + + await tester.pumpAndSettle(); + expect(showBottomSheetThenCalled, isFalse); + expect(find.text('BottomSheet'), findsOneWidget); + + // Swipe the bottom sheet to dismiss it. + await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); + await tester.pumpAndSettle(); // Bottom sheet dismiss animation. + expect(showBottomSheetThenCalled, isTrue); + expect(find.text('BottomSheet'), findsNothing); + }); + + testWidgets('Swiping down a BottomSheet should not dismiss it when enableDrag is false', (WidgetTester tester) async { + + final GlobalKey scaffoldKey = GlobalKey(); + bool showBottomSheetThenCalled = false; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: scaffoldKey, + body: const Center(child: Text('body')), + ), + )); + + await tester.pump(); + expect(showBottomSheetThenCalled, isFalse); + expect(find.text('BottomSheet'), findsNothing); + + scaffoldKey.currentState!.showBottomSheet((BuildContext context) { + return const SizedBox( + height: 200.0, + child: Text('BottomSheet'), + ); + }, + enableDrag: false + ).closed.whenComplete(() { + showBottomSheetThenCalled = true; + }); + + await tester.pumpAndSettle(); + expect(showBottomSheetThenCalled, isFalse); + expect(find.text('BottomSheet'), findsOneWidget); + + // Swipe the bottom sheet, attempting to dismiss it. + await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); + await tester.pumpAndSettle(); // Bottom sheet should not dismiss. + expect(showBottomSheetThenCalled, isFalse); + expect(find.text('BottomSheet'), findsOneWidget); + }); + + testWidgets('Swiping down a BottomSheet should dismiss it when enableDrag is true', (WidgetTester tester) async { + final GlobalKey scaffoldKey = GlobalKey(); + bool showBottomSheetThenCalled = false; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: scaffoldKey, + body: const Center(child: Text('body')), + ), + )); + + await tester.pump(); + expect(showBottomSheetThenCalled, isFalse); + expect(find.text('BottomSheet'), findsNothing); + + scaffoldKey.currentState!.showBottomSheet((BuildContext context) { + return const SizedBox( + height: 200.0, + child: Text('BottomSheet'), + ); + }, + enableDrag: true + ).closed.whenComplete(() { + showBottomSheetThenCalled = true; + }); + + await tester.pumpAndSettle(); + expect(showBottomSheetThenCalled, isFalse); + expect(find.text('BottomSheet'), findsOneWidget); + + // Swipe the bottom sheet to dismiss it. + await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); + await tester.pumpAndSettle(); // Bottom sheet dismiss animation. + expect(showBottomSheetThenCalled, isTrue); + expect(find.text('BottomSheet'), findsNothing); + }); + + testWidgets('Modal BottomSheet builder should only be called once', (WidgetTester tester) async { + late BuildContext savedContext; + + await tester.pumpWidget(MaterialApp( + home: Builder( + builder: (BuildContext context) { + savedContext = context; + return Container(); + }, + ), + )); + + int numBuilderCalls = 0; + showModalBottomSheet( + context: savedContext, + isDismissible: false, + enableDrag: true, + builder: (BuildContext context) { + numBuilderCalls++; + return const Text('BottomSheet'); + }, + ); + + await tester.pumpAndSettle(); + expect(numBuilderCalls, 1); + + // Swipe the bottom sheet to dismiss it. + await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); + await tester.pumpAndSettle(); // Bottom sheet dismiss animation. + expect(numBuilderCalls, 1); + }); + testWidgets('Tapping on a modal BottomSheet should not dismiss it', (WidgetTester tester) async { late BuildContext savedContext;