Unverified Commit 76b3c675 authored by Casey Rogers's avatar Casey Rogers Committed by GitHub

Allow Developers to Stop Navigator From Requesting Focus (#90097)

parent c9751c92
...@@ -214,7 +214,9 @@ abstract class Route<T> { ...@@ -214,7 +214,9 @@ abstract class Route<T> {
@mustCallSuper @mustCallSuper
TickerFuture didPush() { TickerFuture didPush() {
return TickerFuture.complete()..then<void>((void _) { return TickerFuture.complete()..then<void>((void _) {
navigator?.focusScopeNode.requestFocus(); if (navigator?.widget.requestFocus == true) {
navigator!.focusScopeNode.requestFocus();
}
}); });
} }
...@@ -228,14 +230,15 @@ abstract class Route<T> { ...@@ -228,14 +230,15 @@ abstract class Route<T> {
@protected @protected
@mustCallSuper @mustCallSuper
void didAdd() { void didAdd() {
if (navigator?.widget.requestFocus == true) {
// This TickerFuture serves two purposes. First, we want to make sure // This TickerFuture serves two purposes. First, we want to make sure
// animations triggered by other operations finish before focusing the // that animations triggered by other operations will finish before focusing the
// navigator. Second, navigator.focusScopeNode might acquire more focused // navigator. Second, navigator.focusScopeNode might acquire more focused
// children in Route.install asynchronously. This TickerFuture will wait for // children in Route.install asynchronously. This TickerFuture will wait for
// it to finish first. // it to finish first.
// //
// The later case can be found when subclasses manage their own focus scopes. // The later case can be found when subclasses manage their own focus scopes.
// For example, ModalRoute create a focus scope in its overlay entries. The // For example, ModalRoute creates a focus scope in its overlay entries. The
// focused child can only be attached to navigator after initState which // focused child can only be attached to navigator after initState which
// will be guarded by the asynchronous gap. // will be guarded by the asynchronous gap.
TickerFuture.complete().then<void>((void _) { TickerFuture.complete().then<void>((void _) {
...@@ -253,6 +256,7 @@ abstract class Route<T> { ...@@ -253,6 +256,7 @@ abstract class Route<T> {
navigator?.focusScopeNode.requestFocus(); navigator?.focusScopeNode.requestFocus();
}); });
} }
}
/// Called after [install] when the route replaced another in the navigator. /// Called after [install] when the route replaced another in the navigator.
/// ///
...@@ -1309,6 +1313,7 @@ class Navigator extends StatefulWidget { ...@@ -1309,6 +1313,7 @@ class Navigator extends StatefulWidget {
this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(), this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),
this.reportsRouteUpdateToEngine = false, this.reportsRouteUpdateToEngine = false,
this.observers = const <NavigatorObserver>[], this.observers = const <NavigatorObserver>[],
this.requestFocus = true,
this.restorationScopeId, this.restorationScopeId,
}) : assert(pages != null), }) : assert(pages != null),
assert(onGenerateInitialRoutes != null), assert(onGenerateInitialRoutes != null),
...@@ -1473,6 +1478,12 @@ class Navigator extends StatefulWidget { ...@@ -1473,6 +1478,12 @@ class Navigator extends StatefulWidget {
/// Defaults to false. /// Defaults to false.
final bool reportsRouteUpdateToEngine; final bool reportsRouteUpdateToEngine;
/// Whether or not the navigator and it's new topmost route should request focus
/// when the new route is pushed onto the navigator.
///
/// Defaults to true.
final bool requestFocus;
/// Push a named route onto the navigator that most tightly encloses the given /// Push a named route onto the navigator that most tightly encloses the given
/// context. /// context.
/// ///
......
...@@ -755,7 +755,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -755,7 +755,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
if (widget.route.secondaryAnimation != null) widget.route.secondaryAnimation!, if (widget.route.secondaryAnimation != null) widget.route.secondaryAnimation!,
]; ];
_listenable = Listenable.merge(animations); _listenable = Listenable.merge(animations);
if (widget.route.isCurrent) { if (widget.route.isCurrent && _shouldRequestFocus) {
widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode); widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode);
} }
} }
...@@ -764,7 +764,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -764,7 +764,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
void didUpdateWidget(_ModalScope<T> oldWidget) { void didUpdateWidget(_ModalScope<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
assert(widget.route == oldWidget.route); assert(widget.route == oldWidget.route);
if (widget.route.isCurrent) { if (widget.route.isCurrent && _shouldRequestFocus) {
widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode); widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode);
} }
} }
...@@ -792,10 +792,14 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -792,10 +792,14 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
(widget.route.navigator?.userGestureInProgress ?? false); (widget.route.navigator?.userGestureInProgress ?? false);
} }
bool get _shouldRequestFocus {
return widget.route.navigator!.widget.requestFocus;
}
// This should be called to wrap any changes to route.isCurrent, route.canPop, // This should be called to wrap any changes to route.isCurrent, route.canPop,
// and route.offstage. // and route.offstage.
void _routeSetState(VoidCallback fn) { void _routeSetState(VoidCallback fn) {
if (widget.route.isCurrent && !_shouldIgnoreFocusRequest) { if (widget.route.isCurrent && !_shouldIgnoreFocusRequest && _shouldRequestFocus) {
widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode); widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode);
} }
setState(fn); setState(fn);
...@@ -1143,7 +1147,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1143,7 +1147,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
@override @override
TickerFuture didPush() { TickerFuture didPush() {
if (_scopeKey.currentState != null) { if (_scopeKey.currentState != null && navigator!.widget.requestFocus) {
navigator!.focusScopeNode.setFirstFocus(_scopeKey.currentState!.focusScopeNode); navigator!.focusScopeNode.setFirstFocus(_scopeKey.currentState!.focusScopeNode);
} }
return super.didPush(); return super.didPush();
...@@ -1151,7 +1155,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1151,7 +1155,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
@override @override
void didAdd() { void didAdd() {
if (_scopeKey.currentState != null) { if (_scopeKey.currentState != null && navigator!.widget.requestFocus) {
navigator!.focusScopeNode.setFirstFocus(_scopeKey.currentState!.focusScopeNode); navigator!.focusScopeNode.setFirstFocus(_scopeKey.currentState!.focusScopeNode);
} }
super.didAdd(); super.didAdd();
......
...@@ -3594,6 +3594,148 @@ void main() { ...@@ -3594,6 +3594,148 @@ void main() {
await tester.pumpWidget(Container(child: build(key))); await tester.pumpWidget(Container(child: build(key)));
expect(observer.navigator, tester.state<NavigatorState>(find.byType(Navigator))); expect(observer.navigator, tester.state<NavigatorState>(find.byType(Navigator)));
}); });
testWidgets('Navigator requests focus if requestFocus is true', (WidgetTester tester) async {
final GlobalKey navigatorKey = GlobalKey();
final GlobalKey innerKey = GlobalKey();
final Map<String, Widget> routes = <String, Widget>{
'/': const Text('A'),
'/second': Text('B', key: innerKey),
};
late final NavigatorState navigator = navigatorKey.currentState! as NavigatorState;
final FocusScopeNode focusNode = FocusScopeNode();
await tester.pumpWidget(Column(
children: <Widget>[
FocusScope(node: focusNode, child: Container()),
Expanded(
child: MaterialApp(
home: Navigator(
key: navigatorKey,
onGenerateRoute: (RouteSettings settings) {
return PageRouteBuilder<void>(
settings: settings,
pageBuilder: (BuildContext _, Animation<double> __,
Animation<double> ___) {
return routes[settings.name!]!;
},
);
},
),
),
),
],
));
expect(navigator.widget.requestFocus, true);
expect(find.text('A'), findsOneWidget);
expect(find.text('B', skipOffstage: false), findsNothing);
expect(focusNode.hasFocus, false);
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
navigator.pushNamed('/second');
await tester.pumpAndSettle();
expect(find.text('A', skipOffstage: false), findsOneWidget);
expect(find.text('B'), findsOneWidget);
expect(focusNode.hasFocus, false);
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
navigator.pop();
await tester.pumpAndSettle();
expect(find.text('A'), findsOneWidget);
expect(find.text('B', skipOffstage: false), findsNothing);
// Pop does not take focus.
expect(focusNode.hasFocus, true);
navigator.pushReplacementNamed('/second');
await tester.pumpAndSettle();
expect(find.text('A', skipOffstage: false), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(focusNode.hasFocus, false);
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
ModalRoute.of(innerKey.currentContext!)!.addLocalHistoryEntry(
LocalHistoryEntry(),
);
await tester.pumpAndSettle();
// addLocalHistoryEntry does not take focus.
expect(focusNode.hasFocus, true);
});
testWidgets('Navigator does not request focus if requestFocus is false',
(WidgetTester tester) async {
final GlobalKey navigatorKey = GlobalKey();
final GlobalKey innerKey = GlobalKey();
final Map<String, Widget> routes = <String, Widget>{
'/': const Text('A'),
'/second': Text('B', key: innerKey),
};
late final NavigatorState navigator =
navigatorKey.currentState! as NavigatorState;
final FocusScopeNode focusNode = FocusScopeNode();
await tester.pumpWidget(Column(
children: <Widget>[
FocusScope(node: focusNode, child: Container()),
Expanded(
child: MaterialApp(
home: Navigator(
key: navigatorKey,
onGenerateRoute: (RouteSettings settings) {
return PageRouteBuilder<void>(
settings: settings,
pageBuilder: (BuildContext _, Animation<double> __,
Animation<double> ___) {
return routes[settings.name!]!;
},
);
},
requestFocus: false,
),
),
),
],
));
expect(find.text('A'), findsOneWidget);
expect(find.text('B', skipOffstage: false), findsNothing);
expect(focusNode.hasFocus, false);
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
navigator.pushNamed('/second');
await tester.pumpAndSettle();
expect(find.text('A', skipOffstage: false), findsOneWidget);
expect(find.text('B'), findsOneWidget);
expect(focusNode.hasFocus, true);
navigator.pop();
await tester.pumpAndSettle();
expect(find.text('A'), findsOneWidget);
expect(find.text('B', skipOffstage: false), findsNothing);
expect(focusNode.hasFocus, true);
navigator.pushReplacementNamed('/second');
await tester.pumpAndSettle();
expect(find.text('A', skipOffstage: false), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(focusNode.hasFocus, true);
ModalRoute.of(innerKey.currentContext!)!.addLocalHistoryEntry(
LocalHistoryEntry(),
);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, true);
});
} }
typedef AnnouncementCallBack = void Function(Route<dynamic>?); typedef AnnouncementCallBack = void Function(Route<dynamic>?);
......
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