Unverified Commit 590cc27b authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Revert "Implement focus traversal for desktop platforms, shoehorn edition. (#30040)" (#31461)

This reverts commit 4218c0bc.
parent 096439b4
...@@ -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: () {
_focusNode.requestFocus(); FocusScope.of(context).requestFocus(_focusNode);
}, },
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: const Text(_title)), appBar: AppBar(title: 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: const Text(_title)), appBar: AppBar(title: 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: const Text('Tap to focus'), /// child: 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(debugLabel: 'Tab Focus Scope $index'), (int index) => FocusScopeNode(),
); );
} }
...@@ -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.dispose(); focusScopeNode.detach();
} }
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,8 +208,7 @@ abstract class SearchDelegate<T> { ...@@ -208,8 +208,7 @@ abstract class SearchDelegate<T> {
/// ///
/// * [showResults] to show the search results. /// * [showResults] to show the search results.
void showSuggestions(BuildContext context) { void showSuggestions(BuildContext context) {
assert(_focusNode != null, '_focusNode must be set by route before showSuggestions is called.'); FocusScope.of(context).requestFocus(_focusNode);
_focusNode.requestFocus();
_currentBody = _SearchBody.suggestions; _currentBody = _SearchBody.suggestions;
} }
...@@ -219,7 +218,7 @@ abstract class SearchDelegate<T> { ...@@ -219,7 +218,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);
...@@ -233,9 +232,7 @@ abstract class SearchDelegate<T> { ...@@ -233,9 +232,7 @@ abstract class SearchDelegate<T> {
/// page. /// page.
Animation<double> get transitionAnimation => _proxyAnimation; Animation<double> get transitionAnimation => _proxyAnimation;
// The focus node to use for manipulating focus on the search page. This is final FocusNode _focusNode = FocusNode();
// managed, owned, and set by the _SearchPageRoute using this delegate.
FocusNode _focusNode;
final TextEditingController _queryTextController = TextEditingController(); final TextEditingController _queryTextController = TextEditingController();
...@@ -249,6 +246,7 @@ abstract class SearchDelegate<T> { ...@@ -249,6 +246,7 @@ 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
...@@ -348,18 +346,13 @@ class _SearchPage<T> extends StatefulWidget { ...@@ -348,18 +346,13 @@ 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);
focusNode.addListener(_onFocusChanged); widget.delegate._focusNode.addListener(_onFocusChanged);
widget.delegate._focusNode = focusNode;
} }
@override @override
...@@ -368,8 +361,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -368,8 +361,7 @@ 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 = null; widget.delegate._focusNode.removeListener(_onFocusChanged);
focusNode.dispose();
} }
void _onAnimationStatusChanged(AnimationStatus status) { void _onAnimationStatusChanged(AnimationStatus status) {
...@@ -378,12 +370,12 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -378,12 +370,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) {
focusNode.requestFocus(); FocusScope.of(context).requestFocus(widget.delegate._focusNode);
} }
} }
void _onFocusChanged() { void _onFocusChanged() {
if (focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) { if (widget.delegate._focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
widget.delegate.showSuggestions(context); widget.delegate.showSuggestions(context);
} }
} }
...@@ -444,7 +436,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -444,7 +436,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: focusNode, focusNode: widget.delegate._focusNode,
style: theme.textTheme.title, style: theme.textTheme.title,
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
onSubmitted: (String _) { onSubmitted: (String _) {
......
...@@ -1455,8 +1455,7 @@ class RenderEditable extends RenderBox { ...@@ -1455,8 +1455,7 @@ 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)
...@@ -1528,8 +1527,7 @@ class RenderEditable extends RenderBox { ...@@ -1528,8 +1527,7 @@ 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;
...@@ -1595,8 +1593,7 @@ class RenderEditable extends RenderBox { ...@@ -1595,8 +1593,7 @@ 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.
...@@ -1683,8 +1680,7 @@ class RenderEditable extends RenderBox { ...@@ -1683,8 +1680,7 @@ 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)
...@@ -1692,8 +1688,7 @@ class RenderEditable extends RenderBox { ...@@ -1692,8 +1688,7 @@ 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: const Text('Tap to focus'), /// child: Text('Tap to focus'),
/// ); /// );
/// } /// }
/// return Text(_message ?? 'Press a key'); /// return Text(_message ?? 'Press a key');
......
...@@ -787,7 +787,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -787,7 +787,6 @@ 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.
...@@ -810,7 +809,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -810,7 +809,6 @@ 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);
...@@ -838,8 +836,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -838,8 +836,6 @@ 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();
} }
...@@ -856,7 +852,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -856,7 +852,6 @@ 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();
} }
...@@ -1096,7 +1091,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1096,7 +1091,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (_hasFocus) { if (_hasFocus) {
_openInputConnection(); _openInputConnection();
} else { } else {
widget.focusNode.requestFocus(); final List<FocusScopeNode> ancestorScopes = FocusScope.ancestorsOf(context);
for (int i = ancestorScopes.length - 1; i >= 1; i -= 1)
ancestorScopes[i].setFirstFocus(ancestorScopes[i - 1]);
FocusScope.of(context).requestFocus(widget.focusNode);
} }
} }
...@@ -1402,7 +1400,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1402,7 +1400,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
_focusAttachment.reparent(); FocusScope.of(context).reparentIfNeeded(widget.focusNode);
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls; final TextSelectionControls controls = widget.selectionControls;
......
...@@ -3,505 +3,61 @@ ...@@ -3,505 +3,61 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'binding.dart'; /// A leaf node in the focus tree that can receive focus.
import 'focus_scope.dart';
import 'framework.dart';
/// Signature of a callback used by [Focus.onKey] and [FocusScope.onKey]
/// to receive key events.
///
/// The [node] is the node that received the event.
typedef FocusOnKeyCallback = bool Function(FocusNode node, RawKeyEvent event);
/// An attachment point for a [FocusNode].
///
/// Once created, a [FocusNode] must be attached to the widget tree by its
/// _host_ [StatefulWidget] via a [FocusAttachment] object. [FocusAttachment]s
/// are owned by the [StatefulWidget] that hosts a [FocusNode] or
/// [FocusScopeNode]. There can be multiple [FocusAttachment]s for each
/// [FocusNode], but the node will only ever be attached to one of them at a
/// time.
///
/// This attachment is created by calling [FocusNode.attach], usually from the
/// host widget's [State.initState] method. If the widget is updated to have a
/// different focus node, then the new node needs to be attached in
/// [State.didUpdateWidget], after calling [detach] on the previous
/// [FocusAttachment]. Once detached, the attachment is defunct and will no
/// longer make changes to the [FocusNode] through [reparent].
///
/// Without these attachment points, it would be possible for a focus node to
/// simultaneously be attached to more than one part of the widget tree during
/// the build stage.
class FocusAttachment {
/// A private constructor, because [FocusAttachment]s are only to be created
/// by [FocusNode.attach].
FocusAttachment._(this._node) : assert(_node != null);
// The focus node that this attachment manages an attachment for. The node may
// not yet have a parent, or may have been detached from this attachment, so
// don't count on this node being in a usable state.
final FocusNode _node;
/// Returns true if the associated node is attached to this attachment.
///
/// It is possible to be attached to the widget tree, but not be placed in
/// the focus tree (i.e. to not have a parent yet in the focus tree).
bool get isAttached => _node._attachment == this;
/// Detaches the [FocusNode] this attachment point is associated with from the
/// focus tree, and disconnects it from this attachment point.
///
/// Calling [FocusNode.dispose] will also automatically detach the node.
void detach() {
assert(_node != null);
if (isAttached) {
_node._parent?._removeChild(_node);
_node._attachment = null;
}
assert(!isAttached);
}
/// Ensures that the [FocusNode] attached at this attachment point has the
/// proper parent node, changing it if necessary.
///
/// If given, ensures that the given [parent] node is the parent of the node
/// that is attached at this attachment point, changing it if necessary.
/// However, it is usually not necessary to supply an explicit parent, since
/// [reparent] will use [Focus.of] to determine the correct parent node for
/// the context given in [FocusNode.attach].
///
/// If [isAttached] is false, then calling this method does nothing.
///
/// Should be called whenever the associated widget is rebuilt in order to
/// maintain the focus hierarchy.
///
/// A [StatefulWidget] that hosts a [FocusNode] should call this method on the
/// node it hosts during its [State.build] or [State.didChangeDependencies]
/// methods in case the widget is moved from one location in the tree to
/// another location that has a different [FocusScope] or context.
///
/// The optional [parent] argument must be supplied when not using [Focus] and
/// [FocusScope] widgets to build the focus tree, or if there is a need to
/// supply the parent explicitly (which are both uncommon).
void reparent({FocusNode parent}) {
assert(_node != null);
if (isAttached) {
assert(_node.context != null);
parent ??= Focus.of(_node.context);
assert(parent != null);
parent._reparent(_node);
}
}
}
/// An object that can be used by a stateful widget to obtain the keyboard focus
/// and to handle keyboard events.
///
/// _Please see the [Focus] and [FocusScope] widgets, which are utility widgets
/// that manage their own [FocusNode]s and [FocusScopeNode]s, respectively. If
/// they aren't appropriate, [FocusNode]s can be managed directly._
///
/// [FocusNode]s are persistent objects that form a _focus tree_ that is a
/// representation of the widgets in the hierarchy that are interested in focus.
/// A focus node might need to be created if it is passed in from an ancestor of
/// a [Focus] widget to control the focus of the children from the ancestor, or
/// a widget might need to host one if the widget subsystem is not being used,
/// or if the [Focus] and [FocusScope] widgets provide insufficient control.
///
/// [FocusNodes] are organized into _scopes_ (see [FocusScopeNode]), which form
/// sub-trees of nodes that can be traversed as a group. Within a scope, the
/// most recent nodes to have focus are remembered, and if a node is focused and
/// then removed, the previous node receives focus again.
///
/// The focus node hierarchy can be traversed using the [parent], [children],
/// [ancestors] and [descendants] accessors.
///
/// [FocusNode]s are [ChangeNotifier]s, so a listener can be registered to
/// receive a notification when the focus changes. If the [Focus] and
/// [FocusScope] widgets are being used to manage the nodes, consider
/// establishing an [InheritedWidget] dependency on them by calling [Focus.of]
/// or [FocusScope.of] instead. [Focus.hasFocus] can also be used to establish a
/// similar dependency, especially if all that is needed is to determine whether
/// or not the widget is focused at build time.
///
/// To see the focus tree in the debug console, call [debugDumpFocusTree]. To
/// get the focus tree as a string, call [debugDescribeFocusTree].
///
/// {@template flutter.widgets.focus_manager.focus.lifecycle}
/// ## Lifecycle
///
/// There are several actors involved in the lifecycle of a
/// [FocusNode]/[FocusScopeNode]. They are created and disposed by their
/// _owner_, attached, detached, and reparented using a [FocusAttachment] by
/// their _host_ (which must be owned by the [State] of a [StatefulWidget]), and
/// they are managed by the [FocusManager]. Different parts of the [FocusNode]
/// API are intended for these different actors.
///
/// [FocusNode]s (and hence [FocusScopeNode]s) are persistent objects that form
/// part of a _focus tree_ that is a sparse representation of the widgets in the
/// hierarchy that are interested in receiving keyboard events. They must be
/// managed like other persistent state, which is typically done by a
/// [StatefulWidget] that owns the node. A stateful widget that owns a focus
/// scope node must call [dispose] from its [State.dispose] method.
///
/// Once created, a [FocusNode] must be attached to the widget tree via a
/// [FocusAttachment] object. This attachment is created by calling [attach],
/// usually from the [State.initState] method. If the hosting widget is updated
/// to have a different focus node, then the updated node needs to be attached
/// in [State.didUpdateWidget], after calling [detach] on the previous
/// [FocusAttachment].
///
/// Because [FocusNode]s form a sparse representation of the widget tree,
/// they must be updated whenever the widget tree is rebuilt. This is done by
/// calling [FocusAttachment.reparent], usually from the [State.build] or
/// [State.didChangeDependencies] methods of the widget that represents the
/// focused region, so that the [BuildContext] assigned to the [FocusScopeNode]
/// can be tracked (the context is used to obtain the [RenderObject], from which
/// the geometry of focused regions can be determined).
/// ///
/// Creating a [FocusNode] each time [State.build] is invoked will cause the /// The focus tree keeps track of which widget is the user's current focus. The
/// focus to be lost each time the widget is built, which is usually not desired /// focused widget often listens for keyboard events.
/// behavior (call [unfocus] if losing focus is desired).
/// ///
/// If, as is common, the hosting [StatefulWidget] is also the owner of the /// To request focus, find the [FocusScopeNode] for the current [BuildContext]
/// focus node, then it will also call [dispose] from its [State.dispose] (in /// and call the [FocusScopeNode.requestFocus] method:
/// which case the [detach] may be skipped, since dispose will automatically
/// detach). If another object owns the focus node, then it must call [dispose]
/// when the node is done being used.
/// {@endtemplate}
/// ///
/// {@template flutter.widgets.focus_manager.focus.keyEvents} /// ```dart
/// ## Key Event Propagation /// FocusScope.of(context).requestFocus(focusNode);
///
/// The [FocusManager] receives all key events and will pass them to the focused
/// nodes. It starts with the node with the primary focus, and will call the
/// [onKey] callback for that node. If the callback returns false, indicating
/// that it did not handle the event, the [FocusManager] will move to the parent
/// of that node and call its [onKey]. If that [onKey] returns true, then it
/// will stop propagating the event. If it reaches the root [FocusScopeNode],
/// [FocusManager.rootScope], the event is discarded.
/// {@endtemplate}
///
/// {@tool snippet --template=stateless_widget_scaffold}
/// This example shows how a FocusNode should be managed if not using the
/// [Focus] or [FocusScope] widgets. See the [Focus] widget for a similar
/// example using [Focus] and [FocusScope] widgets.
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ``` /// ```
/// ///
/// ```dart preamble /// If your widget requests focus, be sure to call
/// class ColorfulButton extends StatefulWidget { /// `FocusScope.of(context).reparentIfNeeded(focusNode);` in your `build`
/// ColorfulButton({Key key}) : super(key: key); /// method to reparent your [FocusNode] if your widget moves from one
/// /// location in the tree to another.
/// @override
/// _ColorfulButtonState createState() => _ColorfulButtonState();
/// }
///
/// class _ColorfulButtonState extends State<ColorfulButton> {
/// FocusNode _node;
/// FocusAttachment _nodeAttachment;
/// Color _color = Colors.white;
///
/// @override
/// void initState() {
/// super.initState();
/// _node = FocusNode(debugLabel: 'Button');
/// _nodeAttachment = _node.attach(context, onKey: _handleKeyPress);
/// }
///
/// bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
/// if (event is RawKeyDownEvent) {
/// print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}');
/// if (event.logicalKey == LogicalKeyboardKey.keyR) {
/// print('Changing color to red.');
/// setState(() {
/// _color = Colors.red;
/// });
/// return true;
/// } else if (event.logicalKey == LogicalKeyboardKey.keyG) {
/// print('Changing color to green.');
/// setState(() {
/// _color = Colors.green;
/// });
/// return true;
/// } else if (event.logicalKey == LogicalKeyboardKey.keyB) {
/// print('Changing color to blue.');
/// setState(() {
/// _color = Colors.blue;
/// });
/// return true;
/// }
/// }
/// return false;
/// }
///
/// @override
/// void dispose() {
/// // The attachment will automatically be detached in dispose().
/// _node.dispose();
/// super.dispose();
/// }
/// ///
/// @override /// ## Lifetime
/// Widget build(BuildContext context) {
/// _nodeAttachment.reparent();
/// return GestureDetector(
/// onTap: () {
/// if (_node.hasFocus) {
/// setState(() {
/// _node.unfocus();
/// });
/// } else {
/// setState(() {
/// _node.requestFocus();
/// });
/// }
/// },
/// child: Center(
/// child: Container(
/// width: 400,
/// height: 100,
/// color: _node.hasFocus ? _color : Colors.white,
/// alignment: Alignment.center,
/// child: Text(
/// _node.hasFocus ? "I'm in color! Press R,G,B!" : 'Press to focus'),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// ///
/// ```dart /// Focus nodes are long-lived objects. For example, if a stateful widget has a
/// Widget build(BuildContext context) { /// focusable child widget, it should create a [FocusNode] in the
/// final TextTheme textTheme = Theme.of(context).textTheme; /// [State.initState] method, and [dispose] it in the [State.dispose] method,
/// return DefaultTextStyle( /// providing the same [FocusNode] to the focusable child each time the
/// style: textTheme.display1, /// [State.build] method is run. In particular, creating a [FocusNode] each time
/// child: ColorfulButton(), /// [State.build] is invoked will cause the focus to be lost each time the
/// ); /// widget is built.
/// }
/// ```
/// {@end-tool}
/// ///
/// See also: /// See also:
/// ///
/// * [Focus], a widget that manages a [FocusNode] and provides access to /// * [FocusScopeNode], which is an interior node in the focus tree.
/// focus information and actions to its descendant widgets. /// * [FocusScope.of], which provides the [FocusScopeNode] for a given
/// * [FocusScope], a widget that manages a [FocusScopeNode] and provides /// [BuildContext].
/// access to scope information and actions to its descendant widgets. class FocusNode extends ChangeNotifier {
/// * [FocusAttachment], a widget that connects a [FocusScopeNode] to the FocusScopeNode _parent;
/// widget tree.
/// * [FocusManager], a singleton that manages the focus and distributes key
/// events to focused nodes.
class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// Creates a focus node.
///
/// The [debugLabel] is ignored on release builds.
FocusNode({
String debugLabel,
FocusOnKeyCallback onKey,
}) : _onKey = onKey {
// Set it via the setter so that it does nothing on release builds.
this.debugLabel = debugLabel;
}
/// The context that was supplied to [attach].
///
/// This is typically the context for the widget that is being focused, as it
/// is used to determine the bounds of the widget.
BuildContext get context => _context;
BuildContext _context;
/// Called if this focus node receives a key event while focused (i.e. when
/// [hasFocus] returns true).
///
/// {@macro flutter.widgets.focus_manager.focus.keyEvents}
FocusOnKeyCallback get onKey => _onKey;
FocusOnKeyCallback _onKey;
FocusManager _manager; FocusManager _manager;
bool _hasKeyboardToken = false; bool _hasKeyboardToken = false;
/// Returns the parent node for this object. /// Whether this node has the overall focus.
/// ///
/// All nodes except for the root [FocusScopeNode] ([FocusManager.rootScope]) /// A [FocusNode] has the overall focus when the node is focused in its
/// will be given a parent when they are added to the focus tree, which is /// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for
/// done using [FocusAttachment.reparent]. /// that scope and all its ancestor scopes.
FocusNode get parent => _parent;
FocusNode _parent;
/// An iterator over the children of this node.
Iterable<FocusNode> get children => _children;
final List<FocusNode> _children = <FocusNode>[];
/// A debug label that is used for diagnostic output.
/// ///
/// Will always return null in release builds. /// To request focus, find the [FocusScopeNode] for the current [BuildContext]
String get debugLabel => _debugLabel; /// and call the [FocusScopeNode.requestFocus] method:
String _debugLabel;
set debugLabel(String value) {
assert(() {
// Only set the value in debug builds.
_debugLabel = value;
return true;
}());
}
FocusAttachment _attachment;
/// An [Iterable] over the hierarchy of children below this one, in
/// depth-first order.
Iterable<FocusNode> get descendants sync* {
for (FocusNode child in _children) {
yield* child.descendants;
yield child;
}
}
/// An [Iterable] over the ancestors of this node.
///
/// Iterates the ancestors of this node starting at the parent and iterating
/// over successively more remote ancestors of this node, ending at the root
/// [FocusScope] ([FocusManager.rootScope]).
Iterable<FocusNode> get ancestors sync* {
FocusNode parent = _parent;
while (parent != null) {
yield parent;
parent = parent._parent;
}
}
/// Whether this node has input focus.
/// ///
/// A [FocusNode] has focus when it is an ancestor of a node that returns true /// ```dart
/// from [hasPrimaryFocus], or it has the primary focus itself. /// FocusScope.of(context).requestFocus(focusNode);
/// /// ```
/// The [hasFocus] accessor is different from [hasPrimaryFocus] in that
/// [hasFocus] is true if the node is anywhere in the focus chain, but for
/// [hasPrimaryFocus] the node must to be at the end of the chain to return
/// true.
///
/// A node that returns true for [hasFocus] will receive key events if none of
/// its focused descendants returned true from their [onKey] handler.
///
/// This object is a [ChangeNotifier], and notifies its [Listenable] listeners
/// (registered via [addListener]) whenever this value changes.
///
/// See also:
///
/// * [Focus.isAt], which is a static method that will return the focus
/// state of the nearest ancestor [Focus] widget's focus node.
bool get hasFocus {
if (_manager?._currentFocus == null) {
return false;
}
if (hasPrimaryFocus) {
return true;
}
return _manager._currentFocus.ancestors.contains(this);
}
/// Returns true if this node currently has the application-wide input focus.
///
/// A [FocusNode] has the primary focus when the node is focused in its
/// nearest ancestor [FocusScopeNode] and [hasFocus] is true for all its
/// ancestor nodes, but none of its descendants.
///
/// This is different from [hasFocus] in that [hasFocus] is true if the node
/// is anywhere in the focus chain, but here the node has to be at the end of
/// the chain to return true.
///
/// A node that returns true for [hasPrimaryFocus] will be the first node to
/// receive key events through its [onKey] handler.
/// ///
/// This object notifies its listeners whenever this value changes. /// This object notifies its listeners whenever this value changes.
bool get hasPrimaryFocus => _manager?._currentFocus == this; bool get hasFocus => _manager?._currentFocus == this;
/// Returns the nearest enclosing scope node above this node, including
/// this node, if it's a scope.
///
/// Returns null if no scope is found.
///
/// Use [enclosingScope] to look for scopes above this node.
FocusScopeNode get nearestScope => enclosingScope;
/// Returns the nearest enclosing scope node above this node, or null if the
/// node has not yet be added to the focus tree.
///
/// If this node is itself a scope, this will only return ancestors of this
/// scope.
///
/// Use [nearestScope] to start at this node instead of above it.
FocusScopeNode get enclosingScope {
return ancestors.firstWhere((FocusNode node) => node is FocusScopeNode, orElse: () => null);
}
/// Returns the size of the attached widget's [RenderObject], in logical
/// units.
Size get size {
assert(
context != null,
"Tried to get the size of a focus node that didn't have its context set yet.\n"
'The context needs to be set before trying to evaluate traversal policies. This '
'is typically done with the attach method.');
return context.findRenderObject().semanticBounds.size;
}
/// Returns the global offset to the upper left corner of the attached
/// widget's [RenderObject], in logical units.
Offset get offset {
assert(
context != null,
"Tried to get the offset of a focus node that didn't have its context set yet.\n"
'The context needs to be set before trying to evaluate traversal policies. This '
'is typically done with the attach method.');
final RenderObject object = context.findRenderObject();
return MatrixUtils.transformPoint(object.getTransformTo(null), object.semanticBounds.topLeft);
}
/// Returns the global rectangle of the attached widget's [RenderObject], in
/// logical units.
Rect get rect {
assert(
context != null,
"Tried to get the bounds of a focus node that didn't have its context set yet.\n"
'The context needs to be set before trying to evaluate traversal policies. This '
'is typically done with the attach method.');
final RenderObject object = context.findRenderObject();
final Offset globalOffset = MatrixUtils.transformPoint(object.getTransformTo(null), object.semanticBounds.topLeft);
return globalOffset & object.semanticBounds.size;
}
/// Removes focus from a node that has the primary focus, and cancels any
/// outstanding requests to focus it.
///
/// Calling [requestFocus] sends a request to the [FocusManager] to make that
/// node the primary focus, which schedules a microtask to resolve the latest
/// request into an update of the focus state on the tree. Calling [unfocus]
/// cancels a request that has been requested, but not yet acted upon.
///
/// This method is safe to call regardless of whether this node has ever
/// requested focus.
///
/// Has no effect on nodes that return true from [hasFocus], but false from
/// [hasPrimaryFocus].
void unfocus() {
final FocusScopeNode scope = enclosingScope;
if (scope == null) {
// This node isn't part of a tree.
return;
}
scope._focusedChildren.remove(this);
_manager?._willUnfocusNode(this);
}
/// Removes the keyboard token from this focus node if it has one. /// Removes the keyboard token from this focus node if it has one.
/// ///
...@@ -512,360 +68,330 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -512,360 +68,330 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// [FocusScopeNode.requestFocus] or [FocusScopeNode.autofocus]), the focus /// [FocusScopeNode.requestFocus] or [FocusScopeNode.autofocus]), the focus
/// node receives a keyboard token if it does not already have one. Later, /// node receives a keyboard token if it does not already have one. Later,
/// when the focus node becomes focused, the widget that manages the /// when the focus node becomes focused, the widget that manages the
/// [TextInputConnection] should show the keyboard (i.e. call /// [TextInputConnection] should show the keyboard (i.e., call
/// [TextInputConnection.show]) only if it successfully consumes the keyboard /// [TextInputConnection.show]) only if it successfully consumes the keyboard
/// token from the focus node. /// token from the focus node.
/// ///
/// Returns true if this method successfully consumes the keyboard token. /// Returns whether this function successfully consumes a keyboard token.
bool consumeKeyboardToken() { bool consumeKeyboardToken() {
if (!_hasKeyboardToken) { if (!_hasKeyboardToken)
return false; return false;
}
_hasKeyboardToken = false; _hasKeyboardToken = false;
return true; return true;
} }
// Marks the node as dirty, meaning that it needs to notify listeners of a /// Cancels any outstanding requests for focus.
// focus change the next time focus is resolved by the manager.
void _markAsDirty({FocusNode newFocus}) {
if (_manager != null) {
// If we have a manager, then let it handle the focus change.
_manager._dirtyNodes?.add(this);
_manager._markNeedsUpdate(newFocus: newFocus);
} else {
// If we don't have a manager, then change the focus locally.
newFocus?._setAsFocusedChild();
newFocus?._notify();
if (newFocus != this) {
_notify();
}
}
}
// Removes the given FocusNode and its children as a child of this node.
@mustCallSuper
void _removeChild(FocusNode node) {
assert(_children.contains(node), "Tried to remove a node that wasn't a child.");
assert(node._parent == this);
assert(node._manager == _manager);
// If the child was (or requested to be) the primary focus, then unfocus it
// and cancel any outstanding request to be focused.
node.unfocus();
node._parent = null;
_children.remove(node);
assert(_manager == null || !_manager.rootScope.descendants.contains(node));
}
void _updateManager(FocusManager manager) {
_manager = manager;
for (FocusNode descendant in descendants) {
descendant._manager = manager;
}
}
// Used by FocusAttachment.reparent to perform the actual parenting operation.
@mustCallSuper
void _reparent(FocusNode child) {
assert(child != null);
assert(child != this, 'Tried to make a child into a parent of itself.');
if (child._parent == this) {
assert(_children.contains(child), "Found a node that says it's a child, but doesn't appear in the child list.");
// The child is already a child of this parent.
return;
}
assert(_manager == null || child != _manager.rootScope, "Reparenting the root node isn't allowed.");
assert(!ancestors.contains(child), 'The supplied child is already an ancestor of this node. Loops are not allowed.');
FocusNode oldPrimaryFocus;
if (child._manager != null) {
// We want to find out what the primary focus is, since the new child
// might be an ancestor of the primary focus, and the primary focus should
// move with the child.
oldPrimaryFocus = child.hasFocus ? child._manager._currentFocus : null;
assert(oldPrimaryFocus == null || oldPrimaryFocus == child || oldPrimaryFocus.ancestors.contains(child),
"child has focus, but primary focus isn't a descendant of it.");
}
// If the child currently has focus, we have to do some extra work to keep
// that focus, and to notify any scopes that used to be ancestors, and no
// longer have focus after we move it.
final Set<FocusNode> oldFocusPath = oldPrimaryFocus?.ancestors?.toSet() ?? <FocusNode>{};
child._parent?._removeChild(child);
_children.add(child);
child._parent = this;
child._updateManager(_manager);
if (oldPrimaryFocus != null) {
final Set<FocusNode> newFocusPath = _manager?._currentFocus?.ancestors?.toSet() ?? <FocusNode>{};
// Nodes that will no longer be focused need to be marked dirty.
for (FocusNode node in oldFocusPath.difference(newFocusPath)) {
node._markAsDirty();
}
// If the node used to have focus, make sure it keeps it's old primary
// focus when it moves.
oldPrimaryFocus.requestFocus();
}
}
/// Called by the _host_ [StatefulWidget] to attach a [FocusNode] to the
/// widget tree.
///
/// In order to attach a [FocusNode] to the widget tree, call [attach],
/// typically from the [StatefulWidget]'s [State.initState] method.
/// ///
/// If the focus node in the host widget is swapped out, the new node will /// This method is safe to call regardless of whether this node has ever
/// need to be attached. [FocusAttachment.detach] should be called on the old /// requested focus.
/// node, and then [attach] called on the new node. This typically happens in void unfocus() {
/// the [State.didUpdateWidget] method. _parent?._resignFocus(this);
@mustCallSuper assert(_parent == null);
FocusAttachment attach(BuildContext context, {FocusOnKeyCallback onKey}) { assert(_manager == null);
_context = context;
_onKey = onKey;
_attachment = FocusAttachment._(this);
return _attachment;
} }
@override @override
void dispose() { void dispose() {
_manager?._willDisposeFocusNode(this); _manager?._willDisposeFocusNode(this);
_attachment?.detach(); _parent?._resignFocus(this);
assert(_parent == null);
assert(_manager == null);
super.dispose(); super.dispose();
} }
@mustCallSuper
void _notify() { void _notify() {
if (_parent == null) {
// no longer part of the tree, so don't notify.
return;
}
if (hasPrimaryFocus) {
_setAsFocusedChild();
}
notifyListeners(); notifyListeners();
} }
/// Requests the primary focus for this node, or for a supplied [node], which
/// will also give focus to its [ancestors].
///
/// If called without a node, request focus for this node.
///
/// If the given [node] is not yet a part of the focus tree, then this method
/// will add the [node] as a child of this node before requesting focus.
///
/// If the given [node] is a [FocusScopeNode] and that focus scope node has a
/// non-null [focusedChild], then request the focus for the focused child.
/// This process is recursive and continues until it encounters either a focus
/// scope node with a null focused child or an ordinary (non-scope)
/// [FocusNode] is found.
///
/// The node is notified that it has received the primary focus in a
/// microtask, so notification may lag the request by up to one frame.
void requestFocus([FocusNode node]) {
if (node != null) {
if (node._parent == null) {
_reparent(node);
}
assert(node.ancestors.contains(this),
'Focus was requested for a node that is not a descendant of the scope from which it was requested.');
node._doRequestFocus(isFromPolicy: false);
return;
}
_doRequestFocus(isFromPolicy: false);
}
// Note that this is overridden in FocusScopeNode.
void _doRequestFocus({@required bool isFromPolicy}) {
assert(isFromPolicy != null);
_setAsFocusedChild();
if (hasPrimaryFocus) {
return;
}
_hasKeyboardToken = true;
_markAsDirty(newFocus: this);
}
// Sets this node as the focused child for the enclosing scope, and that scope
// as the focused child for the scope above it, etc., until it reaches the
// root node. It doesn't change the primary focus, it just changes what node
// would be focused if the enclosing scope receives focus, and keeps track of
// previously focused children so that if one is removed, the previous focus
// returns.
void _setAsFocusedChild() {
FocusNode scopeFocus = this;
for (FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
assert(scopeFocus != ancestor, 'Somehow made a loop by setting focusedChild to its scope.');
// Remove it anywhere in the focused child history.
ancestor._focusedChildren.remove(scopeFocus);
// Add it to the end of the list, which is also the top of the queue: The
// end of the list represents the currently focused child.
ancestor._focusedChildren.add(scopeFocus);
scopeFocus = ancestor;
}
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { String toString() => '${describeIdentity(this)}${hasFocus ? '(FOCUSED)' : ''}';
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
properties.add(FlagProperty('hasFocus', value: hasFocus, ifTrue: 'FOCUSED', defaultValue: false));
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
int count = 1;
return _children.map<DiagnosticsNode>((FocusNode child) {
return child.toDiagnosticsNode(name: 'Child ${count++}');
}).toList();
}
} }
/// A subclass of [FocusNode] that acts as a scope for its descendants, /// An interior node in the focus tree.
/// maintaining information about which descendant is currently or was last
/// focused.
/// ///
/// _Please see the [FocusScope] and [Focus] widgets, which are utility widgets /// The focus tree keeps track of which widget is the user's current focus. The
/// that manage their own [FocusScopeNode]s and [FocusNode]s, respectively. If /// focused widget often listens for keyboard events.
/// they aren't appropriate, [FocusScopeNode]s can be managed directly._
/// ///
/// [FocusScopeNode] organizes [FocusNodes] into _scopes_. Scopes form sub-trees /// The interior nodes in the focus tree cannot themselves be focused but
/// of nodes that can be traversed as a group. Within a scope, the most recent /// instead remember previous focus states. A scope is currently active in its
/// nodes to have focus are remembered, and if a node is focused and then /// parent whenever [isFirstFocus] is true. If that scope is detached from its
/// removed, the original node receives focus again. /// parent, its previous sibling becomes the parent's first focus.
/// ///
/// From a [FocusScopeNode], calling [setFirstFocus], sets the given focus scope /// A [FocusNode] has the overall focus when the node is focused in its
/// as the [focusedChild] of this node, adopting if it isn't already part of the /// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for
/// focus tree. /// that scope and all its ancestor scopes.
/// ///
/// {@macro flutter.widgets.focusManager.lifecycle} /// If a [FocusScopeNode] is removed, then the next sibling node will be set as
/// {@macro flutter.widgets.focus_manager.focus.keyEvents} /// the focused node by the [FocusManager].
/// ///
/// See also: /// See also:
/// ///
/// * [Focus], a widget that manages a [FocusNode] and provides access to /// * [FocusNode], which is a leaf node in the focus tree that can receive
/// focus information and actions to its descendant widgets. /// focus.
/// * [FocusScope], a widget that manages a [FocusScopeNode] and provides /// * [FocusScope.of], which provides the [FocusScopeNode] for a given
/// access to scope information and actions to its descendant widgets. /// [BuildContext].
/// * [FocusAttachment], a widget that connects a [FocusScopeNode] to the /// * [FocusScope], which is a widget that associates a [FocusScopeNode] with
/// focus tree. /// its location in the tree.
/// * [FocusManager], a singleton that manages the focus and distributes key class FocusScopeNode extends Object with DiagnosticableTreeMixin {
/// events to focused nodes. FocusManager _manager;
class FocusScopeNode extends FocusNode { FocusScopeNode _parent;
/// Creates a FocusScope node.
///
/// All parameters are optional.
FocusScopeNode({
String debugLabel,
FocusOnKeyCallback onKey,
}) : super(debugLabel: debugLabel, onKey: onKey);
@override FocusScopeNode _nextSibling;
FocusScopeNode get nearestScope => this; FocusScopeNode _previousSibling;
/// Returns true if this scope is the focused child of its parent scope. FocusScopeNode _firstChild;
bool get isFirstFocus => enclosingScope.focusedChild == this; FocusScopeNode _lastChild;
/// Returns the child of this node that should receive focus if this scope FocusNode _focus;
/// node receives focus. List<FocusScopeNode> _focusPath;
///
/// If [hasFocus] is true, then this points to the child of this node that is /// Whether this scope is currently active in its parent scope.
/// currently focused. bool get isFirstFocus => _parent == null || _parent._firstChild == this;
///
/// Returns null if there is no currently focused child. // Returns this FocusScopeNode's ancestors, starting with the node
FocusNode get focusedChild { // below the FocusManager's rootScope.
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this, List<FocusScopeNode> _getFocusPath() {
'Focused child does not have the same idea of its enclosing scope as the scope does.'); final List<FocusScopeNode> nodes = <FocusScopeNode>[this];
return _focusedChildren.isNotEmpty ? _focusedChildren.last : null; FocusScopeNode node = _parent;
while (node != null && node != _manager?.rootScope) {
nodes.add(node);
node = node._parent;
}
return nodes;
} }
// A stack of the children that have been set as the focusedChild, most recent void _prepend(FocusScopeNode child) {
// last (which is the top of the stack). assert(child != this);
final List<FocusNode> _focusedChildren = <FocusNode>[]; assert(child != _firstChild);
assert(child != _lastChild);
assert(child._parent == null);
assert(child._manager == null);
assert(child._nextSibling == null);
assert(child._previousSibling == null);
assert(() {
FocusScopeNode node = this;
while (node._parent != null)
node = node._parent;
assert(node != child); // indicates we are about to create a cycle
return true;
}());
child._parent = this;
child._nextSibling = _firstChild;
if (_firstChild != null)
_firstChild._previousSibling = child;
_firstChild = child;
_lastChild ??= child;
child._updateManager(_manager);
}
@override void _updateManager(FocusManager manager) {
void _reparent(FocusNode child) { void update(FocusScopeNode child) {
final bool hadChildren = _children.isNotEmpty; if (child._manager == manager)
super._reparent(child); return;
final FocusScopeNode currentEnclosingScope = child.enclosingScope; child._manager = manager;
// If we just added our first child to this scope, and this scope had the // We don't proactively null out the manager for FocusNodes because the
// focus, then focus the child. // manager holds the currently active focus node until the end of the
if (!hadChildren && currentEnclosingScope.focusedChild == null && currentEnclosingScope.hasFocus) { // microtask, even if that node is detached from the focus tree.
child.requestFocus(); if (manager != null)
child._focus?._manager = manager;
child._visitChildren(update);
} }
update(this);
} }
/// Make the given [scope] the active child scope for this scope. void _visitChildren(void visitor(FocusScopeNode child)) {
/// FocusScopeNode child = _firstChild;
/// If the given [scope] is not yet a part of the focus tree, then add it to while (child != null) {
/// the tree as a child of this scope. visitor(child);
/// child = child._nextSibling;
/// The given scope must be a descendant of this scope.
void setFirstFocus(FocusScopeNode scope) {
assert(scope != null);
if (scope._parent == null) {
_reparent(scope);
} }
assert(scope.ancestors.contains(this)); }
// Move down the tree, checking each focusedChild until we get to a node
// that either isn't a scope node, or has no focused child, and then request bool _debugUltimatePreviousSiblingOf(FocusScopeNode child, { FocusScopeNode equals }) {
// focus on that node. while (child._previousSibling != null) {
FocusNode descendantFocus = scope.focusedChild; assert(child._previousSibling != child);
while (descendantFocus is FocusScopeNode && descendantFocus != null) { child = child._previousSibling;
final FocusScopeNode descendantScope = descendantFocus; }
descendantFocus = descendantScope.focusedChild; return child == equals;
}
bool _debugUltimateNextSiblingOf(FocusScopeNode child, { FocusScopeNode equals }) {
while (child._nextSibling != null) {
assert(child._nextSibling != child);
child = child._nextSibling;
}
return child == equals;
}
void _remove(FocusScopeNode child) {
assert(child._parent == this);
assert(child._manager == _manager);
assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild));
assert(_debugUltimateNextSiblingOf(child, equals: _lastChild));
if (child._previousSibling == null) {
assert(_firstChild == child);
_firstChild = child._nextSibling;
} else {
child._previousSibling._nextSibling = child._nextSibling;
} }
if (descendantFocus != null) { if (child._nextSibling == null) {
descendantFocus?._doRequestFocus(isFromPolicy: false); assert(_lastChild == child);
_lastChild = child._previousSibling;
} else { } else {
scope._doRequestFocus(isFromPolicy: false); child._nextSibling._previousSibling = child._previousSibling;
} }
child._previousSibling = null;
child._nextSibling = null;
child._parent = null;
child._updateManager(null);
} }
/// If this scope lacks a focus, request that the given node become the focus. void _didChangeFocusChain() {
if (isFirstFocus)
_manager?._markNeedsUpdate();
}
/// Requests that the given node becomes the focus for this scope.
/// ///
/// If the given node is not yet part of the focus tree, then add it as a /// If the given node is currently focused in another scope, the node will
/// child of this node. /// first be unfocused in that scope.
///
/// The node will receive the overall focus if this [isFirstFocus] is true
/// in this scope and all its ancestor scopes. The node is notified that it
/// has received the overall focus in a microtask.
void requestFocus(FocusNode node) {
assert(node != null);
if (_focus == node && listEquals<FocusScopeNode>(_focusPath, _manager?._getCurrentFocusPath()))
return;
_focus?.unfocus();
node._hasKeyboardToken = true;
_setFocus(node);
}
/// If this scope lacks a focus, request that the given node becomes the
/// focus.
/// ///
/// Useful for widgets that wish to grab the focus if no other widget already /// Useful for widgets that wish to grab the focus if no other widget already
/// has the focus. /// has the focus.
/// ///
/// The node is notified that it has received the primary focus in a /// The node is notified that it has received the overall focus in a
/// microtask, so notification may lag the request by up to one frame. /// microtask.
void autofocus(FocusNode node) { void autofocus(FocusNode node) {
if (focusedChild == null) { assert(node != null);
if (node._parent == null) { if (_focus == null) {
_reparent(node); node._hasKeyboardToken = true;
} _setFocus(node);
assert(node.ancestors.contains(this),
'Autofocus was requested for a node that is not a descendant of the scope from which it was requested.');
node.requestFocus();
} }
} }
@override /// Adopts the given node if it is focused in another scope.
void _doRequestFocus({@required bool isFromPolicy}) { ///
assert(isFromPolicy != null); /// A widget that requests that a node is focused should call this method
// Start with the primary focus as the focused child of this scope, if there /// during its `build` method in case the widget is moved from one location
// is one. Otherwise start with this node itself. /// in the tree to another location that has a different focus scope.
FocusNode primaryFocus = focusedChild ?? this; void reparentIfNeeded(FocusNode node) {
// Keep going down through scopes until the ultimately focusable item is assert(node != null);
// found, a scope doesn't have a focusedChild, or a non-scope is if (node._parent == null || node._parent == this)
// encountered. return;
while (primaryFocus is FocusScopeNode && primaryFocus.focusedChild != null) { node.unfocus();
final FocusScopeNode scope = primaryFocus; assert(node._parent == null);
primaryFocus = scope.focusedChild; if (_focus == null)
} _setFocus(node);
if (primaryFocus is FocusScopeNode) { }
// We didn't find a FocusNode at the leaf, so we're focusing the scope.
_markAsDirty(newFocus: primaryFocus); void _setFocus(FocusNode node) {
assert(node != null);
assert(node._parent == null);
assert(_focus == null);
_focus = node;
_focus._parent = this;
_focus._manager = _manager;
_focus._hasKeyboardToken = true;
_focusPath = _getFocusPath();
_didChangeFocusChain();
}
void _resignFocus(FocusNode node) {
assert(node != null);
if (_focus != node)
return;
_focus._parent = null;
_focus._manager = null;
_focus = null;
_didChangeFocusChain();
}
/// Makes the given child the first focus of this scope.
///
/// If the child has another parent scope, the child is first removed from
/// that scope. After this method returns [isFirstFocus] will be true for
/// the child.
void setFirstFocus(FocusScopeNode child) {
assert(child != null);
if (_firstChild == child)
return;
child.detach();
_prepend(child);
assert(child._parent == this);
_didChangeFocusChain();
}
/// Adopts the given scope if it is the first focus of another scope.
///
/// A widget that sets a scope as the first focus of another scope should
/// call this method during its `build` method in case the widget is moved
/// from one location in the tree to another location that has a different
/// focus scope.
///
/// If the given scope is not the first focus of its old parent, the scope
/// is simply detached from its old parent.
void reparentScopeIfNeeded(FocusScopeNode child) {
assert(child != null);
if (child._parent == null || child._parent == this)
return;
if (child.isFirstFocus) {
setFirstFocus(child);
} else { } else {
primaryFocus.requestFocus(); child.detach();
} }
} }
/// Remove this scope from its parent child list.
///
/// This method is safe to call even if this scope does not have a parent.
///
/// A widget that sets a scope as the first focus of another scope should
/// call this method during [State.dispose] to avoid leaving dangling
/// children in their parent scope.
void detach() {
_didChangeFocusChain();
_parent?._remove(this);
assert(_parent == null);
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<FocusNode>('focusedChild', focusedChild, defaultValue: null)); if (_focus != null)
properties.add(DiagnosticsProperty<FocusNode>('focus', _focus));
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> children = <DiagnosticsNode>[];
if (_firstChild != null) {
FocusScopeNode child = _firstChild;
int count = 1;
while (true) {
children.add(child.toDiagnosticsNode(name: 'child $count'));
if (child == _lastChild)
break;
child = child._nextSibling;
count += 1;
}
}
return children;
} }
} }
...@@ -892,175 +418,70 @@ class FocusScopeNode extends FocusNode { ...@@ -892,175 +418,70 @@ class FocusScopeNode extends FocusNode {
/// ///
/// See also: /// See also:
/// ///
/// * [FocusNode], which is a node in the focus tree that can receive focus. /// * [FocusNode], which is a leaf node in the focus tree that can receive
/// * [FocusScopeNode], which is an node in the focus tree used to collect /// focus.
/// subtrees into groups. /// * [FocusScopeNode], which is an interior node in the focus tree.
/// * [Focus.of], which provides the nearest ancestor [FocusNode] for a given /// * [FocusScope.of], which provides the [FocusScopeNode] for a given
/// [BuildContext]. /// [BuildContext].
/// * [FocusScope.of], which provides the nearest ancestor [FocusScopeNode] for class FocusManager {
/// a given [BuildContext].
class FocusManager with DiagnosticableTreeMixin {
/// Creates an object that manages the focus tree. /// Creates an object that manages the focus tree.
/// ///
/// This constructor is rarely called directly. To access the [FocusManager], /// This constructor is rarely called directly. To access the [FocusManager],
/// consider using [WidgetsBinding.focusManager] instead. /// consider using [WidgetsBinding.focusManager] instead.
FocusManager() { FocusManager() {
rootScope._manager = this; rootScope._manager = this;
RawKeyboard.instance.addListener(_handleRawKeyEvent); assert(rootScope._firstChild == null);
assert(rootScope._lastChild == null);
} }
/// The root [FocusScopeNode] in the focus tree. /// The root [FocusScopeNode] in the focus tree.
/// ///
/// This field is rarely used directly. To find the nearest [FocusScopeNode] /// This field is rarely used directly. Instead, to find the
/// for a given [FocusNode], call [FocusNode.nearestScope]. /// [FocusScopeNode] for a given [BuildContext], use [FocusScope.of].
FocusScopeNode get rootScope => _rootScope; final FocusScopeNode rootScope = FocusScopeNode();
FocusScopeNode _rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope');
void _handleRawKeyEvent(RawKeyEvent event) {
// Walk the current focus from the leaf to the root, calling each one's
// onKey on the way up, and if one responds that they handled it, stop.
if (_currentFocus == null) {
return;
}
Iterable<FocusNode> allNodes(FocusNode node) sync* {
yield node;
for (FocusNode ancestor in node.ancestors) {
yield ancestor;
}
}
for (FocusNode node in allNodes(_currentFocus)) {
if (node.onKey != null && node.onKey(node, event)) {
break;
}
}
}
/// Resets the FocusManager to a base state.
///
/// This is used by test infrastructure to reset the state between tests.
/// It is not meant for regular production use.
void reset() {
_currentFocus = null;
_nextFocus = null;
_haveScheduledUpdate = false;
_rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope');
rootScope._manager = this;
}
// The node that currently has the primary focus.
FocusNode _currentFocus; FocusNode _currentFocus;
// The node that has requested to have the primary focus, but hasn't been
// given it yet.
FocusNode _nextFocus;
// The set of nodes that need to notify their listeners of changes at the next
// update.
final Set<FocusNode> _dirtyNodes = <FocusNode>{};
// Called to indicate that the given node is being disposed.
void _willDisposeFocusNode(FocusNode node) { void _willDisposeFocusNode(FocusNode node) {
assert(node != null); assert(node != null);
_willUnfocusNode(node); if (_currentFocus == node)
_dirtyNodes.remove(node);
}
// Called to indicate that the given node is being unfocused, and that any
// pending request to be focused should be canceled.
void _willUnfocusNode(FocusNode node) {
assert(node != null);
if (_currentFocus == node) {
_currentFocus = null; _currentFocus = null;
_dirtyNodes.add(node);
_markNeedsUpdate();
}
if (_nextFocus == node) {
_nextFocus = null;
_dirtyNodes.add(node);
_markNeedsUpdate();
}
} }
// True indicates that there is an update pending.
bool _haveScheduledUpdate = false; bool _haveScheduledUpdate = false;
void _markNeedsUpdate() {
// Request that an update be scheduled, optionally requesting focus for the if (_haveScheduledUpdate)
// given newFocus node.
void _markNeedsUpdate({FocusNode newFocus}) {
// If newFocus isn't specified, then don't mess with _nextFocus, just
// schedule the update.
_nextFocus = newFocus ?? _nextFocus;
if (_haveScheduledUpdate) {
return; return;
}
_haveScheduledUpdate = true; _haveScheduledUpdate = true;
scheduleMicrotask(_applyFocusChange); scheduleMicrotask(_update);
} }
void _applyFocusChange() { FocusNode _findNextFocus() {
FocusScopeNode scope = rootScope;
while (scope._firstChild != null)
scope = scope._firstChild;
return scope._focus;
}
void _update() {
_haveScheduledUpdate = false; _haveScheduledUpdate = false;
final FocusNode nextFocus = _findNextFocus();
if (_currentFocus == nextFocus)
return;
final FocusNode previousFocus = _currentFocus; final FocusNode previousFocus = _currentFocus;
if (_currentFocus == null && _nextFocus == null) { _currentFocus = nextFocus;
_nextFocus = rootScope; previousFocus?._notify();
} _currentFocus?._notify();
if (_nextFocus != null && _nextFocus != _currentFocus) {
_currentFocus = _nextFocus;
final Set<FocusNode> previousPath = previousFocus?.ancestors?.toSet() ?? <FocusNode>{};
final Set<FocusNode> nextPath = _nextFocus.ancestors.toSet();
// Notify nodes that are newly focused.
_dirtyNodes.addAll(nextPath.difference(previousPath));
// Notify nodes that are no longer focused
_dirtyNodes.addAll(previousPath.difference(nextPath));
_nextFocus = null;
}
if (previousFocus != _currentFocus) {
if (previousFocus != null) {
_dirtyNodes.add(previousFocus);
}
if (_currentFocus != null) {
_dirtyNodes.add(_currentFocus);
}
}
for (FocusNode node in _dirtyNodes) {
node._notify();
}
_dirtyNodes.clear();
} }
@override List<FocusScopeNode> _getCurrentFocusPath() => _currentFocus?._parent?._getFocusPath();
List<DiagnosticsNode> debugDescribeChildren() {
return <DiagnosticsNode>[
rootScope.toDiagnosticsNode(name: 'rootScope'),
];
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { String toString() {
properties.add(FlagProperty('haveScheduledUpdate', value: _haveScheduledUpdate, ifTrue: 'UPDATE SCHEDULED')); final String status = _haveScheduledUpdate ? ' UPDATE SCHEDULED' : '';
properties.add(DiagnosticsProperty<FocusNode>('currentFocus', _currentFocus, defaultValue: null)); const String indent = ' ';
return '${describeIdentity(this)}$status\n'
'${indent}currentFocus: $_currentFocus\n'
'${rootScope.toStringDeep(prefixLineOne: indent, prefixOtherLines: indent)}';
} }
} }
/// Returns a text representation of the current focus tree, along with the
/// current attributes on each node.
///
/// Will return an empty string in release builds.
String debugDescribeFocusTree() {
assert(WidgetsBinding.instance != null);
String result;
assert(() {
result = WidgetsBinding.instance.focusManager.toStringDeep();
return true;
}());
return result ?? '';
}
/// Prints a text representation of the current focus tree, along with the
/// current attributes on each node.
///
/// Will do nothing in release builds.
void debugDumpFocusTree() {
assert(() {
debugPrint(debugDescribeFocusTree());
return true;
}());
}
// Copyright 2015 The Chromium Authors. All rights reserved. // Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
import 'inherited_notifier.dart';
/// A widget that manages a [FocusNode] to allow keyboard focus to be given class _FocusScopeMarker extends InheritedWidget {
/// to this widget and its descendants. const _FocusScopeMarker({
/// Key key,
/// When the focus is gained or lost, [onFocusChanged] is called. @required this.node,
/// Widget child,
/// For keyboard events, [onKey] is called if [FocusNode.hasFocus] is true for }) : assert(node != null),
/// this widget's [focusNode], unless a focused descendant's [onKey] callback super(key: key, child: child);
/// returns false when called.
/// final FocusScopeNode node;
/// This widget does not provide any visual indication that the focus has
/// changed. Any desired visual changes should be made when [onFocusChanged] is @override
/// called. bool updateShouldNotify(_FocusScopeMarker oldWidget) {
/// return node != oldWidget.node;
/// To access the [FocusNode] of the nearest ancestor [Focus] widget and }
/// establish a relationship that will rebuild the widget when the focus }
/// changes, use the [Focus.of] and [FocusScope.of] static methods.
/// /// Establishes a scope in which widgets can receive focus.
/// To access the focused state of the nearest [Focus] widget, use
/// [Focus.hasFocus] from a build method, which also establishes a relationship
/// between the calling widget and the [Focus] widget that will rebuild the
/// calling widget when the focus changes.
///
/// Managing a [FocusNode] means managing its lifecycle, listening for changes
/// in focus, and re-parenting it when needed to keep the focus hierarchy in
/// sync with the widget hierarchy. See [FocusNode] for more information about
/// the details of what node management entails if not using a [Focus] widget.
///
/// To collect a sub-tree of nodes into a group, use a [FocusScope].
/// ///
/// {@tool snippet --template=stateful_widget_scaffold} /// The focus tree keeps track of which widget is the user's current focus. The
/// This example shows how to manage focus using the [Focus] and [FocusScope] /// focused widget often listens for keyboard events.
/// widgets. See [FocusNode] for a similar example that doesn't use [Focus] or
/// [FocusScope].
/// ///
/// ```dart imports /// A focus scope does not itself receive focus but instead helps remember
/// import 'package:flutter/services.dart'; /// previous focus states. A scope is currently active when its [node] is the
/// ``` /// first focus of its parent scope. To activate a [FocusScope], either use the
/// [autofocus] property or explicitly make the [node] the first focus in the
/// parent scope:
/// ///
/// ```dart /// ```dart
/// Color _color = Colors.white; /// FocusScope.of(context).setFirstFocus(node);
///
/// bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
/// if (event is RawKeyDownEvent) {
/// print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}');
/// if (event.logicalKey == LogicalKeyboardKey.keyR) {
/// print('Changing color to red.');
/// setState(() {
/// _color = Colors.red;
/// });
/// return true;
/// } else if (event.logicalKey == LogicalKeyboardKey.keyG) {
/// print('Changing color to green.');
/// setState(() {
/// _color = Colors.green;
/// });
/// return true;
/// } else if (event.logicalKey == LogicalKeyboardKey.keyB) {
/// print('Changing color to blue.');
/// setState(() {
/// _color = Colors.blue;
/// });
/// return true;
/// }
/// }
/// return false;
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// final TextTheme textTheme = Theme.of(context).textTheme;
/// return FocusScope(
/// debugLabel: 'Scope',
/// autofocus: true,
/// child: DefaultTextStyle(
/// style: textTheme.display1,
/// child: Focus(
/// onKey: _handleKeyPress,
/// debugLabel: 'Button',
/// child: Builder(
/// builder: (BuildContext context) {
/// final FocusNode focusNode = Focus.of(context);
/// final bool hasFocus = focusNode.hasFocus;
/// return GestureDetector(
/// onTap: () {
/// if (hasFocus) {
/// setState(() {
/// focusNode.unfocus();
/// });
/// } else {
/// setState(() {
/// focusNode.requestFocus();
/// });
/// }
/// },
/// child: Center(
/// child: Container(
/// width: 400,
/// height: 100,
/// alignment: Alignment.center,
/// color: hasFocus ? _color : Colors.white,
/// child: Text(hasFocus ? "I'm in color! Press R,G,B!" : 'Press to focus'),
/// ),
/// ),
/// );
/// },
/// ),
/// ),
/// ),
/// );
/// }
/// ``` /// ```
/// {@end-tool} ///
/// If a [FocusScope] is removed from the widget tree, then the previously
/// focused node will be focused, but only if the [node] is the same [node]
/// object as in the previous frame. To assure this, you can use a GlobalKey to
/// keep the [FocusScope] widget from being rebuilt from one frame to the next,
/// or pass in the [node] from a parent that is not rebuilt. If there is no next
/// sibling, then the parent scope node will be focused.
/// ///
/// See also: /// See also:
/// ///
/// * [FocusNode], which represents a node in the focus hierarchy and /// * [FocusScopeNode], which is the associated node in the focus tree.
/// [FocusNode]'s API documentation includes a detailed explanation of its /// * [FocusNode], which is a leaf node in the focus tree that can receive
/// role in the overall focus system. /// focus.
/// * [FocusScope], a widget that manages a group of focusable widgets using a class FocusScope extends StatefulWidget {
/// [FocusScopeNode]. /// Creates a scope in which widgets can receive focus.
/// * [FocusScopeNode], a node that collects focus nodes into a group for
/// traversal.
/// * [FocusManager], a singleton that manages the primary focus and
/// distributes key events to focused nodes.
class Focus extends StatefulWidget {
/// Creates a widget that manages a [FocusNode].
///
/// The [child] argument is required and must not be null.
/// ///
/// The [autofocus] argument must not be null. /// The [node] argument must not be null.
const Focus({ const FocusScope({
Key key, Key key,
@required this.child, @required this.node,
this.focusNode,
this.autofocus = false, this.autofocus = false,
this.onFocusChange, this.child,
this.onKey, }) : assert(node != null),
this.debugLabel, assert(autofocus != null),
}) : assert(child != null), super(key: key);
assert(autofocus != null),
super(key: key);
/// A debug label for this widget.
///
/// Not used for anything except to be printed in the diagnostic output from
/// [toString] or [toStringDeep]. Also unused if a [focusNode] is provided,
/// since that node can have its own [FocusNode.debugLabel].
///
/// To get a string with the entire tree, call [debugDescribeFocusTree]. To
/// print it to the console call [debugDumpFocusTree].
///
/// Defaults to null.
final String debugLabel;
/// The child widget of this [Focus].
///
/// {@macro flutter.widgets.child}
final Widget child;
/// Handler for keys pressed when this object or one of its children has /// Controls whether this scope is currently active.
/// focus. final FocusScopeNode node;
///
/// Key events are first given to the [FocusNode] that has primary focus, and
/// if its [onKey] method return false, then they are given to each ancestor
/// node up the focus hierarchy in turn. If an event reaches the root of the
/// hierarchy, it is discarded.
///
/// This is not the way to get text input in the manner of a text field: it
/// leaves out support for input method editors, and doesn't support soft
/// keyboards in general. For text input, consider [TextField],
/// [EditableText], or [CupertinoTextField] instead, which do support these
/// things.
final FocusOnKeyCallback onKey;
/// Handler called when the focus changes. /// Whether this scope should attempt to become active when first added to
/// /// the tree.
/// Called with true if this node gains focus, and false if it loses
/// focus.
final ValueChanged<bool> onFocusChange;
/// True if this widget will be selected as the initial focus when no other
/// node in its scope is currently focused.
///
/// Ideally, there is only one [Focus] with autofocus set in each
/// [FocusScope]. If there is more than one [Focus] with autofocus set, then
/// the first one added to the tree will get focus.
final bool autofocus; final bool autofocus;
/// An optional focus node to use as the focus node for this [Focus] widget. /// The widget below this widget in the tree.
/// ///
/// If one is not supplied, then one will be allocated and owned by this /// {@macro flutter.widgets.child}
/// widget. final Widget child;
///
/// Supplying a focus node is sometimes useful if an ancestor to this widget
/// wants to control when this widget has the focus. The owner will be
/// responsible for calling [FocusNode.dispose] on the focus node when it is
/// done with it, but this [Focus] widget will attach/detach and reparent the
/// node when needed.
final FocusNode focusNode;
/// Returns the [focusNode] of the [Focus] that most tightly encloses the given /// Returns the [node] of the [FocusScope] that most tightly encloses the
/// [BuildContext]. /// given [BuildContext].
///
/// If this node doesn't have a [Focus] widget ancestor, then the
/// [FocusManager.rootScope] is returned.
/// ///
/// The [context] argument must not be null. /// The [context] argument must not be null.
static FocusNode of(BuildContext context) { static FocusScopeNode of(BuildContext context) {
assert(context != null); assert(context != null);
final _FocusMarker marker = context.inheritFromWidgetOfExactType(_FocusMarker); final _FocusScopeMarker scope = context.inheritFromWidgetOfExactType(_FocusScopeMarker);
return marker?.notifier ?? context.owner.focusManager.rootScope; return scope?.node ?? context.owner.focusManager.rootScope;
} }
/// Returns true if the nearest enclosing [Focus] widget's node is focused. /// A list of the [FocusScopeNode]s for each [FocusScope] ancestor of
/// the given [BuildContext]. The first element of the list is the
/// nearest ancestor's [FocusScopeNode].
/// ///
/// A convenience method to allow build methods to write: /// The returned list does not include the [FocusManager]'s `rootScope`.
/// `Focus.isAt(context)` to get whether or not the nearest [Focus] or /// Only the [FocusScopeNode]s that belong to [FocusScope] widgets are
/// [FocusScope] above them in the widget hierarchy currently has the keyboard /// returned.
/// focus. ///
static bool isAt(BuildContext context) => Focus.of(context).hasFocus; /// The [context] argument must not be null.
static List<FocusScopeNode> ancestorsOf(BuildContext context) {
@override assert(context != null);
void debugFillProperties(DiagnosticPropertiesBuilder properties) { final List<FocusScopeNode> ancestors = <FocusScopeNode>[];
super.debugFillProperties(properties); while (true) {
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null)); context = context.ancestorInheritedElementForWidgetOfExactType(_FocusScopeMarker);
properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false)); if (context == null)
properties.add(DiagnosticsProperty<FocusScopeNode>('node', focusNode, defaultValue: null)); return ancestors;
final _FocusScopeMarker scope = context.widget;
ancestors.add(scope.node);
context.visitAncestorElements((Element parent) {
context = parent;
return false;
});
}
} }
@override @override
_FocusState createState() => _FocusState(); _FocusScopeState createState() => _FocusScopeState();
} }
class _FocusState extends State<Focus> { class _FocusScopeState extends State<FocusScope> {
FocusNode _internalNode;
FocusNode get node => widget.focusNode ?? _internalNode;
bool _hasFocus;
bool _didAutofocus = false; bool _didAutofocus = false;
FocusAttachment _focusAttachment;
@override
void initState() {
super.initState();
_initNode();
}
void _initNode() {
if (widget.focusNode == null) {
// Only create a new node if the widget doesn't have one.
_internalNode ??= _createNode();
}
_focusAttachment = node.attach(context, onKey: widget.onKey);
_hasFocus = node.hasFocus;
// Add listener even if the _internalNode existed before, since it should
// not be listening now if we're re-using a previous one, because it should
// have already removed its listener.
node.addListener(_handleFocusChanged);
}
FocusNode _createNode() {
return FocusNode(
debugLabel: widget.debugLabel,
);
}
@override
void dispose() {
// Regardless of the node owner, we need to remove it from the tree and stop
// listening to it.
node.removeListener(_handleFocusChanged);
_focusAttachment.detach();
// Don't manage the lifetime of external nodes given to the widget, just the
// internal node.
_internalNode?.dispose();
super.dispose();
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_focusAttachment?.reparent();
if (!_didAutofocus && widget.autofocus) { if (!_didAutofocus && widget.autofocus) {
FocusScope.of(context).autofocus(node); FocusScope.of(context).setFirstFocus(widget.node);
_didAutofocus = true; _didAutofocus = true;
} }
} }
@override @override
void didUpdateWidget(Focus oldWidget) { void dispose() {
super.didUpdateWidget(oldWidget); widget.node.detach();
if (oldWidget.debugLabel != widget.debugLabel && _internalNode != null) { super.dispose();
_internalNode.debugLabel = widget.debugLabel;
}
if ((oldWidget.focusNode == widget.focusNode && oldWidget.onKey == widget.onKey)
|| oldWidget.focusNode == null && widget.focusNode == null) {
// Either there aren't changes, or the _internalNode is already attached
// and being listened to.
return;
}
_focusAttachment.detach();
if (oldWidget.focusNode == null && widget.focusNode != null) {
// We're no longer using the node we were managing. We don't stop managing
// it until dispose, so just detach it: we might re-use it eventually, and
// calling dispose on it here will confuse other widgets that haven't yet
// been notified of a widget change and might still be listening.
_internalNode?.removeListener(_handleFocusChanged);
_focusAttachment = widget.focusNode?.attach(context, onKey: widget.onKey);
widget.focusNode?.addListener(_handleFocusChanged);
} else if (oldWidget.focusNode != null && widget.focusNode == null) {
oldWidget.focusNode?.removeListener(_handleFocusChanged);
// We stopped using the external node, and now we need to manage one.
_initNode();
} else {
// We just switched which node the widget had, so just change what we
// listen to/attach.
oldWidget.focusNode.removeListener(_handleFocusChanged);
widget.focusNode.addListener(_handleFocusChanged);
_focusAttachment = widget.focusNode.attach(context, onKey: widget.onKey);
}
_hasFocus = node.hasFocus;
}
void _handleFocusChanged() {
if (_hasFocus != node.hasFocus) {
setState(() {
_hasFocus = node.hasFocus;
});
if (widget.onFocusChange != null) {
widget.onFocusChange(node.hasFocus);
}
}
}
@override
Widget build(BuildContext context) {
_focusAttachment.reparent();
return _FocusMarker(
node: node,
child: widget.child,
);
}
}
/// A [FocusScope] is similar to a [Focus], but also serves as a scope for other
/// [Focus]s and [FocusScope]s, grouping them together.
///
/// Like [Focus], [FocusScope] provides an [onFocusChange] as a way to be
/// notified when the focus is given to or removed from this widget.
///
/// The [onKey] argument allows specification of a key event handler that is
/// invoked when this node or one of its children has focus. Keys are handed to
/// the primary focused widget first, and then they propagate through the
/// ancestors of that node, stopping if one of them returns true from [onKey],
/// indicating that it has handled the event.
///
/// A [FocusScope] manages a [FocusScopeNode]. Managing a [FocusScopeNode] means
/// managing its lifecycle, listening for changes in focus, and re-parenting it
/// when the widget hierarchy changes. See [FocusNode] and [FocusScopeNode] for
/// more information about the details of what node management entails if not
/// using a [FocusScope] widget.
///
/// See also:
///
/// * [FocusScopeNode], which represents a scope node in the focus hierarchy.
/// * [FocusNode], which represents a node in the focus hierarchy and has an
/// explanation of the focus system.
/// * [Focus], a widget that manages a [FocusNode] and allows easy access to
/// managing focus without having to manage the node.
/// * [FocusManager], a singleton that manages the focus and distributes key
/// events to focused nodes.
class FocusScope extends Focus {
/// Creates a widget that manages a [FocusScopeNode].
///
/// The [child] argument is required and must not be null.
///
/// The [autofocus], and [showDecorations] arguments must not be null.
const FocusScope({
Key key,
FocusNode node,
@required Widget child,
bool autofocus = false,
ValueChanged<bool> onFocusChange,
FocusOnKeyCallback onKey,
String debugLabel,
}) : assert(child != null),
assert(autofocus != null),
super(
key: key,
child: child,
focusNode: node,
autofocus: autofocus,
onFocusChange: onFocusChange,
onKey: onKey,
debugLabel: debugLabel,
);
/// Returns the [FocusScopeNode] of the [FocusScope] that most tightly
/// encloses the given [context].
///
/// If this node doesn't have a [Focus] widget ancestor, then the
/// [FocusManager.rootScope] is returned.
///
/// The [context] argument must not be null.
static FocusScopeNode of(BuildContext context) {
assert(context != null);
final _FocusMarker marker = context.inheritFromWidgetOfExactType(_FocusMarker);
return marker?.notifier?.nearestScope ?? context.owner.focusManager.rootScope;
}
@override
_FocusScopeState createState() => _FocusScopeState();
}
class _FocusScopeState extends _FocusState {
@override
FocusScopeNode _createNode() {
return FocusScopeNode(
debugLabel: widget.debugLabel,
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_focusAttachment.reparent(); FocusScope.of(context).reparentScopeIfNeeded(widget.node);
return Semantics( return Semantics(
explicitChildNodes: true, explicitChildNodes: true,
child: _FocusMarker( child: _FocusScopeMarker(
node: node, node: widget.node,
child: widget.child, child: widget.child,
), ),
); );
} }
} }
// The InheritedWidget marker for Focus and FocusScope.
class _FocusMarker extends InheritedNotifier<FocusNode> {
const _FocusMarker({
Key key,
@required FocusNode node,
@required Widget child,
}) : assert(node != null),
assert(child != null),
super(key: key, notifier: node, child: child);
}
...@@ -2124,7 +2124,7 @@ class BuildOwner { ...@@ -2124,7 +2124,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.
FocusManager focusManager = FocusManager(); final 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(debugLabel: 'Navigator Scope'); final FocusScopeNode focusScopeNode = FocusScopeNode();
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.dispose(); focusScopeNode.detach();
super.dispose(); super.dispose();
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
} }
......
...@@ -7,7 +7,6 @@ import 'package:flutter/services.dart'; ...@@ -7,7 +7,6 @@ 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;
...@@ -113,5 +112,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> { ...@@ -113,5 +112,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> {
} }
@override @override
Widget build(BuildContext context) => Focus(focusNode: widget.focusNode, child: widget.child); Widget build(BuildContext context) => widget.child;
} }
...@@ -583,9 +583,6 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -583,9 +583,6 @@ 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();
...@@ -595,14 +592,12 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -595,14 +592,12 @@ 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);
widget.route._grabFocusIfNeeded(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);
widget.route._grabFocusIfNeeded(focusScopeNode);
} }
@override @override
...@@ -617,12 +612,6 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -617,12 +612,6 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
}); });
} }
@override
void dispose() {
super.dispose();
focusScopeNode.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) {
...@@ -640,7 +629,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -640,7 +629,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: focusScopeNode, // immutable node: widget.route.focusScopeNode, // immutable
child: RepaintBoundary( child: RepaintBoundary(
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _listenable, // immutable animation: _listenable, // immutable
...@@ -898,6 +887,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -898,6 +887,9 @@ 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);
...@@ -905,20 +897,18 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -905,20 +897,18 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation); _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
} }
bool _wantsFocus = false;
void _grabFocusIfNeeded(FocusScopeNode node) {
if (_wantsFocus) {
_wantsFocus = false;
navigator.focusScopeNode.setFirstFocus(node);
}
}
@override @override
TickerFuture didPush() { TickerFuture didPush() {
_wantsFocus = true; navigator.focusScopeNode.setFirstFocus(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,10 +106,7 @@ void main() { ...@@ -106,10 +106,7 @@ 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>[ final List<FocusNode> focusNodes = <FocusNode>[FocusNode(), FocusNode()];
FocusNode(debugLabel: 'Node 1'),
FocusNode(debugLabel: 'Node 2'),
];
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
...@@ -142,10 +139,7 @@ void main() { ...@@ -142,10 +139,7 @@ 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(debugLabel: 'Node 1'), FocusNode(), FocusNode(), FocusNode(), FocusNode(),
FocusNode(debugLabel: 'Node 2'),
FocusNode(debugLabel: 'Node 3'),
FocusNode(debugLabel: 'Node 4'),
]; ];
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -9,14 +9,11 @@ void main() { ...@@ -9,14 +9,11 @@ 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(
MaterialApp( const MaterialApp(
home: Material( home: Material(
child: Center( child: Center(
child: TextField( child: TextField(
focusNode: focusNode,
autofocus: true, autofocus: true,
), ),
), ),
...@@ -133,7 +130,7 @@ void main() { ...@@ -133,7 +130,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,31 +2754,29 @@ void main() { ...@@ -2754,31 +2754,29 @@ void main() {
controller = TextEditingController(); controller = TextEditingController();
}); });
Future<void> setupWidget(WidgetTester tester) async { MaterialApp setupWidget() {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
controller = TextEditingController(); controller = TextEditingController();
await tester.pumpWidget( return MaterialApp(
MaterialApp( home: Material(
home: Material( child: RawKeyboardListener(
child: RawKeyboardListener( focusNode: focusNode,
focusNode: focusNode, onKey: null,
onKey: null, child: TextField(
child: TextField( controller: controller,
controller: controller, maxLines: 3,
maxLines: 3, strutStyle: StrutStyle.disabled,
strutStyle: StrutStyle.disabled,
),
), ),
), ) ,
), ),
); );
focusNode.requestFocus();
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);
...@@ -2791,7 +2789,7 @@ void main() { ...@@ -2791,7 +2789,7 @@ void main() {
}); });
testWidgets('Control Shift test', (WidgetTester tester) async { testWidgets('Control Shift test', (WidgetTester tester) async {
await setupWidget(tester); await tester.pumpWidget(setupWidget());
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);
...@@ -2807,7 +2805,7 @@ void main() { ...@@ -2807,7 +2805,7 @@ void main() {
}); });
testWidgets('Down and up test', (WidgetTester tester) async { testWidgets('Down and up test', (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);
...@@ -2829,7 +2827,7 @@ void main() { ...@@ -2829,7 +2827,7 @@ void main() {
}); });
testWidgets('Down and up test 2', (WidgetTester tester) async { testWidgets('Down and up test 2', (WidgetTester tester) async {
await setupWidget(tester); await tester.pumpWidget(setupWidget());
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);
...@@ -2916,8 +2914,6 @@ void main() { ...@@ -2916,8 +2914,6 @@ 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);
...@@ -2988,8 +2984,6 @@ void main() { ...@@ -2988,8 +2984,6 @@ 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);
...@@ -3099,8 +3093,6 @@ void main() { ...@@ -3099,8 +3093,6 @@ 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);
......
...@@ -16,8 +16,8 @@ import 'editable_text_utils.dart'; ...@@ -16,8 +16,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(debugLabel: 'EditableText Node'); final FocusNode focusNode = FocusNode();
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'EditableText Scope Node'); final FocusScopeNode focusScopeNode = FocusScopeNode();
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);
...@@ -975,9 +975,6 @@ void main() { ...@@ -975,9 +975,6 @@ void main() {
), ),
)); ));
focusNode.requestFocus();
await tester.pump();
expect( expect(
semantics, semantics,
includesNodeWith( includesNodeWith(
...@@ -1535,8 +1532,6 @@ void main() { ...@@ -1535,8 +1532,6 @@ 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(
...@@ -1911,7 +1906,7 @@ void main() { ...@@ -1911,7 +1906,7 @@ void main() {
); );
final GlobalKey<EditableTextState> editableTextKey = final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>(); GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode(debugLabel: 'Test Focus Node'); final FocusNode focusNode = FocusNode();
await tester.pumpWidget(MaterialApp( // So we can show overlays. await tester.pumpWidget(MaterialApp( // So we can show overlays.
home: EditableText( home: EditableText(
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void sendFakeKeyEvent(Map<String, dynamic> data) {
BinaryMessages.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data),
(ByteData data) {},
);
}
void main() {
final GlobalKey widgetKey = GlobalKey();
Future<BuildContext> setupWidget(WidgetTester tester) async {
await tester.pumpWidget(Container(key: widgetKey));
return widgetKey.currentContext;
}
group(FocusNode, () {
testWidgets('Can add children.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusNode parent = FocusNode();
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
parentAttachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent);
expect(child1.parent, equals(parent));
expect(parent.children.first, equals(child1));
expect(parent.children.last, equals(child1));
child2Attachment.reparent(parent: parent);
expect(child1.parent, equals(parent));
expect(child2.parent, equals(parent));
expect(parent.children.first, equals(child1));
expect(parent.children.last, equals(child2));
});
testWidgets('Can remove children.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusNode parent = FocusNode();
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
parentAttachment.reparent(parent: tester.binding.focusManager.rootScope);
child1Attachment.reparent(parent: parent);
child2Attachment.reparent(parent: parent);
expect(child1.parent, equals(parent));
expect(child2.parent, equals(parent));
expect(parent.children.first, equals(child1));
expect(parent.children.last, equals(child2));
child1Attachment.detach();
expect(child1.parent, isNull);
expect(child2.parent, equals(parent));
expect(parent.children.first, equals(child2));
expect(parent.children.last, equals(child2));
child2Attachment.detach();
expect(child1.parent, isNull);
expect(child2.parent, isNull);
expect(parent.children, isEmpty);
});
testWidgets('implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
FocusNode(
debugLabel: 'Label',
).debugFillProperties(builder);
final List<String> description = builder.properties.where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'debugLabel: "Label"',
]);
});
});
group(FocusScopeNode, () {
testWidgets('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
scope.attach(context);
final FocusScopeNode parent = FocusScopeNode(debugLabel: 'Parent');
parent.attach(context);
final FocusScopeNode child1 = FocusScopeNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusScopeNode child2 = FocusScopeNode(debugLabel: 'Child 2');
child2.attach(context);
scope.setFirstFocus(parent);
parent.setFirstFocus(child1);
parent.setFirstFocus(child2);
child1.requestFocus();
await tester.pump();
expect(scope.hasFocus, isFalse);
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(scope.focusedChild, equals(parent));
expect(parent.focusedChild, equals(child1));
child1Attachment.detach();
expect(scope.hasFocus, isFalse);
expect(scope.focusedChild, equals(parent));
});
testWidgets('Removing a node removes it from scope.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode();
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent = FocusNode();
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parentAttachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent);
child2Attachment.reparent(parent: parent);
child1.requestFocus();
await tester.pump();
expect(scope.hasFocus, isTrue);
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(scope.focusedChild, equals(child1));
child1Attachment.detach();
expect(scope.hasFocus, isFalse);
expect(scope.focusedChild, isNull);
});
testWidgets('Can add children to scope and focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode();
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent = FocusNode();
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parentAttachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent);
child2Attachment.reparent(parent: parent);
expect(scope.children.first, equals(parent));
expect(parent.parent, equals(scope));
expect(child1.parent, equals(parent));
expect(child2.parent, equals(parent));
expect(parent.children.first, equals(child1));
expect(parent.children.last, equals(child2));
child1.requestFocus();
await tester.pump();
expect(scope.focusedChild, equals(child1));
expect(parent.hasFocus, isTrue);
expect(parent.hasPrimaryFocus, isFalse);
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(child2.hasFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
child2.requestFocus();
await tester.pump();
expect(scope.focusedChild, equals(child2));
expect(parent.hasFocus, isTrue);
expect(parent.hasPrimaryFocus, isFalse);
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasFocus, isTrue);
expect(child2.hasPrimaryFocus, isTrue);
});
testWidgets('Autofocus works.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent = FocusNode(debugLabel: 'Parent');
final FocusAttachment parentAttachment = parent.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parentAttachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent);
child2Attachment.reparent(parent: parent);
scope.autofocus(child2);
await tester.pump();
expect(scope.focusedChild, equals(child2));
expect(parent.hasFocus, isTrue);
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasFocus, isTrue);
expect(child2.hasPrimaryFocus, isTrue);
child1.requestFocus();
scope.autofocus(child2);
await tester.pump();
expect(scope.focusedChild, equals(child1));
expect(parent.hasFocus, isTrue);
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(child2.hasFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
});
testWidgets('Adding a focusedChild to a scope sets scope as focusedChild in parent scope', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode();
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode();
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: scope1);
child1Attachment.reparent(parent: scope1);
child2Attachment.reparent(parent: scope2);
child2.requestFocus();
await tester.pump();
expect(scope2.focusedChild, equals(child2));
expect(scope1.focusedChild, equals(scope2));
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(child2.hasFocus, isTrue);
expect(child2.hasPrimaryFocus, isTrue);
child1.requestFocus();
await tester.pump();
expect(scope2.focusedChild, equals(child2));
expect(scope1.focusedChild, equals(child1));
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(child2.hasFocus, isFalse);
expect(child2.hasPrimaryFocus, isFalse);
});
testWidgets('Can move node with focus without losing focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope);
parent2Attachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
expect(scope.children.first, equals(parent1));
expect(scope.children.last, equals(parent2));
expect(parent1.parent, equals(scope));
expect(parent2.parent, equals(scope));
expect(child1.parent, equals(parent1));
expect(child2.parent, equals(parent1));
expect(parent1.children.first, equals(child1));
expect(parent1.children.last, equals(child2));
child1.requestFocus();
await tester.pump();
child1Attachment.reparent(parent: parent2);
await tester.pump();
expect(scope.focusedChild, equals(child1));
expect(child1.parent, equals(parent2));
expect(child2.parent, equals(parent1));
expect(parent1.children.first, equals(child2));
expect(parent2.children.first, equals(child1));
});
testWidgets('Can move node between scopes and lose scope focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode()..attach(context);
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode();
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode();
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode();
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode();
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode();
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
child1.requestFocus();
await tester.pump();
expect(scope1.focusedChild, equals(child1));
expect(parent2.children.contains(child1), isFalse);
child1Attachment.reparent(parent: parent2);
await tester.pump();
expect(scope1.focusedChild, isNull);
expect(parent2.children.contains(child1), isTrue);
});
testWidgets('Can move focus between scopes and keep focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode();
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode();
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode();
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode();
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode();
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode();
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode();
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode();
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
child4.requestFocus();
await tester.pump();
child1.requestFocus();
await tester.pump();
expect(child4.hasFocus, isFalse);
expect(child4.hasPrimaryFocus, isFalse);
expect(child1.hasFocus, isTrue);
expect(child1.hasPrimaryFocus, isTrue);
expect(scope1.hasFocus, isTrue);
expect(scope1.hasPrimaryFocus, isFalse);
expect(scope2.hasFocus, isFalse);
expect(scope2.hasPrimaryFocus, isFalse);
expect(parent1.hasFocus, isTrue);
expect(parent2.hasFocus, isFalse);
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child4));
scope2.requestFocus();
await tester.pump();
expect(child4.hasFocus, isTrue);
expect(child4.hasPrimaryFocus, isTrue);
expect(child1.hasFocus, isFalse);
expect(child1.hasPrimaryFocus, isFalse);
expect(scope1.hasFocus, isFalse);
expect(scope1.hasPrimaryFocus, isFalse);
expect(scope2.hasFocus, isTrue);
expect(scope2.hasPrimaryFocus, isFalse);
expect(parent1.hasFocus, isFalse);
expect(parent2.hasFocus, isTrue);
expect(scope1.focusedChild, equals(child1));
expect(scope2.focusedChild, equals(child4));
});
testWidgets('Key handling bubbles up and terminates when handled.', (WidgetTester tester) async {
final Set<FocusNode> receivedAnEvent = <FocusNode>{};
final Set<FocusNode> shouldHandle = <FocusNode>{};
bool handleEvent(FocusNode node, RawKeyEvent event) {
if (shouldHandle.contains(node)) {
receivedAnEvent.add(node);
return true;
}
return false;
}
void sendEvent() {
receivedAnEvent.clear();
sendFakeKeyEvent(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
}
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1');
final FocusAttachment scope1Attachment = scope1.attach(context, onKey: handleEvent);
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'Scope 2');
final FocusAttachment scope2Attachment = scope2.attach(context, onKey: handleEvent);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context, onKey: handleEvent);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context, onKey: handleEvent);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context, onKey: handleEvent);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context, onKey: handleEvent);
final FocusNode child3 = FocusNode(debugLabel: 'Child 3');
final FocusAttachment child3Attachment = child3.attach(context, onKey: handleEvent);
final FocusNode child4 = FocusNode(debugLabel: 'Child 4');
final FocusAttachment child4Attachment = child4.attach(context, onKey: handleEvent);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
child4.requestFocus();
await tester.pump();
shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4});
sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{child4}));
shouldHandle.remove(child4);
sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{parent2}));
shouldHandle.remove(parent2);
sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{scope2}));
shouldHandle.clear();
sendEvent();
expect(receivedAnEvent, isEmpty);
child1.requestFocus();
await tester.pump();
shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4});
sendEvent();
// Since none of the focused nodes handle this event, nothing should
// receive it.
expect(receivedAnEvent, isEmpty);
});
testWidgets('implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
FocusScopeNode(
debugLabel: 'Scope Label',
).debugFillProperties(builder);
final List<String> description = builder.properties.where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'debugLabel: "Scope Label"',
]);
});
testWidgets('debugDescribeFocusTree produces correct output', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1');
final FocusAttachment scope1Attachment = scope1.attach(context);
final FocusScopeNode scope2 = FocusScopeNode(); // No label, Just to test that it works.
final FocusAttachment scope2Attachment = scope2.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(); // No label, Just to test that it works.
final FocusAttachment child2Attachment = child2.attach(context);
final FocusNode child3 = FocusNode(debugLabel: 'Child 3');
final FocusAttachment child3Attachment = child3.attach(context);
final FocusNode child4 = FocusNode(debugLabel: 'Child 4');
final FocusAttachment child4Attachment = child4.attach(context);
scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope1);
parent2Attachment.reparent(parent: scope2);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child3Attachment.reparent(parent: parent2);
child4Attachment.reparent(parent: parent2);
child4.requestFocus();
await tester.pump();
final String description = debugDescribeFocusTree();
expect(
description,
equalsIgnoringHashCodes(
'FocusManager#00000\n'
' │ currentFocus: FocusNode#00000\n'
' │\n'
' └─rootScope: FocusScopeNode#00000\n'
' │ FOCUSED\n'
' │ debugLabel: "Root Focus Scope"\n'
' │ focusedChild: FocusScopeNode#00000\n'
' │\n'
' ├─Child 1: FocusScopeNode#00000\n'
' │ │ context: Container-[GlobalKey#00000]\n'
' │ │ debugLabel: "Scope 1"\n'
' │ │\n'
' │ └─Child 1: FocusNode#00000\n'
' │ │ context: Container-[GlobalKey#00000]\n'
' │ │ debugLabel: "Parent 1"\n'
' │ │\n'
' │ ├─Child 1: FocusNode#00000\n'
' │ │ context: Container-[GlobalKey#00000]\n'
' │ │ debugLabel: "Child 1"\n'
' │ │\n'
' │ └─Child 2: FocusNode#00000\n'
' │ context: Container-[GlobalKey#00000]\n'
' │\n'
' └─Child 2: FocusScopeNode#00000\n'
' │ context: Container-[GlobalKey#00000]\n'
' │ FOCUSED\n'
' │ focusedChild: FocusNode#00000\n'
' │\n'
' └─Child 1: FocusNode#00000\n'
' │ context: Container-[GlobalKey#00000]\n'
' │ FOCUSED\n'
' │ debugLabel: "Parent 2"\n'
' │\n'
' ├─Child 1: FocusNode#00000\n'
' │ context: Container-[GlobalKey#00000]\n'
' │ debugLabel: "Child 3"\n'
' │\n'
' └─Child 2: FocusNode#00000\n'
' context: Container-[GlobalKey#00000]\n'
' FOCUSED\n'
' debugLabel: "Child 4"\n'
));
});
});
}
...@@ -5,25 +5,22 @@ ...@@ -5,25 +5,22 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class TestFocus extends StatefulWidget { class TestFocusable extends StatefulWidget {
const TestFocus({ const TestFocusable({
Key key, Key key,
this.debugLabel,
this.name = 'a', this.name = 'a',
this.autofocus = false, this.autofocus = false,
}) : super(key: key); }) : super(key: key);
final String debugLabel;
final String name; final String name;
final bool autofocus; final bool autofocus;
@override @override
TestFocusState createState() => TestFocusState(); TestFocusableState createState() => TestFocusableState();
} }
class TestFocusState extends State<TestFocus> { class TestFocusableState extends State<TestFocusable> {
FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
FocusAttachment focusAttachment;
bool _didAutofocus = false; bool _didAutofocus = false;
@override @override
...@@ -32,16 +29,9 @@ class TestFocusState extends State<TestFocus> { ...@@ -32,16 +29,9 @@ class TestFocusState extends State<TestFocus> {
super.dispose(); super.dispose();
} }
@override
void initState() {
super.initState();
focusNode = FocusNode(debugLabel: widget.debugLabel);
focusAttachment = focusNode.attach(context);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
focusAttachment.reparent(); FocusScope.of(context).reparentIfNeeded(focusNode);
if (!_didAutofocus && widget.autofocus) { if (!_didAutofocus && widget.autofocus) {
_didAutofocus = true; _didAutofocus = true;
FocusScope.of(context).autofocus(focusNode); FocusScope.of(context).autofocus(focusNode);
...@@ -64,899 +54,676 @@ class TestFocusState extends State<TestFocus> { ...@@ -64,899 +54,676 @@ class TestFocusState extends State<TestFocus> {
} }
void main() { void main() {
group(FocusScope, () { testWidgets('Can focus', (WidgetTester tester) async {
testWidgets('Can focus', (WidgetTester tester) async { final GlobalKey<TestFocusableState> key = GlobalKey();
final GlobalKey<TestFocusState> key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
TestFocus(key: key, name: 'a'), TestFocusable(key: key, name: 'a'),
); );
expect(key.currentState.focusNode.hasFocus, isFalse); expect(key.currentState.focusNode.hasFocus, isFalse);
FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode); FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isTrue); expect(key.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
}); });
testWidgets('Can unfocus', (WidgetTester tester) async { testWidgets('Can unfocus', (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusableState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusableState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
TestFocus(key: keyA, name: 'a'), TestFocusable(key: keyA, name: 'a'),
TestFocus(key: keyB, name: 'b'), TestFocusable(key: keyB, name: 'b'),
], ],
), ),
); );
expect(keyA.currentState.focusNode.hasFocus, isFalse); expect(keyA.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget); expect(find.text('a'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(keyA.currentState.focusNode.hasFocus, isTrue); expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
// Set focus to the "B" node to unfocus the "A" node. // Set focus to the "B" node to unfocus the "A" node.
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(keyA.currentState.focusNode.hasFocus, isFalse); expect(keyA.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget); expect(find.text('a'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isTrue); expect(keyB.currentState.focusNode.hasFocus, isTrue);
expect(find.text('B FOCUSED'), findsOneWidget); expect(find.text('B FOCUSED'), findsOneWidget);
}); });
testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async { testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusableState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusableState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
TestFocus( TestFocusable(
key: keyA, key: keyA,
name: 'a', name: 'a',
autofocus: true, autofocus: true,
),
TestFocus(
key: keyB,
name: 'b',
),
],
),
);
// Autofocus is delayed one frame.
await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.tap(find.text('A FOCUSED'));
await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.tap(find.text('b'));
await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isTrue);
expect(find.text('B FOCUSED'), findsOneWidget);
await tester.tap(find.text('a'));
await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
});
// This moves a focus node first into a focus scope that is added to its
// parent, and then out of that focus scope again.
testWidgets('Can move focus in and out of FocusScope', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node');
final GlobalKey<TestFocusState> key = GlobalKey();
// Initially create the focus inside of the parent FocusScope.
await tester.pumpWidget(
FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope,
autofocus: true,
child: Column(
children: <Widget>[
TestFocus(
key: key,
name: 'a',
debugLabel: 'Child',
),
],
),
),
);
expect(key.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget);
FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode);
await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(parentFocusScope, hasAGoodToStringDeep);
expect(
parentFocusScope.toStringDeep(),
equalsIgnoringHashCodes('FocusScopeNode#00000\n'
' │ context: FocusScope\n'
' │ FOCUSED\n'
' │ debugLabel: "Parent Scope Node"\n'
' │ focusedChild: FocusNode#00000\n'
' │\n'
' └─Child 1: FocusNode#00000\n'
' context: TestFocus-[LabeledGlobalKey<TestFocusState>#00000]\n'
' FOCUSED\n'
' debugLabel: "Child"\n'),
);
expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep);
expect(
WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes('FocusScopeNode#00000\n'
' │ FOCUSED\n'
' │ debugLabel: "Root Focus Scope"\n'
' │ focusedChild: FocusScopeNode#00000\n'
' │\n'
' └─Child 1: FocusScopeNode#00000\n'
' │ context: FocusScope\n'
' │ FOCUSED\n'
' │ debugLabel: "Parent Scope Node"\n'
' │ focusedChild: FocusNode#00000\n'
' │\n'
' └─Child 1: FocusNode#00000\n'
' context: TestFocus-[LabeledGlobalKey<TestFocusState>#00000]\n'
' FOCUSED\n'
' debugLabel: "Child"\n'),
);
// Add the child focus scope to the focus tree.
final FocusAttachment childAttachment = childFocusScope.attach(key.currentContext);
parentFocusScope.setFirstFocus(childFocusScope);
await tester.pumpAndSettle();
expect(childFocusScope.isFirstFocus, isTrue);
// Now add the child focus scope with no child focusable in it to the tree.
await tester.pumpWidget(
FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope,
child: Column(
children: <Widget>[
TestFocus(
key: key,
debugLabel: 'Child',
),
FocusScope(
debugLabel: 'Child Scope',
node: childFocusScope,
child: Container(),
),
],
),
),
);
expect(key.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget);
// Now move the existing focus node into the child focus scope.
await tester.pumpWidget(
FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope,
child: Column(
children: <Widget>[
FocusScope(
debugLabel: 'Child Scope',
node: childFocusScope,
child: TestFocus(
key: key,
debugLabel: 'Child',
),
),
],
), ),
), TestFocusable(
); key: keyB,
name: 'b',
await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
// Now remove the child focus scope.
await tester.pumpWidget(
FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope,
child: Column(
children: <Widget>[
TestFocus(
key: key,
debugLabel: 'Child',
),
],
), ),
],
),
);
// Autofocus is delayed one frame.
await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.tap(find.text('A FOCUSED'));
await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.tap(find.text('b'));
await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isTrue);
expect(find.text('B FOCUSED'), findsOneWidget);
await tester.tap(find.text('a'));
await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
});
// This moves a focus node first into a focus scope that is added to its
// parent, and then out of that focus scope again.
testWidgets('Can move focus in and out of FocusScope', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope = FocusScopeNode();
final FocusScopeNode childFocusScope = FocusScopeNode();
final GlobalKey<TestFocusableState> key = GlobalKey();
// Initially create the focus inside of the parent FocusScope.
await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
autofocus: true,
child: Column(
children: <Widget>[
TestFocusable(key: key, name: 'a'),
],
), ),
); ),
);
await tester.pumpAndSettle(); expect(key.currentState.focusNode.hasFocus, isFalse);
expect(key.currentState.focusNode.hasFocus, isTrue); expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsOneWidget); FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode);
await tester.pumpAndSettle();
// Must detach the child because we had to attach it in order to call expect(key.currentState.focusNode.hasFocus, isTrue);
// setFirstFocus before adding to the widget. expect(find.text('A FOCUSED'), findsOneWidget);
childAttachment.detach();
});
// Arguably, this isn't correct behavior, but it is what happens now. expect(parentFocusScope, hasAGoodToStringDeep);
testWidgets("Removing focused widget doesn't move focus to next widget", (WidgetTester tester) async { expect(
final GlobalKey<TestFocusState> keyA = GlobalKey(); parentFocusScope.toStringDeep(minLevel: DiagnosticLevel.info),
final GlobalKey<TestFocusState> keyB = GlobalKey(); equalsIgnoringHashCodes('FocusScopeNode#00000\n'
' focus: FocusNode#00000(FOCUSED)\n'),
);
await tester.pumpWidget( expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep);
Column( expect(
WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes('FocusScopeNode#00000\n'
' └─child 1: FocusScopeNode#00000\n'
' focus: FocusNode#00000(FOCUSED)\n'),
);
// Add the child focus scope to the focus tree.
parentFocusScope.setFirstFocus(childFocusScope);
expect(childFocusScope.isFirstFocus, isTrue);
// Now add the child focus scope with no focus node in it to the tree.
await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
child: Column(
children: <Widget>[ children: <Widget>[
TestFocus( TestFocusable(key: key),
key: keyA, FocusScope(
name: 'a', node: childFocusScope,
), child: Container(),
TestFocus(
key: keyB,
name: 'b',
), ),
], ],
), ),
); ),
);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); expect(key.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget);
await tester.pumpAndSettle(); // Now move the existing focus node into the child focus scope.
await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
child: Column(
children: <Widget>[
FocusScope(
node: childFocusScope,
child: TestFocusable(key: key),
),
],
),
),
);
await tester.pumpAndSettle();
expect(keyA.currentState.focusNode.hasFocus, isTrue); expect(key.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.pumpWidget( // Now remove the child focus scope.
Column( await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
child: Column(
children: <Widget>[ children: <Widget>[
TestFocus( TestFocusable(key: key),
key: keyB,
name: 'b',
),
], ],
), ),
); ),
);
await tester.pump(); await tester.pumpAndSettle();
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(key.currentState.focusNode.hasFocus, isTrue);
expect(find.text('b'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
}); });
testWidgets('Adding a new FocusScope attaches the child it to its parent.', (WidgetTester tester) async { // Arguably, this isn't correct behavior, but it is what happens now.
final GlobalKey<TestFocusState> keyA = GlobalKey(); testWidgets("Removing focused widget doesn't move focus to next widget", (WidgetTester tester) async {
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); final GlobalKey<TestFocusableState> keyA = GlobalKey();
final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node'); final GlobalKey<TestFocusableState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( Column(
node: childFocusScope, children: <Widget>[
child: TestFocus( TestFocusable(
debugLabel: 'Child',
key: keyA, key: keyA,
name: 'a',
), ),
), TestFocusable(
); key: keyB,
name: 'b',
),
],
),
);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
expect(FocusScope.of(keyA.currentContext), equals(childFocusScope));
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(keyA.currentState.focusNode.hasFocus, isTrue); expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
expect(childFocusScope.isFirstFocus, isTrue); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( Column(
node: parentFocusScope, children: <Widget>[
child: FocusScope( TestFocusable(
node: childFocusScope, key: keyB,
child: TestFocus( name: 'b',
debugLabel: 'Child',
key: keyA,
),
),
),
);
await tester.pump();
expect(childFocusScope.isFirstFocus, isTrue);
// Node keeps it's focus when moved to the new scope.
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
});
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope');
await tester.pumpWidget(
FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope,
autofocus: true,
child: Column(
children: <Widget>[
TestFocus(
debugLabel: 'Widget A',
key: keyA,
name: 'a',
),
TestFocus(
debugLabel: 'Widget B',
key: keyB,
name: 'b',
),
],
),
),
);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
final FocusScopeNode scope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(scope);
await tester.pumpAndSettle();
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
child: Column(
children: <Widget>[
TestFocus(
key: keyB,
name: 'b',
),
],
), ),
), ],
); ),
);
await tester.pump(); await tester.pump();
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
}); });
testWidgets('Removing a FocusScope removes its node from the tree', (WidgetTester tester) async { testWidgets('Adding a new FocusScope attaches the child it to its parent.', (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusableState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode();
final GlobalKey<TestFocusState> scopeKeyA = GlobalKey(); final FocusScopeNode childFocusScope = FocusScopeNode();
final GlobalKey<TestFocusState> scopeKeyB = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope');
// This checks both FocusScopes that have their own nodes, as well as those await tester.pumpWidget(
// that use external nodes. FocusScope(
await tester.pumpWidget( node: childFocusScope,
Column( child: TestFocusable(
children: <Widget>[ key: keyA,
FocusScope( name: 'a',
key: scopeKeyA,
node: parentFocusScope,
child: Column(
children: <Widget>[
TestFocus(
debugLabel: 'Child A',
key: keyA,
name: 'a',
),
],
),
),
FocusScope(
key: scopeKeyB,
child: Column(
children: <Widget>[
TestFocus(
debugLabel: 'Child B',
key: keyB,
name: 'b',
),
],
),
),
],
), ),
); ),
);
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(FocusScope.of(keyA.currentContext).isFirstFocus, isTrue); expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(keyA.currentState.focusNode.hasFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('A FOCUSED'), findsOneWidget); expect(childFocusScope.isFirstFocus, isTrue);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.pumpWidget(Container()); await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
child: FocusScope(
node: childFocusScope,
child: TestFocusable(
key: keyA,
name: 'a',
),
),
),
);
await tester.pump();
expect(WidgetsBinding.instance.focusManager.rootScope.children, isEmpty); expect(childFocusScope.isFirstFocus, isTrue);
}); expect(keyA.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget);
});
// Arguably, this isn't correct behavior, but it is what happens now. // Arguably, this isn't correct behavior, but it is what happens now.
testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { testWidgets("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusableState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusableState> keyB = GlobalKey();
final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1'); final FocusScopeNode parentFocusScope = FocusScopeNode();
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
await tester.pumpWidget( await tester.pumpWidget(
Column( FocusScope(
node: parentFocusScope,
autofocus: true,
child: Column(
children: <Widget>[ children: <Widget>[
FocusScope( TestFocusable(
node: parentFocusScope1, key: keyA,
child: Column( name: 'a',
children: <Widget>[
TestFocus(
debugLabel: 'Child A',
key: keyA,
name: 'a',
),
],
),
),
FocusScope(
node: parentFocusScope2,
child: Column(
children: <Widget>[
TestFocus(
debugLabel: 'Child B',
key: keyB,
name: 'b',
),
],
),
), ),
], TestFocusable(
), key: keyB,
); name: 'b',
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
await tester.pumpAndSettle();
expect(FocusScope.of(keyA.currentContext).isFirstFocus, isTrue);
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
// If the FocusScope widgets are not pinned with GlobalKeys, then the first
// one remains and gets its guts replaced with the parentFocusScope2 and the
// "B" test widget, and in the process, the focus manager loses track of the
// focus.
await tester.pumpWidget(
Column(
children: <Widget>[
FocusScope(
node: parentFocusScope2,
child: Column(
children: <Widget>[
TestFocus(
key: keyB,
name: 'b',
autofocus: true,
),
],
),
), ),
], ],
), ),
); ),
await tester.pump(); );
expect(keyB.currentState.focusNode.hasFocus, isFalse); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
expect(find.text('b'), findsOneWidget); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
});
testWidgets('Moving widget from one scope to another retains focus', (WidgetTester tester) async { await tester.pumpAndSettle();
final FocusScopeNode parentFocusScope1 = FocusScopeNode();
final FocusScopeNode parentFocusScope2 = FocusScopeNode();
final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey();
await tester.pumpWidget( expect(keyA.currentState.focusNode.hasFocus, isTrue);
Column( expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.pumpWidget(
FocusScope(
node: parentFocusScope,
child: Column(
children: <Widget>[ children: <Widget>[
FocusScope( TestFocusable(
node: parentFocusScope1, key: keyB,
child: Column( name: 'b',
children: <Widget>[
TestFocus(
key: keyA,
name: 'a',
),
],
),
),
FocusScope(
node: parentFocusScope2,
child: Column(
children: <Widget>[
TestFocus(
key: keyB,
name: 'b',
),
],
),
), ),
], ],
), ),
); ),
);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); await tester.pump();
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
await tester.pumpAndSettle(); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
});
expect(keyA.currentState.focusNode.hasFocus, isTrue); // By "pinned", it means kept in the tree by a GlobalKey.
expect(find.text('A FOCUSED'), findsOneWidget); testWidgets('Removing pinned focused scope moves focus to focused widget within next FocusScope', (WidgetTester tester) async {
expect(keyB.currentState.focusNode.hasFocus, isFalse); final GlobalKey<TestFocusableState> keyA = GlobalKey();
expect(find.text('b'), findsOneWidget); final GlobalKey<TestFocusableState> keyB = GlobalKey();
final GlobalKey<TestFocusableState> scopeKeyA = GlobalKey();
final GlobalKey<TestFocusableState> scopeKeyB = GlobalKey();
final FocusScopeNode parentFocusScope1 = FocusScopeNode();
final FocusScopeNode parentFocusScope2 = FocusScopeNode();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
FocusScope( FocusScope(
node: parentFocusScope1, key: scopeKeyA,
child: Column( node: parentFocusScope1,
children: <Widget>[ child: Column(
TestFocus( children: <Widget>[
key: keyB, TestFocusable(
name: 'b', key: keyA,
), name: 'a',
], ),
), ],
), ),
FocusScope( ),
node: parentFocusScope2, FocusScope(
child: Column( key: scopeKeyB,
children: <Widget>[ node: parentFocusScope2,
TestFocus( child: Column(
key: keyA, children: <Widget>[
name: 'a', TestFocusable(
), key: keyB,
], name: 'b',
), ),
],
), ),
], ),
), ],
); ),
);
await tester.pump(); FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyB.currentContext));
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
expect(keyA.currentState.focusNode.hasFocus, isTrue); await tester.pumpAndSettle();
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
});
testWidgets('Moving FocusScopeNodes retains focus', (WidgetTester tester) async { expect(FocusScope.of(keyA.currentContext).isFirstFocus, isTrue);
final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1'); expect(keyA.currentState.focusNode.hasFocus, isTrue);
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Scope 2'); expect(find.text('A FOCUSED'), findsOneWidget);
final GlobalKey<TestFocusState> keyA = GlobalKey(); expect(keyB.currentState.focusNode.hasFocus, isFalse);
final GlobalKey<TestFocusState> keyB = GlobalKey(); expect(find.text('b'), findsOneWidget);
await tester.pumpWidget( // Since the FocusScope widgets are pinned with GlobalKeys, when the first
Column( // one gets removed, the second one stays registered with the focus
children: <Widget>[ // manager and ends up getting the focus since it remains as part of the
FocusScope( // focus tree.
node: parentFocusScope1, await tester.pumpWidget(
child: Column( Column(
children: <Widget>[ children: <Widget>[
TestFocus( FocusScope(
debugLabel: 'Child A', key: scopeKeyB,
key: keyA, node: parentFocusScope2,
name: 'a', child: Column(
), children: <Widget>[
], TestFocusable(
), key: keyB,
), name: 'b',
FocusScope( autofocus: true,
node: parentFocusScope2, ),
child: Column( ],
children: <Widget>[
TestFocus(
debugLabel: 'Child B',
key: keyB,
name: 'b',
),
],
),
), ),
], ),
), ],
); ),
);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); await tester.pump();
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
await tester.pumpAndSettle(); expect(keyB.currentState.focusNode.hasFocus, isTrue);
expect(find.text('B FOCUSED'), findsOneWidget);
});
expect(keyA.currentState.focusNode.hasFocus, isTrue); // Arguably, this isn't correct behavior, but it is what happens now.
expect(find.text('A FOCUSED'), findsOneWidget); testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async {
expect(keyB.currentState.focusNode.hasFocus, isFalse); final GlobalKey<TestFocusableState> keyA = GlobalKey();
expect(find.text('b'), findsOneWidget); final GlobalKey<TestFocusableState> keyB = GlobalKey();
final FocusScopeNode parentFocusScope1 = FocusScopeNode();
final FocusScopeNode parentFocusScope2 = FocusScopeNode();
// This just swaps the FocusScopeNodes that the FocusScopes have in them. await tester.pumpWidget(
await tester.pumpWidget( Column(
Column( children: <Widget>[
children: <Widget>[ FocusScope(
FocusScope( node: parentFocusScope1,
node: parentFocusScope2, child: Column(
child: Column( children: <Widget>[
children: <Widget>[ TestFocusable(
TestFocus( key: keyA,
debugLabel: 'Child A', name: 'a',
key: keyA, ),
name: 'a', ],
),
],
),
), ),
FocusScope( ),
node: parentFocusScope1, FocusScope(
child: Column( node: parentFocusScope2,
children: <Widget>[ child: Column(
TestFocus( children: <Widget>[
debugLabel: 'Child B', TestFocusable(
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
], ],
),
), ),
], ),
), ],
); ),
);
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyB.currentContext));
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
await tester.pump(); await tester.pumpAndSettle();
expect(keyA.currentState.focusNode.hasFocus, isTrue); expect(FocusScope.of(keyA.currentContext).isFirstFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget); expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget); expect(keyB.currentState.focusNode.hasFocus, isFalse);
}); expect(find.text('b'), findsOneWidget);
// If the FocusScope widgets are not pinned with GlobalKeys, then the first
// one remains and gets its guts replaced with the parentFocusScope2 and the
// "B" test widget, and in the process, the focus manager loses track of the
// focus.
await tester.pumpWidget(
Column(
children: <Widget>[
FocusScope(
node: parentFocusScope2,
child: Column(
children: <Widget>[
TestFocusable(
key: keyB,
name: 'b',
autofocus: true,
),
],
),
),
],
),
);
await tester.pump();
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
}); });
group(Focus, () {
testWidgets('Focus.of stops at the nearest FocusScope.', (WidgetTester tester) async { // Arguably, this isn't correct behavior, but it is what happens now.
final GlobalKey key1 = GlobalKey(debugLabel: '1'); testWidgets('Moving widget from one scope to another does not retain focus', (WidgetTester tester) async {
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusScopeNode parentFocusScope1 = FocusScopeNode();
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final FocusScopeNode parentFocusScope2 = FocusScopeNode();
final GlobalKey key4 = GlobalKey(debugLabel: '4'); final GlobalKey<TestFocusableState> keyA = GlobalKey();
final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey<TestFocusableState> keyB = GlobalKey();
final GlobalKey key6 = GlobalKey(debugLabel: '6');
await tester.pumpWidget( await tester.pumpWidget(
Focus( Column(
key: key1, children: <Widget>[
debugLabel: 'Key 1', FocusScope(
child: Container( node: parentFocusScope1,
key: key2, child: Column(
child: Focus( children: <Widget>[
debugLabel: 'Key 3', TestFocusable(
key: key3, key: keyA,
child: Container( name: 'a',
key: key4,
child: Focus(
debugLabel: 'Key 5',
key: key5,
child: Container(
key: key6,
),
), ),
), ],
), ),
), ),
), FocusScope(
); node: parentFocusScope2,
final Element element1 = tester.element(find.byKey(key1)); child: Column(
final Element element2 = tester.element(find.byKey(key2)); children: <Widget>[
final Element element3 = tester.element(find.byKey(key3)); TestFocusable(
final Element element4 = tester.element(find.byKey(key4)); key: keyB,
final Element element5 = tester.element(find.byKey(key5)); name: 'b',
final Element element6 = tester.element(find.byKey(key6));
final FocusNode root = element1.owner.focusManager.rootScope;
expect(Focus.of(element1), equals(root));
expect(Focus.of(element2).parent, equals(root));
expect(Focus.of(element3).parent, equals(root));
expect(Focus.of(element4).parent.parent, equals(root));
expect(Focus.of(element5).parent.parent, equals(root));
expect(Focus.of(element6).parent.parent.parent, equals(root));
});
testWidgets('Can traverse Focus children.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3');
final GlobalKey key4 = GlobalKey(debugLabel: '4');
final GlobalKey key5 = GlobalKey(debugLabel: '5');
final GlobalKey key6 = GlobalKey(debugLabel: '6');
final GlobalKey key7 = GlobalKey(debugLabel: '7');
final GlobalKey key8 = GlobalKey(debugLabel: '8');
await tester.pumpWidget(
Focus(
child: Column(
key: key1,
children: <Widget>[
Focus(
key: key2,
child: Container(
child: Focus(
key: key3,
child: Container(),
),
), ),
), ],
Focus( ),
key: key4, ),
child: Container( ],
child: Focus( ),
key: key5, );
child: Container(),
), FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
await tester.pumpAndSettle();
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.pumpWidget(
Column(
children: <Widget>[
FocusScope(
node: parentFocusScope1,
child: Column(
children: <Widget>[
TestFocusable(
key: keyB,
name: 'b',
), ),
), ],
Focus( ),
key: key6, ),
child: Column( FocusScope(
children: <Widget>[ node: parentFocusScope2,
Focus( child: Column(
key: key7, children: <Widget>[
child: Container(), TestFocusable(
), key: keyA,
Focus( name: 'a',
key: key8,
child: Container(),
),
],
), ),
), ],
], ),
), ),
), ],
); ),
);
final Element firstScope = tester.element(find.byKey(key1));
final List<FocusNode> nodes = <FocusNode>[];
final List<Key> keys = <Key>[];
bool visitor(FocusNode node) {
nodes.add(node);
keys.add(node.context.widget.key);
return true;
}
await tester.pump();
Focus.of(firstScope).descendants.forEach(visitor);
expect(nodes.length, equals(7));
expect(keys.length, equals(7));
// Depth first.
expect(keys, equals(<Key>[key3, key2, key5, key4, key7, key8, key6]));
// Just traverses a sub-tree.
final Element secondScope = tester.element(find.byKey(key7));
nodes.clear();
keys.clear();
Focus.of(secondScope).descendants.forEach(visitor);
expect(nodes.length, equals(2));
expect(keys, equals(<Key>[key7, key8]));
});
testWidgets('Can set focus.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
bool gotFocus;
await tester.pumpWidget(
Focus(
onFocusChange: (bool focused) => gotFocus = focused,
child: Container(key: key1),
),
);
final Element firstNode = tester.element(find.byKey(key1));
final FocusNode node = Focus.of(firstNode);
node.requestFocus();
await tester.pump();
expect(gotFocus, isTrue);
expect(node.hasFocus, isTrue);
});
testWidgets('Can focus root node.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
await tester.pumpWidget(
Focus(
key: key1,
child: Container(),
),
);
final Element firstElement = tester.element(find.byKey(key1));
final FocusNode rootNode = Focus.of(firstElement);
rootNode.requestFocus();
await tester.pump(); await tester.pump();
expect(rootNode.hasFocus, isTrue); expect(keyA.currentState.focusNode.hasFocus, isFalse);
expect(rootNode, equals(firstElement.owner.focusManager.rootScope)); expect(find.text('a'), findsOneWidget);
}); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
}); });
testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); // Arguably, this isn't correct behavior, but it is what happens now.
bool gotFocus; testWidgets('Moving FocusScopeNodes does not retain focus', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope1 = FocusScopeNode();
final FocusScopeNode parentFocusScope2 = FocusScopeNode();
final GlobalKey<TestFocusableState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( Column(
child: Focus( children: <Widget>[
onFocusChange: (bool focused) => gotFocus = focused, FocusScope(
child: Container(key: key1), node: parentFocusScope1,
), child: Column(
children: <Widget>[
TestFocusable(
key: keyA,
name: 'a',
),
],
),
),
FocusScope(
node: parentFocusScope2,
child: Column(
children: <Widget>[
TestFocusable(
key: keyB,
name: 'b',
),
],
),
),
],
), ),
); );
final Element firstNode = tester.element(find.byKey(key1)); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
final FocusNode node = Focus.of(firstNode); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
node.requestFocus();
await tester.pump(); await tester.pumpAndSettle();
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
expect(gotFocus, isTrue); // This just swaps the FocusScopeNodes that the FocusScopes have in them.
expect(node.hasFocus, isTrue); await tester.pumpWidget(
Column(
children: <Widget>[
FocusScope(
node: parentFocusScope2,
child: Column(
children: <Widget>[
TestFocusable(
key: keyA,
name: 'a',
),
],
),
),
FocusScope(
node: parentFocusScope1,
child: Column(
children: <Widget>[
TestFocusable(
key: keyB,
name: 'b',
),
],
),
),
],
),
);
await tester.pumpWidget(Container()); await tester.pump();
expect(WidgetsBinding.instance.focusManager.rootScope.descendants, isEmpty); expect(keyA.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
}); });
} }
...@@ -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,15 +29,13 @@ void main() { ...@@ -29,15 +29,13 @@ void main() {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget( await tester.pumpWidget(RawKeyboardListener(
RawKeyboardListener( focusNode: focusNode,
focusNode: focusNode, onKey: events.add,
onKey: events.add, child: Container(),
child: Container(), ));
),
);
focusNode.requestFocus(); tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ sendFakeKeyEvent(<String, dynamic>{
...@@ -67,15 +65,13 @@ void main() { ...@@ -67,15 +65,13 @@ void main() {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget( await tester.pumpWidget(RawKeyboardListener(
RawKeyboardListener( focusNode: focusNode,
focusNode: focusNode, onKey: events.add,
onKey: events.add, child: Container(),
child: Container(), ));
),
);
focusNode.requestFocus(); tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ sendFakeKeyEvent(<String, dynamic>{
......
...@@ -692,7 +692,6 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -692,7 +692,6 @@ 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