From dd43da71febaafc58852000a61a6bc1ac4fb57ef Mon Sep 17 00:00:00 2001 From: Tjong Anthony Date: Tue, 15 Oct 2019 01:00:00 +0800 Subject: [PATCH] Add isDismissible configuration for showModalBottomSheet (#42404) * Allow showModalBottomSheet to present bottom sheet that is not dismissible by tapping on the scrim * Add guards, improve styling and tests for BottomSheet --- .../lib/src/material/bottom_sheet.dart | 11 +- .../test/material/bottom_sheet_test.dart | 104 +++++++++++++++--- 2 files changed, 98 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index 91a41aeb67..6ad37cdc2c 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -343,9 +343,11 @@ class _ModalBottomSheetRoute extends PopupRoute { this.elevation, this.shape, this.clipBehavior, + this.isDismissible = true, @required this.isScrollControlled, RouteSettings settings, }) : assert(isScrollControlled != null), + assert(isDismissible != null), super(settings: settings); final WidgetBuilder builder; @@ -355,12 +357,13 @@ class _ModalBottomSheetRoute extends PopupRoute { final double elevation; final ShapeBorder shape; final Clip clipBehavior; + final bool isDismissible; @override Duration get transitionDuration => _bottomSheetDuration; @override - bool get barrierDismissible => true; + bool get barrierDismissible => isDismissible; @override final String barrierLabel; @@ -428,6 +431,9 @@ class _ModalBottomSheetRoute extends PopupRoute { /// that a modal [BottomSheet] needs to be displayed above all other content /// but the caller is inside another [Navigator]. /// +/// The [isDismissible] parameter specifies whether the bottom sheet will be +/// dismissed when user taps on the scrim. +/// /// The optional [backgroundColor], [elevation], [shape], and [clipBehavior] /// parameters can be passed in to customize the appearance and behavior of /// modal bottom sheets. @@ -453,11 +459,13 @@ Future showModalBottomSheet({ Clip clipBehavior, bool isScrollControlled = false, bool useRootNavigator = false, + bool isDismissible = true, }) { assert(context != null); assert(builder != null); assert(isScrollControlled != null); assert(useRootNavigator != null); + assert(isDismissible != null); assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMaterialLocalizations(context)); @@ -470,6 +478,7 @@ Future showModalBottomSheet({ elevation: elevation, shape: shape, clipBehavior: clipBehavior, + isDismissible: isDismissible, )); } diff --git a/packages/flutter/test/material/bottom_sheet_test.dart b/packages/flutter/test/material/bottom_sheet_test.dart index 34db249f4a..19ab0277c5 100644 --- a/packages/flutter/test/material/bottom_sheet_test.dart +++ b/packages/flutter/test/material/bottom_sheet_test.dart @@ -14,14 +14,16 @@ void main() { testWidgets('Tapping on a modal BottomSheet should not dismiss it', (WidgetTester tester) async { BuildContext savedContext; - await tester.pumpWidget(MaterialApp( - home: Builder( - builder: (BuildContext context) { - savedContext = context; - return Container(); - } + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + savedContext = context; + return Container(); + }, + ), ), - )); + ); await tester.pump(); expect(find.text('BottomSheet'), findsNothing); @@ -45,15 +47,15 @@ void main() { expect(showBottomSheetThenCalled, isFalse); }); - testWidgets('Tapping outside a modal BottomSheet should dismiss it', (WidgetTester tester) async { + testWidgets('Tapping outside a modal BottomSheet should dismiss it by default', (WidgetTester tester) async { BuildContext savedContext; await tester.pumpWidget(MaterialApp( home: Builder( - builder: (BuildContext context) { - savedContext = context; - return Container(); - } + builder: (BuildContext context) { + savedContext = context; + return Container(); + }, ), )); @@ -72,15 +74,85 @@ void main() { expect(find.text('BottomSheet'), findsOneWidget); expect(showBottomSheetThenCalled, isFalse); - // Tap above the bottom sheet to dismiss it + // Tap above the bottom sheet to dismiss it. await tester.tapAt(const Offset(20.0, 20.0)); - await tester.pump(); // bottom sheet dismiss animation starts + await tester.pumpAndSettle(); // Bottom sheet dismiss animation. expect(showBottomSheetThenCalled, isTrue); - await tester.pump(const Duration(seconds: 1)); // animation done - await tester.pump(const Duration(seconds: 1)); // rebuild frame expect(find.text('BottomSheet'), findsNothing); }); + testWidgets('Tapping outside a modal BottomSheet should dismiss it when isDismissible=true', (WidgetTester tester) async { + BuildContext savedContext; + + await tester.pumpWidget(MaterialApp( + home: Builder( + builder: (BuildContext context) { + savedContext = context; + return Container(); + }, + ), + )); + + await tester.pump(); + expect(find.text('BottomSheet'), findsNothing); + + bool showBottomSheetThenCalled = false; + showModalBottomSheet( + context: savedContext, + builder: (BuildContext context) => const Text('BottomSheet'), + isDismissible: true, + ).then((void value) { + showBottomSheetThenCalled = true; + }); + + await tester.pumpAndSettle(); + expect(find.text('BottomSheet'), findsOneWidget); + expect(showBottomSheetThenCalled, isFalse); + + // Tap above the bottom sheet to dismiss it. + await tester.tapAt(const Offset(20.0, 20.0)); + await tester.pumpAndSettle(); // Bottom sheet dismiss animation. + expect(showBottomSheetThenCalled, isTrue); + expect(find.text('BottomSheet'), findsNothing); + }); + + testWidgets('Tapping outside a modal BottomSheet should not dismiss it when isDismissible=false', (WidgetTester tester) async { + BuildContext savedContext; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + savedContext = context; + return Container(); + }, + ), + ), + ); + + await tester.pump(); + expect(find.text('BottomSheet'), findsNothing); + + bool showBottomSheetThenCalled = false; + showModalBottomSheet( + context: savedContext, + builder: (BuildContext context) => const Text('BottomSheet'), + isDismissible: false, + ).then((void value) { + showBottomSheetThenCalled = true; + }); + + await tester.pumpAndSettle(); + expect(find.text('BottomSheet'), findsOneWidget); + expect(showBottomSheetThenCalled, isFalse); + + // Tap above the bottom sheet, attempting to dismiss it. + await tester.tapAt(const Offset(20.0, 20.0)); + await tester.pumpAndSettle(); // Bottom sheet should not dismiss. + expect(showBottomSheetThenCalled, isFalse); + expect(find.text('BottomSheet'), findsOneWidget); + }); + testWidgets('Verify that a downwards fling dismisses a persistent BottomSheet', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); bool showBottomSheetThenCalled = false;