Unverified Commit 7775c237 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

[Re-Land] Implement focus traversal for desktop platforms. (#31614)

This re-lands the Focus changes in #30040. Correctness changes in routes.dart, and removes the automatic requesting of focus on reparent when there is no current focus, which caused undesirable selections.

Addresses #11344, #1608, #13264, and #1678
Fixes #30084
Fixes #26704
parent fdae7bb8
...@@ -62,7 +62,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> { ...@@ -62,7 +62,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
if (!_focusNode.hasFocus) { if (!_focusNode.hasFocus) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
FocusScope.of(context).requestFocus(_focusNode); _focusNode.requestFocus();
}, },
child: Text('Tap to focus', style: textTheme.display1), child: Text('Tap to focus', style: textTheme.display1),
); );
......
...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget { ...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: _title, title: _title,
home: Scaffold( home: Scaffold(
appBar: AppBar(title: Text(_title)), appBar: AppBar(title: const Text(_title)),
body: MyStatefulWidget(), body: MyStatefulWidget(),
), ),
); );
......
...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget { ...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: _title, title: _title,
home: Scaffold( home: Scaffold(
appBar: AppBar(title: Text(_title)), appBar: AppBar(title: const Text(_title)),
body: MyStatelessWidget(), body: MyStatelessWidget(),
), ),
); );
......
...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart'; ...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
/// onTap: () { /// onTap: () {
/// FocusScope.of(context).requestFocus(_focusNode); /// FocusScope.of(context).requestFocus(_focusNode);
/// }, /// },
/// child: Text('Tap to focus'), /// child: const Text('Tap to focus'),
/// ); /// );
/// } /// }
/// return Text(_message ?? 'Press a key'); /// return Text(_message ?? 'Press a key');
......
...@@ -304,7 +304,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { ...@@ -304,7 +304,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
tabs = List<Widget>(widget.tabNumber); tabs = List<Widget>(widget.tabNumber);
tabFocusNodes = List<FocusScopeNode>.generate( tabFocusNodes = List<FocusScopeNode>.generate(
widget.tabNumber, widget.tabNumber,
(int index) => FocusScopeNode(), (int index) => FocusScopeNode(debugLabel: 'Tab Focus Scope $index'),
); );
} }
...@@ -327,7 +327,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { ...@@ -327,7 +327,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
@override @override
void dispose() { void dispose() {
for (FocusScopeNode focusScopeNode in tabFocusNodes) { for (FocusScopeNode focusScopeNode in tabFocusNodes) {
focusScopeNode.detach(); focusScopeNode.dispose();
} }
super.dispose(); super.dispose();
} }
......
...@@ -191,7 +191,7 @@ abstract class SearchDelegate<T> { ...@@ -191,7 +191,7 @@ abstract class SearchDelegate<T> {
/// ///
/// * [showSuggestions] to show the search suggestions again. /// * [showSuggestions] to show the search suggestions again.
void showResults(BuildContext context) { void showResults(BuildContext context) {
_focusNode.unfocus(); _focusNode?.unfocus();
_currentBody = _SearchBody.results; _currentBody = _SearchBody.results;
} }
...@@ -208,7 +208,8 @@ abstract class SearchDelegate<T> { ...@@ -208,7 +208,8 @@ abstract class SearchDelegate<T> {
/// ///
/// * [showResults] to show the search results. /// * [showResults] to show the search results.
void showSuggestions(BuildContext context) { void showSuggestions(BuildContext context) {
FocusScope.of(context).requestFocus(_focusNode); assert(_focusNode != null, '_focusNode must be set by route before showSuggestions is called.');
_focusNode.requestFocus();
_currentBody = _SearchBody.suggestions; _currentBody = _SearchBody.suggestions;
} }
...@@ -218,7 +219,7 @@ abstract class SearchDelegate<T> { ...@@ -218,7 +219,7 @@ abstract class SearchDelegate<T> {
/// to [showSearch] that launched the search initially. /// to [showSearch] that launched the search initially.
void close(BuildContext context, T result) { void close(BuildContext context, T result) {
_currentBody = null; _currentBody = null;
_focusNode.unfocus(); _focusNode?.unfocus();
Navigator.of(context) Navigator.of(context)
..popUntil((Route<dynamic> route) => route == _route) ..popUntil((Route<dynamic> route) => route == _route)
..pop(result); ..pop(result);
...@@ -232,7 +233,9 @@ abstract class SearchDelegate<T> { ...@@ -232,7 +233,9 @@ abstract class SearchDelegate<T> {
/// page. /// page.
Animation<double> get transitionAnimation => _proxyAnimation; Animation<double> get transitionAnimation => _proxyAnimation;
final FocusNode _focusNode = FocusNode(); // The focus node to use for manipulating focus on the search page. This is
// managed, owned, and set by the _SearchPageRoute using this delegate.
FocusNode _focusNode;
final TextEditingController _queryTextController = TextEditingController(); final TextEditingController _queryTextController = TextEditingController();
...@@ -246,7 +249,6 @@ abstract class SearchDelegate<T> { ...@@ -246,7 +249,6 @@ abstract class SearchDelegate<T> {
} }
_SearchPageRoute<T> _route; _SearchPageRoute<T> _route;
} }
/// Describes the body that is currently shown under the [AppBar] in the /// Describes the body that is currently shown under the [AppBar] in the
...@@ -346,13 +348,18 @@ class _SearchPage<T> extends StatefulWidget { ...@@ -346,13 +348,18 @@ class _SearchPage<T> extends StatefulWidget {
} }
class _SearchPageState<T> extends State<_SearchPage<T>> { class _SearchPageState<T> extends State<_SearchPage<T>> {
// This node is owned, but not hosted by, the search page. Hosting is done by
// the text field.
FocusNode focusNode = FocusNode();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
queryTextController.addListener(_onQueryChanged); queryTextController.addListener(_onQueryChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged); widget.animation.addStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged); widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
widget.delegate._focusNode.addListener(_onFocusChanged); focusNode.addListener(_onFocusChanged);
widget.delegate._focusNode = focusNode;
} }
@override @override
...@@ -361,7 +368,8 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -361,7 +368,8 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
queryTextController.removeListener(_onQueryChanged); queryTextController.removeListener(_onQueryChanged);
widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged); widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._focusNode.removeListener(_onFocusChanged); widget.delegate._focusNode = null;
focusNode.dispose();
} }
void _onAnimationStatusChanged(AnimationStatus status) { void _onAnimationStatusChanged(AnimationStatus status) {
...@@ -370,12 +378,12 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -370,12 +378,12 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
} }
widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.animation.removeStatusListener(_onAnimationStatusChanged);
if (widget.delegate._currentBody == _SearchBody.suggestions) { if (widget.delegate._currentBody == _SearchBody.suggestions) {
FocusScope.of(context).requestFocus(widget.delegate._focusNode); focusNode.requestFocus();
} }
} }
void _onFocusChanged() { void _onFocusChanged() {
if (widget.delegate._focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) { if (focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
widget.delegate.showSuggestions(context); widget.delegate.showSuggestions(context);
} }
} }
...@@ -436,7 +444,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -436,7 +444,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
leading: widget.delegate.buildLeading(context), leading: widget.delegate.buildLeading(context),
title: TextField( title: TextField(
controller: queryTextController, controller: queryTextController,
focusNode: widget.delegate._focusNode, focusNode: focusNode,
style: theme.textTheme.title, style: theme.textTheme.title,
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
onSubmitted: (String _) { onSubmitted: (String _) {
......
...@@ -1465,7 +1465,8 @@ class RenderEditable extends RenderBox { ...@@ -1465,7 +1465,8 @@ class RenderEditable extends RenderBox {
} }
TextSelection _selectWordAtOffset(TextPosition position) { TextSelection _selectWordAtOffset(TextPosition position) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
final TextRange word = _textPainter.getWordBoundary(position); final TextRange word = _textPainter.getWordBoundary(position);
// When long-pressing past the end of the text, we want a collapsed cursor. // When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= word.end) if (position.offset >= word.end)
...@@ -1537,7 +1538,8 @@ class RenderEditable extends RenderBox { ...@@ -1537,7 +1538,8 @@ class RenderEditable extends RenderBox {
} }
void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) { void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
// If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while // If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while
// the floating cursor's color is _cursorColor; // the floating cursor's color is _cursorColor;
...@@ -1603,7 +1605,8 @@ class RenderEditable extends RenderBox { ...@@ -1603,7 +1605,8 @@ class RenderEditable extends RenderBox {
} }
void _paintFloatingCaret(Canvas canvas, Offset effectiveOffset) { void _paintFloatingCaret(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
assert(_floatingCursorOn); assert(_floatingCursorOn);
// We always want the floating cursor to render at full opacity. // We always want the floating cursor to render at full opacity.
...@@ -1690,7 +1693,8 @@ class RenderEditable extends RenderBox { ...@@ -1690,7 +1693,8 @@ class RenderEditable extends RenderBox {
} }
void _paintSelection(Canvas canvas, Offset effectiveOffset) { void _paintSelection(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
assert(_selectionRects != null); assert(_selectionRects != null);
final Paint paint = Paint()..color = _selectionColor; final Paint paint = Paint()..color = _selectionColor;
for (ui.TextBox box in _selectionRects) for (ui.TextBox box in _selectionRects)
...@@ -1698,7 +1702,8 @@ class RenderEditable extends RenderBox { ...@@ -1698,7 +1702,8 @@ class RenderEditable extends RenderBox {
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
final Offset effectiveOffset = offset + _paintOffset; final Offset effectiveOffset = offset + _paintOffset;
bool showSelection = false; bool showSelection = false;
......
...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart'; ...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
/// onTap: () { /// onTap: () {
/// FocusScope.of(context).requestFocus(_focusNode); /// FocusScope.of(context).requestFocus(_focusNode);
/// }, /// },
/// child: Text('Tap to focus'), /// child: const Text('Tap to focus'),
/// ); /// );
/// } /// }
/// return Text(_message ?? 'Press a key'); /// return Text(_message ?? 'Press a key');
......
...@@ -787,6 +787,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -787,6 +787,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final LayerLink _layerLink = LayerLink(); final LayerLink _layerLink = LayerLink();
bool _didAutoFocus = false; bool _didAutoFocus = false;
FocusAttachment _focusAttachment;
// This value is an eyeball estimation of the time it takes for the iOS cursor // This value is an eyeball estimation of the time it takes for the iOS cursor
// to ease in and out. // to ease in and out.
...@@ -809,6 +810,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -809,6 +810,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void initState() { void initState() {
super.initState(); super.initState();
widget.controller.addListener(_didChangeTextEditingValue); widget.controller.addListener(_didChangeTextEditingValue);
_focusAttachment = widget.focusNode.attach(context);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
_scrollController.addListener(() { _selectionOverlay?.updateForScroll(); }); _scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
_cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration); _cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration);
...@@ -836,6 +838,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -836,6 +838,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
if (widget.focusNode != oldWidget.focusNode) { if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged); oldWidget.focusNode.removeListener(_handleFocusChanged);
_focusAttachment?.detach();
_focusAttachment = widget.focusNode.attach(context);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive(); updateKeepAlive();
} }
...@@ -852,6 +856,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -852,6 +856,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
assert(_cursorTimer == null); assert(_cursorTimer == null);
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
_selectionOverlay = null; _selectionOverlay = null;
_focusAttachment.detach();
widget.focusNode.removeListener(_handleFocusChanged); widget.focusNode.removeListener(_handleFocusChanged);
super.dispose(); super.dispose();
} }
...@@ -1091,10 +1096,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1091,10 +1096,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (_hasFocus) { if (_hasFocus) {
_openInputConnection(); _openInputConnection();
} else { } else {
final List<FocusScopeNode> ancestorScopes = FocusScope.ancestorsOf(context); widget.focusNode.requestFocus();
for (int i = ancestorScopes.length - 1; i >= 1; i -= 1)
ancestorScopes[i].setFirstFocus(ancestorScopes[i - 1]);
FocusScope.of(context).requestFocus(widget.focusNode);
} }
} }
...@@ -1400,7 +1402,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1400,7 +1402,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
FocusScope.of(context).reparentIfNeeded(widget.focusNode); _focusAttachment.reparent();
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls; final TextSelectionControls controls = widget.selectionControls;
......
...@@ -2117,7 +2117,7 @@ class BuildOwner { ...@@ -2117,7 +2117,7 @@ class BuildOwner {
/// the [FocusScopeNode] for a given [BuildContext]. /// the [FocusScopeNode] for a given [BuildContext].
/// ///
/// See [FocusManager] for more details. /// See [FocusManager] for more details.
final FocusManager focusManager = FocusManager(); FocusManager focusManager = FocusManager();
/// Adds an element to the dirty elements list so that it will be rebuilt /// Adds an element to the dirty elements list so that it will be rebuilt
/// when [WidgetsBinding.drawFrame] calls [buildScope]. /// when [WidgetsBinding.drawFrame] calls [buildScope].
......
...@@ -1468,7 +1468,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1468,7 +1468,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final Set<Route<dynamic>> _poppedRoutes = <Route<dynamic>>{}; final Set<Route<dynamic>> _poppedRoutes = <Route<dynamic>>{};
/// The [FocusScopeNode] for the [FocusScope] that encloses the routes. /// The [FocusScopeNode] for the [FocusScope] that encloses the routes.
final FocusScopeNode focusScopeNode = FocusScopeNode(); final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope');
final List<OverlayEntry> _initialOverlayEntries = <OverlayEntry>[]; final List<OverlayEntry> _initialOverlayEntries = <OverlayEntry>[];
...@@ -1556,7 +1556,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1556,7 +1556,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
route.dispose(); route.dispose();
_poppedRoutes.clear(); _poppedRoutes.clear();
_history.clear(); _history.clear();
focusScopeNode.detach(); focusScopeNode.dispose();
super.dispose(); super.dispose();
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
} }
......
...@@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
export 'package:flutter/services.dart' show RawKeyEvent; export 'package:flutter/services.dart' show RawKeyEvent;
...@@ -112,5 +113,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> { ...@@ -112,5 +113,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> {
} }
@override @override
Widget build(BuildContext context) => widget.child; Widget build(BuildContext context) => Focus(focusNode: widget.focusNode, child: widget.child);
} }
...@@ -583,6 +583,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -583,6 +583,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
// This is the combination of the two animations for the route. // This is the combination of the two animations for the route.
Listenable _listenable; Listenable _listenable;
/// The node this scope will use for its root [FocusScope] widget.
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope');
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -592,12 +595,18 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -592,12 +595,18 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
if (widget.route.secondaryAnimation != null) if (widget.route.secondaryAnimation != null)
animations.add(widget.route.secondaryAnimation); animations.add(widget.route.secondaryAnimation);
_listenable = Listenable.merge(animations); _listenable = Listenable.merge(animations);
if (widget.route.isCurrent) {
widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
}
} }
@override @override
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) {
widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
}
} }
@override @override
...@@ -612,6 +621,12 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -612,6 +621,12 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
}); });
} }
@override
void dispose() {
focusScopeNode.dispose();
super.dispose();
}
// 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) {
...@@ -629,7 +644,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -629,7 +644,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
child: PageStorage( child: PageStorage(
bucket: widget.route._storageBucket, // immutable bucket: widget.route._storageBucket, // immutable
child: FocusScope( child: FocusScope(
node: widget.route.focusScopeNode, // immutable node: focusScopeNode, // immutable
child: RepaintBoundary( child: RepaintBoundary(
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _listenable, // immutable animation: _listenable, // immutable
...@@ -887,9 +902,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -887,9 +902,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return child; return child;
} }
/// The node this route will use for its root [FocusScope] widget.
final FocusScopeNode focusScopeNode = FocusScopeNode();
@override @override
void install(OverlayEntry insertionPoint) { void install(OverlayEntry insertionPoint) {
super.install(insertionPoint); super.install(insertionPoint);
...@@ -899,16 +911,12 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -899,16 +911,12 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
@override @override
TickerFuture didPush() { TickerFuture didPush() {
navigator.focusScopeNode.setFirstFocus(focusScopeNode); if (_scopeKey.currentState != null) {
navigator.focusScopeNode.setFirstFocus(_scopeKey.currentState.focusScopeNode);
}
return super.didPush(); return super.didPush();
} }
@override
void dispose() {
focusScopeNode.detach();
super.dispose();
}
// The API for subclasses to override - used by this class // The API for subclasses to override - used by this class
/// Whether you can dismiss this route by tapping the modal barrier. /// Whether you can dismiss this route by tapping the modal barrier.
......
...@@ -106,7 +106,10 @@ void main() { ...@@ -106,7 +106,10 @@ void main() {
testWidgets('Last tab gets focus', (WidgetTester tester) async { testWidgets('Last tab gets focus', (WidgetTester tester) async {
// 2 nodes for 2 tabs // 2 nodes for 2 tabs
final List<FocusNode> focusNodes = <FocusNode>[FocusNode(), FocusNode()]; final List<FocusNode> focusNodes = <FocusNode>[
FocusNode(debugLabel: 'Node 1'),
FocusNode(debugLabel: 'Node 2'),
];
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
...@@ -139,7 +142,10 @@ void main() { ...@@ -139,7 +142,10 @@ void main() {
testWidgets('Do not affect focus order in the route', (WidgetTester tester) async { testWidgets('Do not affect focus order in the route', (WidgetTester tester) async {
final List<FocusNode> focusNodes = <FocusNode>[ final List<FocusNode> focusNodes = <FocusNode>[
FocusNode(), FocusNode(), FocusNode(), FocusNode(), FocusNode(debugLabel: 'Node 1'),
FocusNode(debugLabel: 'Node 2'),
FocusNode(debugLabel: 'Node 3'),
FocusNode(debugLabel: 'Node 4'),
]; ];
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -9,11 +9,14 @@ void main() { ...@@ -9,11 +9,14 @@ void main() {
testWidgets('Dialog interaction', (WidgetTester tester) async { testWidgets('Dialog interaction', (WidgetTester tester) async {
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
final FocusNode focusNode = FocusNode(debugLabel: 'Editable Text Node');
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( MaterialApp(
home: Material( home: Material(
child: Center( child: Center(
child: TextField( child: TextField(
focusNode: focusNode,
autofocus: true, autofocus: true,
), ),
), ),
...@@ -130,7 +133,7 @@ void main() { ...@@ -130,7 +133,7 @@ void main() {
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
}, skip: true); // https://github.com/flutter/flutter/issues/29384. });
testWidgets('Focus triggers keep-alive', (WidgetTester tester) async { testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
......
...@@ -2754,29 +2754,30 @@ void main() { ...@@ -2754,29 +2754,30 @@ void main() {
controller = TextEditingController(); controller = TextEditingController();
}); });
MaterialApp setupWidget() { Future<void> setupWidget(WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
controller = TextEditingController(); controller = TextEditingController();
return MaterialApp( await tester.pumpWidget(
home: Material( MaterialApp(
child: RawKeyboardListener( home: Material(
focusNode: focusNode, child: RawKeyboardListener(
onKey: null, focusNode: focusNode,
child: TextField( onKey: null,
controller: controller, child: TextField(
maxLines: 3, controller: controller,
strutStyle: StrutStyle.disabled, maxLines: 3,
strutStyle: StrutStyle.disabled,
),
), ),
) , ),
), ),
); );
await tester.pump();
} }
testWidgets('Shift test 1', (WidgetTester tester) async { testWidgets('Shift test 1', (WidgetTester tester) async {
await setupWidget(tester);
await tester.pumpWidget(setupWidget());
const String testValue = 'a big house'; const String testValue = 'a big house';
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2789,7 +2790,7 @@ void main() { ...@@ -2789,7 +2790,7 @@ void main() {
}); });
testWidgets('Control Shift test', (WidgetTester tester) async { testWidgets('Control Shift test', (WidgetTester tester) async {
await tester.pumpWidget(setupWidget()); await setupWidget(tester);
const String testValue = 'their big house'; const String testValue = 'their big house';
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2805,7 +2806,7 @@ void main() { ...@@ -2805,7 +2806,7 @@ void main() {
}); });
testWidgets('Down and up test', (WidgetTester tester) async { testWidgets('Down and up test', (WidgetTester tester) async {
await tester.pumpWidget(setupWidget()); await setupWidget(tester);
const String testValue = 'a big house'; const String testValue = 'a big house';
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2827,7 +2828,7 @@ void main() { ...@@ -2827,7 +2828,7 @@ void main() {
}); });
testWidgets('Down and up test 2', (WidgetTester tester) async { testWidgets('Down and up test 2', (WidgetTester tester) async {
await tester.pumpWidget(setupWidget()); await setupWidget(tester);
const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2914,6 +2915,8 @@ void main() { ...@@ -2914,6 +2915,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2984,6 +2987,8 @@ void main() { ...@@ -2984,6 +2987,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -3093,6 +3098,8 @@ void main() { ...@@ -3093,6 +3098,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
......
...@@ -17,8 +17,8 @@ import 'editable_text_utils.dart'; ...@@ -17,8 +17,8 @@ import 'editable_text_utils.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node');
final FocusScopeNode focusScopeNode = FocusScopeNode(); final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'EditableText Scope Node');
const TextStyle textStyle = TextStyle(); const TextStyle textStyle = TextStyle();
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
...@@ -955,6 +955,9 @@ void main() { ...@@ -955,6 +955,9 @@ void main() {
), ),
)); ));
focusNode.requestFocus();
await tester.pump();
expect( expect(
semantics, semantics,
includesNodeWith( includesNodeWith(
...@@ -1512,6 +1515,8 @@ void main() { ...@@ -1512,6 +1515,8 @@ void main() {
), ),
); );
focusNode.requestFocus();
// Now change it to make it obscure text. // Now change it to make it obscure text.
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
...@@ -1884,7 +1889,7 @@ void main() { ...@@ -1884,7 +1889,7 @@ void main() {
composing: TextRange(start: 5, end: 14), composing: TextRange(start: 5, end: 14),
), ),
); );
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode(debugLabel: 'Test Focus Node');
await tester.pumpWidget(MaterialApp( // So we can show overlays. await tester.pumpWidget(MaterialApp( // So we can show overlays.
home: EditableText( home: EditableText(
......
This diff is collapsed.
...@@ -12,7 +12,7 @@ void sendFakeKeyEvent(Map<String, dynamic> data) { ...@@ -12,7 +12,7 @@ void sendFakeKeyEvent(Map<String, dynamic> data) {
BinaryMessages.handlePlatformMessage( BinaryMessages.handlePlatformMessage(
SystemChannels.keyEvent.name, SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data), SystemChannels.keyEvent.codec.encodeMessage(data),
(ByteData data) { }, (ByteData data) {},
); );
} }
...@@ -29,13 +29,15 @@ void main() { ...@@ -29,13 +29,15 @@ void main() {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget(RawKeyboardListener( await tester.pumpWidget(
focusNode: focusNode, RawKeyboardListener(
onKey: events.add, focusNode: focusNode,
child: Container(), onKey: events.add,
)); child: Container(),
),
);
tester.binding.focusManager.rootScope.requestFocus(focusNode); focusNode.requestFocus();
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ sendFakeKeyEvent(<String, dynamic>{
...@@ -65,13 +67,15 @@ void main() { ...@@ -65,13 +67,15 @@ void main() {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget(RawKeyboardListener( await tester.pumpWidget(
focusNode: focusNode, RawKeyboardListener(
onKey: events.add, focusNode: focusNode,
child: Container(), onKey: events.add,
)); child: Container(),
),
);
tester.binding.focusManager.rootScope.requestFocus(focusNode); focusNode.requestFocus();
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ sendFakeKeyEvent(<String, dynamic>{
......
...@@ -692,6 +692,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -692,6 +692,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
FlutterError.onError = _oldExceptionHandler; FlutterError.onError = _oldExceptionHandler;
_pendingExceptionDetails = null; _pendingExceptionDetails = null;
_parentZone = null; _parentZone = null;
buildOwner.focusManager = FocusManager();
} }
} }
......
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