Unverified Commit 4c0b0be2 authored by Will Lockwood's avatar Will Lockwood Committed by GitHub

Add ability for `ModalRoutes` to ignore pointers during transitions and do so...

Add ability for `ModalRoutes` to ignore pointers during transitions and do so on `Cupertino` routes (#95757)
parent 0052566c
...@@ -235,6 +235,9 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> { ...@@ -235,6 +235,9 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
return result; return result;
} }
@override
bool get ignorePointerDuringTransitions => true;
// Called by _CupertinoBackGestureDetector when a pop ("back") drag start // Called by _CupertinoBackGestureDetector when a pop ("back") drag start
// gesture is detected. The returned controller handles all of the subsequent // gesture is detected. The returned controller handles all of the subsequent
// drag events. // drag events.
...@@ -1049,6 +1052,9 @@ class CupertinoModalPopupRoute<T> extends PopupRoute<T> { ...@@ -1049,6 +1052,9 @@ class CupertinoModalPopupRoute<T> extends PopupRoute<T> {
@override @override
Duration get transitionDuration => _kModalPopupTransitionDuration; Duration get transitionDuration => _kModalPopupTransitionDuration;
@override
bool get ignorePointerDuringTransitions => true;
Animation<double>? _animation; Animation<double>? _animation;
late Tween<Offset> _offsetTween; late Tween<Offset> _offsetTween;
...@@ -1349,4 +1355,7 @@ class CupertinoDialogRoute<T> extends RawDialogRoute<T> { ...@@ -1349,4 +1355,7 @@ class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel, barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context), barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
); );
@override
bool get ignorePointerDuringTransitions => true;
} }
...@@ -293,7 +293,8 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -293,7 +293,8 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
final VoidCallback? previousTrainHoppingListenerRemover = _trainHoppingListenerRemover; final VoidCallback? previousTrainHoppingListenerRemover = _trainHoppingListenerRemover;
_trainHoppingListenerRemover = null; _trainHoppingListenerRemover = null;
if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) { if (nextRoute is TransitionRoute<dynamic>) {
if (canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
final Animation<double>? current = _secondaryAnimation.parent; final Animation<double>? current = _secondaryAnimation.parent;
if (current != null) { if (current != null) {
final Animation<double> currentTrain = (current is TrainHoppingAnimation ? current.currentTrain : current)!; final Animation<double> currentTrain = (current is TrainHoppingAnimation ? current.currentTrain : current)!;
...@@ -356,10 +357,21 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -356,10 +357,21 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
); );
_setSecondaryAnimation(newAnimation, nextRoute.completed); _setSecondaryAnimation(newAnimation, nextRoute.completed);
} }
} else { } else { // This route has no secondary animation.
_setSecondaryAnimation(nextRoute._animation, nextRoute.completed); _setSecondaryAnimation(nextRoute._animation, nextRoute.completed);
} }
} else { } else {
// This route cannot coordinate transitions with nextRoute, so it should
// have no visible secondary animation. By using an AnimationMin, the
// animation's value will always be zero, but it will have nextRoute.animation's
// status until it finishes, allowing this route to wait until all visible
// transitions are complete to stop ignoring pointers.
_setSecondaryAnimation(
AnimationMin<double>(kAlwaysDismissedAnimation, nextRoute._animation!),
nextRoute.completed,
);
}
} else { // The next route is not a TransitionRoute.
_setSecondaryAnimation(kAlwaysDismissedAnimation); _setSecondaryAnimation(kAlwaysDismissedAnimation);
} }
// Finally, we dispose any previous train hopping animation because it // Finally, we dispose any previous train hopping animation because it
...@@ -396,9 +408,9 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -396,9 +408,9 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// the [nextRoute] is popped off of this route, the /// the [nextRoute] is popped off of this route, the
/// `secondaryAnimation` will run from 1.0 - 0.0. /// `secondaryAnimation` will run from 1.0 - 0.0.
/// ///
/// If false, this route's [ModalRoute.buildTransitions] `secondaryAnimation` parameter /// If false, this route's [ModalRoute.buildTransitions] `secondaryAnimation`
/// value will be [kAlwaysDismissedAnimation]. In other words, this route /// will proxy an animation with a constant value of 0. In other words, this
/// will not animate when [nextRoute] is pushed on top of it or when /// route will not animate when [nextRoute] is pushed on top of it or when
/// [nextRoute] is popped off of it. /// [nextRoute] is popped off of it.
/// ///
/// Returns true by default. /// Returns true by default.
...@@ -846,17 +858,19 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -846,17 +858,19 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
context, context,
widget.route.animation!, widget.route.animation!,
widget.route.secondaryAnimation!, widget.route.secondaryAnimation!,
// This additional AnimatedBuilder is include because if the // _listenable updates when this route's animations change
// value of the userGestureInProgressNotifier changes, it's // values, but the _ignorePointerNotifier can also update
// only necessary to rebuild the IgnorePointer widget and set // when the status of animations on popping routes change,
// the focus node's ability to focus. // even when this route's animations' values don't. Also,
// when the value of the _ignorePointerNotifier changes,
// it's only necessary to rebuild the IgnorePointer
// widget and set the focus node's ability to focus.
AnimatedBuilder( AnimatedBuilder(
animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier<bool>(false), animation: widget.route._ignorePointerNotifier,
builder: (BuildContext context, Widget? child) { builder: (BuildContext context, Widget? child) {
final bool ignoreEvents = _shouldIgnoreFocusRequest; focusScopeNode.canRequestFocus = !_shouldIgnoreFocusRequest;
focusScopeNode.canRequestFocus = !ignoreEvents;
return IgnorePointer( return IgnorePointer(
ignoring: ignoreEvents, ignoring: widget.route._ignorePointer,
child: child, child: child,
); );
}, },
...@@ -1140,11 +1154,36 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1140,11 +1154,36 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return child; return child;
} }
/// Whether this route should ignore pointers when transitions are in progress.
///
/// Pointers always are ignored when [isCurrent] is false (e.g., when a route
/// has a new route pushed on top of it, or during a route's exit transition
/// after popping). Override this value to also ignore pointers on pages during
/// transitions where this route is the current route (e.g., after the route
/// above this route pops, or during this route's entrance transition).
///
/// Returns false by default.
///
/// See also:
///
/// * [CupertinoRouteTransitionMixin], [CupertinoModalPopupRoute], and
/// [CupertinoDialogRoute], which use this property to specify that
/// Cupertino routes ignore pointers during transitions.
@protected
bool get ignorePointerDuringTransitions => false;
@override @override
void install() { void install() {
super.install(); super.install();
_animationProxy = ProxyAnimation(super.animation); _animationProxy = ProxyAnimation(super.animation)
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation); ..addStatusListener(_handleAnimationStatusChanged);
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation)
..addStatusListener(_handleAnimationStatusChanged);
navigator!.userGestureInProgressNotifier.addListener(_maybeUpdateIgnorePointer);
}
void _handleAnimationStatusChanged(AnimationStatus status) {
_maybeUpdateIgnorePointer();
} }
@override @override
...@@ -1380,6 +1419,19 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1380,6 +1419,19 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
Animation<double>? get secondaryAnimation => _secondaryAnimationProxy; Animation<double>? get secondaryAnimation => _secondaryAnimationProxy;
ProxyAnimation? _secondaryAnimationProxy; ProxyAnimation? _secondaryAnimationProxy;
bool get _ignorePointer => _ignorePointerNotifier.value;
final ValueNotifier<bool> _ignorePointerNotifier = ValueNotifier<bool>(false);
void _maybeUpdateIgnorePointer() {
bool isTransitioning(Animation<double>? animation) {
return animation?.status == AnimationStatus.forward || animation?.status == AnimationStatus.reverse;
}
_ignorePointerNotifier.value = !isCurrent ||
(navigator?.userGestureInProgress ?? false) ||
(ignorePointerDuringTransitions &&
(isTransitioning(animation) || isTransitioning(secondaryAnimation)));
}
final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[]; final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];
/// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with /// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with
...@@ -1598,9 +1650,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1598,9 +1650,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
child: barrier, child: barrier,
); );
} }
barrier = IgnorePointer( barrier = AnimatedBuilder(
ignoring: animation!.status == AnimationStatus.reverse || // changedInternalState is called when animation.status updates animation: _ignorePointerNotifier,
animation!.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture builder: (BuildContext context, Widget? child) {
return IgnorePointer(
ignoring: _ignorePointer,
child: child,
);
},
child: barrier, child: barrier,
); );
if (semanticsDismissible && barrierDismissible) { if (semanticsDismissible && barrierDismissible) {
......
...@@ -29,13 +29,13 @@ void main() { ...@@ -29,13 +29,13 @@ void main() {
); );
await tester.tap(find.text('Go')); await tester.tap(find.text('Go'));
await tester.pump(); await tester.pumpAndSettle();
expect(find.text('Action Sheet'), findsOneWidget); expect(find.byType(CupertinoActionSheet), findsOneWidget);
await tester.tapAt(const Offset(20.0, 20.0)); await tester.tap(find.byType(ModalBarrier).last);
await tester.pump(); await tester.pumpAndSettle();
expect(find.text('Action Sheet'), findsNothing); expect(find.byType(CupertinoActionSheet), findsNothing);
}); });
testWidgets('Verify that a tap on title section (not buttons) does not dismiss an action sheet', (WidgetTester tester) async { testWidgets('Verify that a tap on title section (not buttons) does not dismiss an action sheet', (WidgetTester tester) async {
...@@ -867,7 +867,7 @@ void main() { ...@@ -867,7 +867,7 @@ void main() {
expect(find.byType(CupertinoActionSheet), findsNothing); expect(find.byType(CupertinoActionSheet), findsNothing);
}); });
testWidgets('Modal barrier is pressed during transition', (WidgetTester tester) async { testWidgets('Modal barrier cannot be dismissed during transition', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet( createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet( CupertinoActionSheet(
...@@ -906,21 +906,20 @@ void main() { ...@@ -906,21 +906,20 @@ void main() {
await tester.pump(const Duration(milliseconds: 60)); await tester.pump(const Duration(milliseconds: 60));
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(337.1, epsilon: 0.1)); expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(337.1, epsilon: 0.1));
// Exit animation // Attempt to dismiss
await tester.tapAt(const Offset(20.0, 20.0)); await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump(const Duration(milliseconds: 60)); await tester.pump(const Duration(milliseconds: 60));
await tester.pump(const Duration(milliseconds: 60)); // Enter animation is continuing
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(374.3, epsilon: 0.1)); expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(325.4, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60)); await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, moreOrLessEquals(470.0, epsilon: 0.1));
await tester.pump(const Duration(milliseconds: 60)); // Attempt to dismiss again
expect(tester.getTopLeft(find.byType(CupertinoActionSheet)).dy, 600.0); await tester.tapAt(const Offset(20.0, 20.0));
await tester.pumpAndSettle();
// Action sheet has disappeared // Action sheet has disappeared
await tester.pump(const Duration(milliseconds: 60));
expect(find.byType(CupertinoActionSheet), findsNothing); expect(find.byType(CupertinoActionSheet), findsNothing);
}); });
...@@ -952,7 +951,7 @@ void main() { ...@@ -952,7 +951,7 @@ void main() {
); );
await tester.tap(find.text('Go')); await tester.tap(find.text('Go'));
await tester.pump(); await tester.pumpAndSettle();
expect( expect(
semantics, semantics,
......
...@@ -1074,6 +1074,8 @@ void main() { ...@@ -1074,6 +1074,8 @@ void main() {
transition = tester.firstWidget(fadeTransitionFinder); transition = tester.firstWidget(fadeTransitionFinder);
expect(transition.opacity.value, moreOrLessEquals(1.0, epsilon: 0.001)); expect(transition.opacity.value, moreOrLessEquals(1.0, epsilon: 0.001));
await tester.pumpAndSettle();
await tester.tap(find.text('Delete')); await tester.tap(find.text('Delete'));
// Exit animation, look at reverse FadeTransition. // Exit animation, look at reverse FadeTransition.
......
...@@ -442,6 +442,8 @@ void main() { ...@@ -442,6 +442,8 @@ void main() {
await tester.pump(const Duration(milliseconds: 40)); await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dy, moreOrLessEquals(0.0, epsilon: 0.1)); expect(tester.getTopLeft(find.byType(Placeholder)).dy, moreOrLessEquals(0.0, epsilon: 0.1));
await tester.pumpAndSettle();
// Exit animation // Exit animation
await tester.tap(find.text('Close')); await tester.tap(find.text('Close'));
await tester.pump(); await tester.pump();
...@@ -547,6 +549,8 @@ void main() { ...@@ -547,6 +549,8 @@ void main() {
await tester.pump(const Duration(milliseconds: 40)); await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, moreOrLessEquals(-267.0, epsilon: 1.0)); expect(tester.getTopLeft(find.byType(Placeholder)).dx, moreOrLessEquals(-267.0, epsilon: 1.0));
await tester.pumpAndSettle();
// Exit animation // Exit animation
await tester.tap(find.text('Close')); await tester.tap(find.text('Close'));
await tester.pump(); await tester.pump();
...@@ -636,6 +640,8 @@ void main() { ...@@ -636,6 +640,8 @@ void main() {
await tester.pump(const Duration(milliseconds: 40)); await tester.pump(const Duration(milliseconds: 40));
expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0); expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0);
await tester.pumpAndSettle();
// Exit animation // Exit animation
await tester.tap(find.text('Close')); await tester.tap(find.text('Close'));
await tester.pump(); await tester.pump();
...@@ -655,6 +661,221 @@ void main() { ...@@ -655,6 +661,221 @@ void main() {
await testNoParallax(tester, fromFullscreenDialog: true); await testNoParallax(tester, fromFullscreenDialog: true);
}); });
group('Route interactivity during transition animations', () {
testWidgets('CupertinoPageRoute ignores pointers when route on top of it pops', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
bool homeTapped = false;
await tester.pumpWidget(
CupertinoApp(
navigatorKey: navigatorKey,
home: TextButton(
onPressed: () => homeTapped = true,
child: const Text('Home'),
),
),
);
navigatorKey.currentState!.push<void>(
CupertinoPageRoute<void>(
builder: (_) => const Text('Page 2'),
)
);
await tester.pumpAndSettle();
expect(find.text('Page 2'), findsOneWidget);
navigatorKey.currentState!.pop();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('Page 2'), findsOneWidget); // Transition still in progress
await tester.tap(find.text('Home'), warnIfMissed: false); // Home route is not tappable
expect(homeTapped, false);
await tester.pumpAndSettle(); // Transition completes
await tester.tap(find.text('Home'));
expect(homeTapped, true);
});
testWidgets('fullscreenDialog CupertinoPageRoute ignores pointers when route on top of it pops', (WidgetTester tester) async {
bool homeTapped = false;
await tester.pumpWidget(
CupertinoApp(
home: TextButton(
onPressed: () => homeTapped = true,
child: const Text('Home'),
),
),
);
tester.state<NavigatorState>(find.byType(Navigator)).push<void>(
CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (_) => const Text('Page 2'),
)
);
await tester.pumpAndSettle();
expect(find.text('Page 2'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('Page 2'), findsOneWidget); // Transition still in progress
await tester.tap(find.text('Home'), warnIfMissed: false); // Home route is not tappable
expect(homeTapped, false);
await tester.pumpAndSettle(); // Transition completes
await tester.tap(find.text('Home'));
expect(homeTapped, true);
});
testWidgets('CupertinoPageRoute ignores pointers when user pop gesture is in progress', (WidgetTester tester) async {
bool homeTapped = false;
await tester.pumpWidget(
CupertinoApp(
home: TextButton(
onPressed: () => homeTapped = true,
child: const Text('Page 1'),
),
),
);
tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>(
builder: (_) => const Text('Page 2'),
),
);
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
final TestGesture swipeGesture = await tester.startGesture(const Offset(5, 100));
await swipeGesture.moveBy(const Offset(100, 0));
await tester.pump();
expect(find.text('Page 1'), findsOneWidget);
expect(tester.state<NavigatorState>(find.byType(Navigator)).userGestureInProgress, true);
await tester.tap(find.text('Page 1'), warnIfMissed: false);
expect(homeTapped, false);
});
testWidgets('CupertinoPageRoute ignores pointers when it is pushed on top of other route', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
onGenerateRoute: (_) => CupertinoPageRoute<void>(
builder: (_) => const Text('Home'),
),
),
);
await tester.tap(find.text('Home'));
tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>(
builder: (_) => const Text('Page 2'),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('Home'), findsOneWidget); // Transition still in progress
// Can't test directly for taps because route is interactive but offstage
// One ignore pointer for each of two overlay entries (ModalScope, ModalBarrier) on each of two routes
expect(find.byType(IgnorePointer, skipOffstage: false), findsNWidgets(4));
final List<Element> ignorePointers = find.byType(IgnorePointer, skipOffstage: false).evaluate().toList();
expect((ignorePointers.first.widget as IgnorePointer).ignoring, true); // Home modalBarrier
expect((ignorePointers[1].widget as IgnorePointer).ignoring, true); // Home modalScope
expect((ignorePointers[2].widget as IgnorePointer).ignoring, true); // Page 2 modalBarrier
expect((ignorePointers.last.widget as IgnorePointer).ignoring, true); // Page 2 modalScope
});
testWidgets('showCupertinoDialog ignores pointers until transition completes', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Builder(
builder: (BuildContext context) {
return TextButton(
onPressed: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext innerContext) => TextButton(
onPressed: Navigator.of(innerContext).pop,
child: const Text('dialog'),
),
);
},
child: const Text('Show Dialog'),
);
},
),
),
);
// Open the dialog.
await tester.tap(find.byType(TextButton));
await tester.pump(const Duration(milliseconds: 100));
// Trigger pop while the transition is in progress
await tester.tap(find.text('dialog'), warnIfMissed: false);
await tester.pumpAndSettle();
// Transition is over and the dialog has not been dismissed
expect(find.text('dialog'), findsOneWidget);
await tester.tap(find.text('dialog'));
await tester.pumpAndSettle();
// The dialog has not been dismissed
expect(find.text('dialog'), findsNothing);
});
testWidgets('showCupertinoModalPopup ignores pointers until transition completes', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Builder(
builder: (BuildContext context) {
return TextButton(
onPressed: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext innerContext) => TextButton(
onPressed: Navigator.of(innerContext).pop,
child: const Text('modal'),
),
);
},
child: const Text('Show modal'),
);
},
),
),
);
// Open the modal popup
await tester.tap(find.byType(TextButton));
await tester.pump(const Duration(milliseconds: 100));
// Trigger pop while the transition is in progress
await tester.tap(find.text('modal'), warnIfMissed: false);
await tester.pumpAndSettle();
// Transition is over and the dialog has not been dismissed
expect(find.text('modal'), findsOneWidget);
await tester.tap(find.text('modal'));
await tester.pumpAndSettle();
// The dialog has not been dismissed
expect(find.text('modal'), findsNothing);
});
});
testWidgets('Animated push/pop is not linear', (WidgetTester tester) async { testWidgets('Animated push/pop is not linear', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const CupertinoApp( const CupertinoApp(
......
...@@ -432,6 +432,7 @@ void main() { ...@@ -432,6 +432,7 @@ void main() {
routes: routes, routes: routes,
), ),
); );
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
await tester.tap(find.text('PUSH')); await tester.tap(find.text('PUSH'));
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2); expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
expect(find.text('PUSH'), findsNothing); expect(find.text('PUSH'), findsNothing);
...@@ -790,8 +791,8 @@ void main() { ...@@ -790,8 +791,8 @@ void main() {
// Tapping the "page" route's back button doesn't do anything either. // Tapping the "page" route's back button doesn't do anything either.
await tester.tap(find.byTooltip('Back'), warnIfMissed: false); await tester.tap(find.byTooltip('Back'), warnIfMissed: false);
await tester.pumpAndSettle(); await tester.pump();
expect(tester.getTopLeft(find.byKey(pageScaffoldKey)), const Offset(400, 0)); expect(tester.getTopLeft(find.byKey(pageScaffoldKey, skipOffstage: false)), const Offset(400, 0));
expect(tester.getTopLeft(find.byKey(homeScaffoldKey)).dx, lessThan(0)); expect(tester.getTopLeft(find.byKey(homeScaffoldKey)).dx, lessThan(0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
......
...@@ -940,6 +940,36 @@ void main() { ...@@ -940,6 +940,36 @@ void main() {
expect(trainHopper2.currentTrain, isNull); // Has been disposed. expect(trainHopper2.currentTrain, isNull); // Has been disposed.
}); });
testWidgets('secondary animation is AnimationMin when transition route that cannot be transitioned to or from pops', (WidgetTester tester) async {
final PageRoute<void> pageRouteOne = MaterialPageRoute<void>(
builder: (_) => const Text('Page One'),
);
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (_) => pageRouteOne,
),
);
final PageRoute<void> pageRouteTwo = MaterialPageRoute<void>(
fullscreenDialog: true,
builder: (_) => const Text('Page Two'),
);
tester.state<NavigatorState>(find.byType(Navigator)).push(pageRouteTwo);
await tester.pumpAndSettle();
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
expect(
((pageRouteOne.secondaryAnimation! as ProxyAnimation).parent! as ProxyAnimation).parent,
isA<AnimationMin<double>>()
..having((AnimationMin<double> a) => a.first, 'first', equals(kAlwaysDismissedAnimation))
..having((AnimationMin<double> a) => a.next, 'first', equals(pageRouteTwo.animation)),
);
});
testWidgets('secondary animation is triggered when pop initial route', (WidgetTester tester) async { testWidgets('secondary animation is triggered when pop initial route', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
late Animation<double> secondaryAnimationOfRouteOne; late Animation<double> secondaryAnimationOfRouteOne;
...@@ -1011,6 +1041,41 @@ void main() { ...@@ -1011,6 +1041,41 @@ void main() {
expect(find.byType(ModalBarrier), findsNWidgets(1)); expect(find.byType(ModalBarrier), findsNWidgets(1));
}); });
testWidgets('showGeneralDialog ModalBarrier does not ignore pointers during transitions', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (BuildContext context) {
return TextButton(
onPressed: () {
showGeneralDialog<void>(
context: context,
transitionDuration: const Duration(milliseconds: 400),
pageBuilder: (BuildContext innerContext, __, ___) => TextButton(
onPressed: Navigator.of(innerContext).pop,
child: const Text('dialog'),
),
);
},
child: const Text('Show Dialog'),
);
},
),
),
);
// Open the dialog.
await tester.tap(find.byType(TextButton));
await tester.pump(const Duration(milliseconds: 200));
// Trigger pop while the transition is in progress
await tester.tap(find.text('dialog'));
await tester.pumpAndSettle();
// The dialog has been dismissed mid-transition
expect(find.text('dialog'), findsNothing);
});
testWidgets('showGeneralDialog adds non-dismissible barrier when barrierDismissible is false', (WidgetTester tester) async { testWidgets('showGeneralDialog adds non-dismissible barrier when barrierDismissible is false', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: Builder( home: Builder(
...@@ -1306,6 +1371,67 @@ void main() { ...@@ -1306,6 +1371,67 @@ void main() {
}); });
}); });
testWidgets('does not ignore pointers when route on top of it pops', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
// Use a transitions builder that will keep the underlying content
// partially visible during a transition
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
},
)
),
home: const Text('Home'),
),
);
tester.state<NavigatorState>(find.byType(Navigator)).push<void>(
MaterialPageRoute<void>(builder: (_) => const Text('Page 2'))
);
await tester.pumpAndSettle();
expect(find.text('Page 2'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('Page 2'), findsOneWidget); // Transition still in progress
await tester.tap(find.text('Home')); // Home route is tappable
});
testWidgets('does not ignore pointers during its own entrance animation', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (_) => MaterialPageRoute<void>(
builder: (_) => const Text('Home'),
),
),
);
await tester.tap(find.text('Home'));
tester.state<NavigatorState>(find.byType(Navigator)).push(
MaterialPageRoute<void>(
builder: (_) => const Text('Page 2'),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('Home'), findsOneWidget); // Transition still in progress
// Can't test directly for taps because route is interactive but offstage
// One ignore pointer for each of two overlay entries (ModalScope, ModalBarrier) on each of two routes
expect(find.byType(IgnorePointer, skipOffstage: false), findsNWidgets(4));
final List<Element> ignorePointers = find.byType(IgnorePointer, skipOffstage: false).evaluate().toList();
expect((ignorePointers.first.widget as IgnorePointer).ignoring, true); // Home modalBarrier
expect((ignorePointers[1].widget as IgnorePointer).ignoring, true); // Home modalScope
expect((ignorePointers[2].widget as IgnorePointer).ignoring, false); // Page 2 modalBarrier
expect((ignorePointers.last.widget as IgnorePointer).ignoring, false); // Page 2 modalScope
});
testWidgets('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async { testWidgets('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey(); final GlobalKey containerKey = GlobalKey();
......
...@@ -440,7 +440,7 @@ void main() { ...@@ -440,7 +440,7 @@ void main() {
await tester.tap(find.text('Next')); await tester.tap(find.text('Next'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle();
await tester.pageBack(); await tester.pageBack();
await tester.pump(); await tester.pump();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment