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> {
@mustCallSuper
TickerFuture didPush() {
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> {
@protected
@mustCallSuper
void didAdd() {
if (navigator?.widget.requestFocus == true) {
// 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
// children in Route.install asynchronously. This TickerFuture will wait for
// it to finish first.
//
// 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
// will be guarded by the asynchronous gap.
TickerFuture.complete().then<void>((void _) {
......@@ -253,6 +256,7 @@ abstract class Route<T> {
navigator?.focusScopeNode.requestFocus();
});
}
}
/// Called after [install] when the route replaced another in the navigator.
///
......@@ -1309,6 +1313,7 @@ class Navigator extends StatefulWidget {
this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),
this.reportsRouteUpdateToEngine = false,
this.observers = const <NavigatorObserver>[],
this.requestFocus = true,
this.restorationScopeId,
}) : assert(pages != null),
assert(onGenerateInitialRoutes != null),
......@@ -1473,6 +1478,12 @@ class Navigator extends StatefulWidget {
/// Defaults to false.
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
/// context.
///
......
......@@ -755,7 +755,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
if (widget.route.secondaryAnimation != null) widget.route.secondaryAnimation!,
];
_listenable = Listenable.merge(animations);
if (widget.route.isCurrent) {
if (widget.route.isCurrent && _shouldRequestFocus) {
widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode);
}
}
......@@ -764,7 +764,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
void didUpdateWidget(_ModalScope<T> oldWidget) {
super.didUpdateWidget(oldWidget);
assert(widget.route == oldWidget.route);
if (widget.route.isCurrent) {
if (widget.route.isCurrent && _shouldRequestFocus) {
widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode);
}
}
......@@ -792,10 +792,14 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
(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,
// and route.offstage.
void _routeSetState(VoidCallback fn) {
if (widget.route.isCurrent && !_shouldIgnoreFocusRequest) {
if (widget.route.isCurrent && !_shouldIgnoreFocusRequest && _shouldRequestFocus) {
widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode);
}
setState(fn);
......@@ -1143,7 +1147,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
@override
TickerFuture didPush() {
if (_scopeKey.currentState != null) {
if (_scopeKey.currentState != null && navigator!.widget.requestFocus) {
navigator!.focusScopeNode.setFirstFocus(_scopeKey.currentState!.focusScopeNode);
}
return super.didPush();
......@@ -1151,7 +1155,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
@override
void didAdd() {
if (_scopeKey.currentState != null) {
if (_scopeKey.currentState != null && navigator!.widget.requestFocus) {
navigator!.focusScopeNode.setFirstFocus(_scopeKey.currentState!.focusScopeNode);
}
super.didAdd();
......
......@@ -3594,6 +3594,148 @@ void main() {
await tester.pumpWidget(Container(child: build(key)));
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>?);
......
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