Fixes an issue where onTapOutside was incorrectly triggered across routes in TapRegion (#155297)

Fixes https://github.com/flutter/flutter/issues/153093

This PR fixes the issue of onTapOutside being triggered on other routes incorrectly.
This commit is contained in:
Mairramer 2024-10-07 17:48:00 -03:00 committed by GitHub
parent da71e6e93d
commit 56a33efd73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 172 additions and 2 deletions

View File

@ -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;

View File

@ -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<String> tapRegion1Key = ValueKey<String>('TapRegion');
const ValueKey<String> tapRegion2Key = ValueKey<String>('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<void> 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<void>(
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<String> tapRegion1Key = ValueKey<String>('TapRegion1');
const ValueKey<String> tapRegion2Key = ValueKey<String>('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<void> 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: <String, WidgetBuilder>{
'/': (BuildContext context) => Scaffold(
body: Center(child: tapRegion1),
),
'/second': (BuildContext context) => Scaffold(
body: Center(child: tapRegion2),
),
},
onGenerateInitialRoutes: (String initialRouteName) {
return <Route<void>>[
MaterialPageRoute<void>(
builder: (BuildContext context) => Scaffold(
body: Center(child: tapRegion1),
),
),
MaterialPageRoute<void>(
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.
});
}