diff --git a/packages/flutter/lib/src/widgets/modal_barrier.dart b/packages/flutter/lib/src/widgets/modal_barrier.dart index 7e77ef95ae..c70df59de1 100644 --- a/packages/flutter/lib/src/widgets/modal_barrier.dart +++ b/packages/flutter/lib/src/widgets/modal_barrier.dart @@ -34,6 +34,7 @@ class ModalBarrier extends StatelessWidget { Key? key, this.color, this.dismissible = true, + this.onDismiss, this.semanticsLabel, this.barrierSemanticsDismissible = true, }) : super(key: key); @@ -46,7 +47,12 @@ class ModalBarrier extends StatelessWidget { /// [ModalBarrier] built by [ModalRoute] pages. final Color? color; - /// Whether touching the barrier will pop the current route off the [Navigator]. + /// Specifies if the barrier will be dismissed when the user taps on it. + /// + /// If true, and [onDismiss] is non-null, [onDismiss] will be called, + /// otherwise the current route will be popped from the ambient [Navigator]. + /// + /// If false, tapping on the barrier will do nothing. /// /// See also: /// @@ -54,6 +60,16 @@ class ModalBarrier extends StatelessWidget { /// [ModalBarrier] built by [ModalRoute] pages. final bool dismissible; + /// Called when the barrier is being dismissed. + /// + /// If non-null [onDismiss] will be called in place of popping the current + /// route. It is up to the callback to handle dismissing the barrier. + /// + /// If null, the ambient [Navigator]'s current route will be popped. + /// + /// This field is ignored if [dismissible] is false. + final VoidCallback? onDismiss; + /// Whether the modal barrier semantics are included in the semantics tree. /// /// See also: @@ -94,7 +110,15 @@ class ModalBarrier extends StatelessWidget { final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible; void handleDismiss() { - Navigator.maybePop(context); + if (dismissible) { + if (onDismiss != null) { + onDismiss!(); + } else { + Navigator.maybePop(context); + } + } else { + SystemSound.play(SystemSoundType.alert); + } } return BlockSemantics( @@ -103,12 +127,7 @@ class ModalBarrier extends StatelessWidget { // modal barriers are not dismissible in accessibility mode. excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible, child: _ModalBarrierGestureDetector( - onDismiss: () { - if (dismissible) - handleDismiss(); - else - SystemSound.play(SystemSoundType.alert); - }, + onDismiss: handleDismiss, child: Semantics( label: semanticsDismissible ? semanticsLabel : null, onDismiss: semanticsDismissible ? handleDismiss : null, diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart index 0470031997..314fb23289 100644 --- a/packages/flutter/test/widgets/modal_barrier_test.dart +++ b/packages/flutter/test/widgets/modal_barrier_test.dart @@ -365,6 +365,60 @@ void main() { expect(willPopCalled, isTrue); }); + testWidgets('ModalBarrier will call onDismiss callback', (WidgetTester tester) async { + bool dismissCallbackCalled = false; + final Map routes = { + '/': (BuildContext context) => const FirstWidget(), + '/modal': (BuildContext context) => SecondWidget(onDismiss: () { + dismissCallbackCalled = true; + }), + }; + + await tester.pumpWidget(MaterialApp(routes: routes)); + + // Initially the barrier is not visible + expect(find.byKey(const ValueKey('barrier')), findsNothing); + + // Tapping on X routes to the barrier + await tester.tap(find.text('X')); + await tester.pump(); // begin transition + await tester.pump(const Duration(seconds: 1)); // end transition + expect(find.byKey(const ValueKey('barrier')), findsOneWidget); + expect(dismissCallbackCalled, false); + + // Tap on the barrier + await tester.tap(find.byKey(const ValueKey('barrier'))); + await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition + expect(dismissCallbackCalled, true); + }); + + testWidgets('ModalBarrier will not pop when given an onDismiss callback', (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => const FirstWidget(), + '/modal': (BuildContext context) => SecondWidget(onDismiss: () {}), + }; + + await tester.pumpWidget(MaterialApp(routes: routes)); + + // Initially the barrier is not visible + expect(find.byKey(const ValueKey('barrier')), findsNothing); + + // Tapping on X routes to the barrier + await tester.tap(find.text('X')); + await tester.pump(); // begin transition + await tester.pump(const Duration(seconds: 1)); // end transition + expect(find.byKey(const ValueKey('barrier')), findsOneWidget); + + // Tap on the barrier + await tester.tap(find.byKey(const ValueKey('barrier'))); + await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition + expect( + find.byKey(const ValueKey('barrier')), + findsOneWidget, + reason: 'The route should not have been dismissed by tapping the barrier, as there was a onDismiss callback given.', + ); + }); + testWidgets('Undismissible ModalBarrier hidden in semantic tree', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(const ModalBarrier(dismissible: false)); @@ -442,11 +496,15 @@ class FirstWidget extends StatelessWidget { } class SecondWidget extends StatelessWidget { - const SecondWidget({ Key? key }) : super(key: key); + const SecondWidget({ Key? key, this.onDismiss }) : super(key: key); + + final VoidCallback? onDismiss; + @override Widget build(BuildContext context) { - return const ModalBarrier( - key: ValueKey('barrier'), + return ModalBarrier( + key: const ValueKey('barrier'), + onDismiss: onDismiss, ); } }