diff --git a/packages/flutter/lib/src/widgets/tap_region.dart b/packages/flutter/lib/src/widgets/tap_region.dart index 1b4f4e9af0..2436493422 100644 --- a/packages/flutter/lib/src/widgets/tap_region.dart +++ b/packages/flutter/lib/src/widgets/tap_region.dart @@ -15,6 +15,7 @@ import 'package:flutter/rendering.dart'; import 'editable_text.dart'; import 'framework.dart'; +import 'routes.dart'; // Enable if you want verbose logging about tap region changes. const bool _kDebugTapRegion = false; @@ -329,6 +330,9 @@ class _DummyTapRecognizer extends GestureArenaMember { /// regions in the group will act as one. /// /// If there is no [TapRegionSurface] ancestor, [TapRegion] will do nothing. +/// +/// [TapRegion] is aware of the [Route]s in the [Navigator], so that [onTapOutside] +/// isn't called after the user navigates to a different page. class TapRegion extends SingleChildRenderObjectWidget { /// Creates a const [TapRegion]. /// @@ -403,12 +407,14 @@ class TapRegion extends SingleChildRenderObjectWidget { @override RenderObject createRenderObject(BuildContext context) { + final bool isCurrent = ModalRoute.isCurrentOf(context) ?? true; + return RenderTapRegion( registry: TapRegionRegistry.maybeOf(context), enabled: enabled, consumeOutsideTaps: consumeOutsideTaps, behavior: behavior, - onTapOutside: onTapOutside, + onTapOutside: isCurrent ? onTapOutside : null, onTapInside: onTapInside, groupId: groupId, debugLabel: debugLabel, @@ -417,12 +423,14 @@ class TapRegion extends SingleChildRenderObjectWidget { @override void updateRenderObject(BuildContext context, covariant RenderTapRegion renderObject) { + final bool isCurrent = ModalRoute.isCurrentOf(context) ?? true; + renderObject ..registry = TapRegionRegistry.maybeOf(context) ..enabled = enabled ..behavior = behavior ..groupId = groupId - ..onTapOutside = onTapOutside + ..onTapOutside = isCurrent ? onTapOutside : null ..onTapInside = onTapInside; if (!kReleaseMode) { renderObject.debugLabel = debugLabel; diff --git a/packages/flutter/test/widgets/tap_region_test.dart b/packages/flutter/test/widgets/tap_region_test.dart index 8dfbd9f93b..8b6eda98af 100644 --- a/packages/flutter/test/widgets/tap_region_test.dart +++ b/packages/flutter/test/widgets/tap_region_test.dart @@ -1053,4 +1053,166 @@ void main() { await click(find.text('Outside Surface')); expect(tappedInside, isEmpty); }); + + // Regression test for https://github.com/flutter/flutter/issues/153093. + testWidgets('TapRegion onTapOutside should only trigger on the current route during navigation', (WidgetTester tester) async { + const ValueKey tapRegion1Key = ValueKey('TapRegion'); + const ValueKey tapRegion2Key = ValueKey('TapRegion2'); + + int count1 = 0; + int count2 = 0; + + final TapRegion tapRegion1 = TapRegion( + key: tapRegion1Key, + onTapOutside: (PointerEvent event) { + count1 += 1; + }, + behavior: HitTestBehavior.opaque, + child: const SizedBox.square(dimension: 100), + ); + + final TapRegion tapRegion2 = TapRegion( + key: tapRegion2Key, + onTapOutside: (PointerEvent event) { + count2 += 1; + }, + behavior: HitTestBehavior.opaque, + child: const SizedBox.square(dimension: 100), + ); + + Future tapOutside(WidgetTester tester, Finder regionFinder) async { + // Find the RenderBox of the region. + final RenderBox renderBox = tester.firstRenderObject(find.byType(Scaffold).last); + final Offset outsidePoint = renderBox.localToGlobal(Offset.zero) + const Offset(200, 200); + + await tester.tapAt(outsidePoint); + await tester.pump(); + } + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center(child: tapRegion1), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + tester.element(find.byType(FloatingActionButton)), + MaterialPageRoute( + builder: (BuildContext context) => Scaffold( + body: Center(child: tapRegion2), + ), + ), + ); + }, + child: const Icon(Icons.add), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Tap outside the first TapRegion to trigger onTapOutside. + await tapOutside(tester, find.byKey(tapRegion1Key)); + expect(count1, 1); + expect(count2, 0); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + // Tap outside the second TapRegion to trigger onTapOutside + await tapOutside(tester, find.byKey(tapRegion2Key)); + expect(count1, 2); // When the Fab is pressed, the first TapRegion is still active. + expect(count2, 1); + + // Back to the first page. + Navigator.pop(tester.element(find.byType(Scaffold).last)); + await tester.pumpAndSettle(); + + // Tap outside the first TapRegion to trigger onTapOutside + await tapOutside(tester, find.byKey(tapRegion1Key)); + expect(count1, 3); + expect(count2, 1); + }); + + // Regression test for https://github.com/flutter/flutter/issues/153093. + testWidgets('TapRegion on non-current routes should not respond to onTapOutside events', (WidgetTester tester) async { + const ValueKey tapRegion1Key = ValueKey('TapRegion1'); + const ValueKey tapRegion2Key = ValueKey('TapRegion2'); + + int count1 = 0; + int count2 = 0; + + final TapRegion tapRegion1 = TapRegion( + key: tapRegion1Key, + onTapOutside: (PointerEvent event) { + count1 += 1; + }, + behavior: HitTestBehavior.opaque, + child: const SizedBox.square(dimension: 100), + ); + + final TapRegion tapRegion2 = TapRegion( + key: tapRegion2Key, + onTapOutside: (PointerEvent event) { + count2 += 1; + }, + behavior: HitTestBehavior.opaque, + child: const SizedBox.square(dimension: 100), + ); + + Future tapOutside(WidgetTester tester, Finder regionFinder) async { + // Find the RenderBox of the region. + final RenderBox renderBox = tester.firstRenderObject(find.byType(Scaffold).last); + final Offset outsidePoint = renderBox.localToGlobal(Offset.zero) + const Offset(200, 200); + + await tester.tapAt(outsidePoint); + await tester.pump(); + } + + await tester.pumpWidget( + MaterialApp( + initialRoute: '/', + routes: { + '/': (BuildContext context) => Scaffold( + body: Center(child: tapRegion1), + ), + '/second': (BuildContext context) => Scaffold( + body: Center(child: tapRegion2), + ), + }, + onGenerateInitialRoutes: (String initialRouteName) { + return >[ + MaterialPageRoute( + builder: (BuildContext context) => Scaffold( + body: Center(child: tapRegion1), + ), + ), + MaterialPageRoute( + builder: (BuildContext context) => Scaffold( + body: Center(child: tapRegion2), + ), + ), + ]; + }, + ), + ); + + await tester.pumpAndSettle(); + + // At this point, tapRegion2 is on top of tapRegion1. + // Tap outside tapRegion2. + await tapOutside(tester, find.byKey(tapRegion2Key)); + expect(count1, 0); // tapRegion1 should not respond. + expect(count2, 1); // tapRegion2 should respond. + + // Now pop the top route to reveal tapRegion1. + Navigator.pop(tester.element(find.byType(Scaffold).last)); + await tester.pumpAndSettle(); + + // Tap outside tapRegion1. + await tapOutside(tester, find.byKey(tapRegion1Key)); + expect(count1, 1); // tapRegion1 should respond. + expect(count2, 1); // tapRegion2 should not respond anymore. + }); }