Unverified Commit 4218c0bc authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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

Implements focus traversal for desktop platforms, including re-implementing the existing focus manager and focus tree.

This implements a Focus widget that can be put into a widget tree to allow input focus to be given to a particular part of a widget tree.

It incorporates with the existing FocusScope and FocusNode infrastructure, and has minimal breakage to the API, although FocusScope.reparentIfNeeded is removed, replaced by a call to FocusAttachment.reparent(), so this is a breaking change:

FocusScopeNodes must now be attached to the focus tree using FocusScopeNode.attach, which takes a context and an optional onKey callback, and returns a FocusAttachment that should be kept by the widget that hosts the FocusScopeNode. This is necessary because of the need to make sure that the focus tree reflects the widget hierarchy.

Callers that used to call FocusScope(context).reparentIfNeeded in their build method will call reparent  on a FocusAttachment instead, which they will obtain by calling FocusScopeNode.attach in their initState method. Widgets that own FocusNodes will need to call dispose on the focus node in their dispose method.

Addresses #11344, #1608, #13264, and #1678
Fixes #30084
Fixes #26704
parent 37bc48f2
...@@ -62,7 +62,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> { ...@@ -62,7 +62,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
if (!_focusNode.hasFocus) { if (!_focusNode.hasFocus) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
FocusScope.of(context).requestFocus(_focusNode); _focusNode.requestFocus();
}, },
child: Text('Tap to focus', style: textTheme.display1), child: Text('Tap to focus', style: textTheme.display1),
); );
......
...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget { ...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: _title, title: _title,
home: Scaffold( home: Scaffold(
appBar: AppBar(title: Text(_title)), appBar: AppBar(title: const Text(_title)),
body: MyStatefulWidget(), body: MyStatefulWidget(),
), ),
); );
......
...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget { ...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: _title, title: _title,
home: Scaffold( home: Scaffold(
appBar: AppBar(title: Text(_title)), appBar: AppBar(title: const Text(_title)),
body: MyStatelessWidget(), body: MyStatelessWidget(),
), ),
); );
......
...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart'; ...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
/// onTap: () { /// onTap: () {
/// FocusScope.of(context).requestFocus(_focusNode); /// FocusScope.of(context).requestFocus(_focusNode);
/// }, /// },
/// child: Text('Tap to focus'), /// child: const Text('Tap to focus'),
/// ); /// );
/// } /// }
/// return Text(_message ?? 'Press a key'); /// return Text(_message ?? 'Press a key');
......
...@@ -304,7 +304,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { ...@@ -304,7 +304,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
tabs = List<Widget>(widget.tabNumber); tabs = List<Widget>(widget.tabNumber);
tabFocusNodes = List<FocusScopeNode>.generate( tabFocusNodes = List<FocusScopeNode>.generate(
widget.tabNumber, widget.tabNumber,
(int index) => FocusScopeNode(), (int index) => FocusScopeNode(debugLabel: 'Tab Focus Scope $index'),
); );
} }
...@@ -327,7 +327,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { ...@@ -327,7 +327,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
@override @override
void dispose() { void dispose() {
for (FocusScopeNode focusScopeNode in tabFocusNodes) { for (FocusScopeNode focusScopeNode in tabFocusNodes) {
focusScopeNode.detach(); focusScopeNode.dispose();
} }
super.dispose(); super.dispose();
} }
......
...@@ -191,7 +191,7 @@ abstract class SearchDelegate<T> { ...@@ -191,7 +191,7 @@ abstract class SearchDelegate<T> {
/// ///
/// * [showSuggestions] to show the search suggestions again. /// * [showSuggestions] to show the search suggestions again.
void showResults(BuildContext context) { void showResults(BuildContext context) {
_focusNode.unfocus(); _focusNode?.unfocus();
_currentBody = _SearchBody.results; _currentBody = _SearchBody.results;
} }
...@@ -208,7 +208,8 @@ abstract class SearchDelegate<T> { ...@@ -208,7 +208,8 @@ abstract class SearchDelegate<T> {
/// ///
/// * [showResults] to show the search results. /// * [showResults] to show the search results.
void showSuggestions(BuildContext context) { void showSuggestions(BuildContext context) {
FocusScope.of(context).requestFocus(_focusNode); assert(_focusNode != null, '_focusNode must be set by route before showSuggestions is called.');
_focusNode.requestFocus();
_currentBody = _SearchBody.suggestions; _currentBody = _SearchBody.suggestions;
} }
...@@ -218,7 +219,7 @@ abstract class SearchDelegate<T> { ...@@ -218,7 +219,7 @@ abstract class SearchDelegate<T> {
/// to [showSearch] that launched the search initially. /// to [showSearch] that launched the search initially.
void close(BuildContext context, T result) { void close(BuildContext context, T result) {
_currentBody = null; _currentBody = null;
_focusNode.unfocus(); _focusNode?.unfocus();
Navigator.of(context) Navigator.of(context)
..popUntil((Route<dynamic> route) => route == _route) ..popUntil((Route<dynamic> route) => route == _route)
..pop(result); ..pop(result);
...@@ -232,7 +233,9 @@ abstract class SearchDelegate<T> { ...@@ -232,7 +233,9 @@ abstract class SearchDelegate<T> {
/// page. /// page.
Animation<double> get transitionAnimation => _proxyAnimation; Animation<double> get transitionAnimation => _proxyAnimation;
final FocusNode _focusNode = FocusNode(); // The focus node to use for manipulating focus on the search page. This is
// managed, owned, and set by the _SearchPageRoute using this delegate.
FocusNode _focusNode;
final TextEditingController _queryTextController = TextEditingController(); final TextEditingController _queryTextController = TextEditingController();
...@@ -246,7 +249,6 @@ abstract class SearchDelegate<T> { ...@@ -246,7 +249,6 @@ abstract class SearchDelegate<T> {
} }
_SearchPageRoute<T> _route; _SearchPageRoute<T> _route;
} }
/// Describes the body that is currently shown under the [AppBar] in the /// Describes the body that is currently shown under the [AppBar] in the
...@@ -346,13 +348,18 @@ class _SearchPage<T> extends StatefulWidget { ...@@ -346,13 +348,18 @@ class _SearchPage<T> extends StatefulWidget {
} }
class _SearchPageState<T> extends State<_SearchPage<T>> { class _SearchPageState<T> extends State<_SearchPage<T>> {
// This node is owned, but not hosted by, the search page. Hosting is done by
// the text field.
FocusNode focusNode = FocusNode();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
queryTextController.addListener(_onQueryChanged); queryTextController.addListener(_onQueryChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged); widget.animation.addStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged); widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
widget.delegate._focusNode.addListener(_onFocusChanged); focusNode.addListener(_onFocusChanged);
widget.delegate._focusNode = focusNode;
} }
@override @override
...@@ -361,7 +368,8 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -361,7 +368,8 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
queryTextController.removeListener(_onQueryChanged); queryTextController.removeListener(_onQueryChanged);
widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged); widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._focusNode.removeListener(_onFocusChanged); widget.delegate._focusNode = null;
focusNode.dispose();
} }
void _onAnimationStatusChanged(AnimationStatus status) { void _onAnimationStatusChanged(AnimationStatus status) {
...@@ -370,12 +378,12 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -370,12 +378,12 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
} }
widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.animation.removeStatusListener(_onAnimationStatusChanged);
if (widget.delegate._currentBody == _SearchBody.suggestions) { if (widget.delegate._currentBody == _SearchBody.suggestions) {
FocusScope.of(context).requestFocus(widget.delegate._focusNode); focusNode.requestFocus();
} }
} }
void _onFocusChanged() { void _onFocusChanged() {
if (widget.delegate._focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) { if (focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
widget.delegate.showSuggestions(context); widget.delegate.showSuggestions(context);
} }
} }
...@@ -436,7 +444,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -436,7 +444,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
leading: widget.delegate.buildLeading(context), leading: widget.delegate.buildLeading(context),
title: TextField( title: TextField(
controller: queryTextController, controller: queryTextController,
focusNode: widget.delegate._focusNode, focusNode: focusNode,
style: theme.textTheme.title, style: theme.textTheme.title,
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
onSubmitted: (String _) { onSubmitted: (String _) {
......
...@@ -1455,7 +1455,8 @@ class RenderEditable extends RenderBox { ...@@ -1455,7 +1455,8 @@ class RenderEditable extends RenderBox {
} }
TextSelection _selectWordAtOffset(TextPosition position) { TextSelection _selectWordAtOffset(TextPosition position) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
final TextRange word = _textPainter.getWordBoundary(position); final TextRange word = _textPainter.getWordBoundary(position);
// When long-pressing past the end of the text, we want a collapsed cursor. // When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= word.end) if (position.offset >= word.end)
...@@ -1527,7 +1528,8 @@ class RenderEditable extends RenderBox { ...@@ -1527,7 +1528,8 @@ class RenderEditable extends RenderBox {
} }
void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) { void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
// If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while // If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while
// the floating cursor's color is _cursorColor; // the floating cursor's color is _cursorColor;
...@@ -1593,7 +1595,8 @@ class RenderEditable extends RenderBox { ...@@ -1593,7 +1595,8 @@ class RenderEditable extends RenderBox {
} }
void _paintFloatingCaret(Canvas canvas, Offset effectiveOffset) { void _paintFloatingCaret(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
assert(_floatingCursorOn); assert(_floatingCursorOn);
// We always want the floating cursor to render at full opacity. // We always want the floating cursor to render at full opacity.
...@@ -1680,7 +1683,8 @@ class RenderEditable extends RenderBox { ...@@ -1680,7 +1683,8 @@ class RenderEditable extends RenderBox {
} }
void _paintSelection(Canvas canvas, Offset effectiveOffset) { void _paintSelection(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
assert(_selectionRects != null); assert(_selectionRects != null);
final Paint paint = Paint()..color = _selectionColor; final Paint paint = Paint()..color = _selectionColor;
for (ui.TextBox box in _selectionRects) for (ui.TextBox box in _selectionRects)
...@@ -1688,7 +1692,8 @@ class RenderEditable extends RenderBox { ...@@ -1688,7 +1692,8 @@ class RenderEditable extends RenderBox {
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
final Offset effectiveOffset = offset + _paintOffset; final Offset effectiveOffset = offset + _paintOffset;
bool showSelection = false; bool showSelection = false;
......
...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart'; ...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
/// onTap: () { /// onTap: () {
/// FocusScope.of(context).requestFocus(_focusNode); /// FocusScope.of(context).requestFocus(_focusNode);
/// }, /// },
/// child: Text('Tap to focus'), /// child: const Text('Tap to focus'),
/// ); /// );
/// } /// }
/// return Text(_message ?? 'Press a key'); /// return Text(_message ?? 'Press a key');
......
...@@ -787,6 +787,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -787,6 +787,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final LayerLink _layerLink = LayerLink(); final LayerLink _layerLink = LayerLink();
bool _didAutoFocus = false; bool _didAutoFocus = false;
FocusAttachment _focusAttachment;
// This value is an eyeball estimation of the time it takes for the iOS cursor // This value is an eyeball estimation of the time it takes for the iOS cursor
// to ease in and out. // to ease in and out.
...@@ -809,6 +810,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -809,6 +810,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void initState() { void initState() {
super.initState(); super.initState();
widget.controller.addListener(_didChangeTextEditingValue); widget.controller.addListener(_didChangeTextEditingValue);
_focusAttachment = widget.focusNode.attach(context);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
_scrollController.addListener(() { _selectionOverlay?.updateForScroll(); }); _scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
_cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration); _cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration);
...@@ -836,6 +838,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -836,6 +838,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
if (widget.focusNode != oldWidget.focusNode) { if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged); oldWidget.focusNode.removeListener(_handleFocusChanged);
_focusAttachment?.detach();
_focusAttachment = widget.focusNode.attach(context);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive(); updateKeepAlive();
} }
...@@ -852,6 +856,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -852,6 +856,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
assert(_cursorTimer == null); assert(_cursorTimer == null);
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
_selectionOverlay = null; _selectionOverlay = null;
_focusAttachment.detach();
widget.focusNode.removeListener(_handleFocusChanged); widget.focusNode.removeListener(_handleFocusChanged);
super.dispose(); super.dispose();
} }
...@@ -1091,10 +1096,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1091,10 +1096,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (_hasFocus) { if (_hasFocus) {
_openInputConnection(); _openInputConnection();
} else { } else {
final List<FocusScopeNode> ancestorScopes = FocusScope.ancestorsOf(context); widget.focusNode.requestFocus();
for (int i = ancestorScopes.length - 1; i >= 1; i -= 1)
ancestorScopes[i].setFirstFocus(ancestorScopes[i - 1]);
FocusScope.of(context).requestFocus(widget.focusNode);
} }
} }
...@@ -1400,7 +1402,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1400,7 +1402,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
FocusScope.of(context).reparentIfNeeded(widget.focusNode); _focusAttachment.reparent();
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls; final TextSelectionControls controls = widget.selectionControls;
......
...@@ -3,61 +3,505 @@ ...@@ -3,61 +3,505 @@
// 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';
/// A leaf node in the focus tree that can receive focus. import 'binding.dart';
import 'focus_scope.dart';
import 'framework.dart';
/// Signature of a callback used by [Focus.onKey] and [FocusScope.onKey]
/// to receive key events.
/// ///
/// The focus tree keeps track of which widget is the user's current focus. The /// The [node] is the node that received the event.
/// focused widget often listens for keyboard events. typedef FocusOnKeyCallback = bool Function(FocusNode node, RawKeyEvent event);
/// An attachment point for a [FocusNode].
/// ///
/// To request focus, find the [FocusScopeNode] for the current [BuildContext] /// Once created, a [FocusNode] must be attached to the widget tree by its
/// and call the [FocusScopeNode.requestFocus] method: /// _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.
/// ///
/// ```dart /// This attachment is created by calling [FocusNode.attach], usually from the
/// FocusScope.of(context).requestFocus(focusNode); /// 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
/// focus to be lost each time the widget is built, which is usually not desired
/// behavior (call [unfocus] if losing focus is desired).
///
/// If, as is common, the hosting [StatefulWidget] is also the owner of the
/// focus node, then it will also call [dispose] from its [State.dispose] (in
/// 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}
/// ## Key Event Propagation
///
/// 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';
/// ``` /// ```
/// ///
/// If your widget requests focus, be sure to call /// ```dart preamble
/// `FocusScope.of(context).reparentIfNeeded(focusNode);` in your `build` /// class ColorfulButton extends StatefulWidget {
/// method to reparent your [FocusNode] if your widget moves from one /// ColorfulButton({Key key}) : super(key: key);
/// 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();
/// }
/// ///
/// ## Lifetime /// @override
/// 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'),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// ///
/// Focus nodes are long-lived objects. For example, if a stateful widget has a /// ```dart
/// focusable child widget, it should create a [FocusNode] in the /// Widget build(BuildContext context) {
/// [State.initState] method, and [dispose] it in the [State.dispose] method, /// final TextTheme textTheme = Theme.of(context).textTheme;
/// providing the same [FocusNode] to the focusable child each time the /// return DefaultTextStyle(
/// [State.build] method is run. In particular, creating a [FocusNode] each time /// style: textTheme.display1,
/// [State.build] is invoked will cause the focus to be lost each time the /// child: ColorfulButton(),
/// widget is built. /// );
/// }
/// ```
/// {@end-tool}
/// ///
/// See also: /// See also:
/// ///
/// * [FocusScopeNode], which is an interior node in the focus tree. /// * [Focus], a widget that manages a [FocusNode] and provides access to
/// * [FocusScope.of], which provides the [FocusScopeNode] for a given /// focus information and actions to its descendant widgets.
/// [BuildContext]. /// * [FocusScope], a widget that manages a [FocusScopeNode] and provides
class FocusNode extends ChangeNotifier { /// access to scope information and actions to its descendant widgets.
FocusScopeNode _parent; /// * [FocusAttachment], a widget that connects a [FocusScopeNode] to the
/// 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;
/// Whether this node has the overall focus. /// Returns the parent node for this object.
/// ///
/// A [FocusNode] has the overall focus when the node is focused in its /// All nodes except for the root [FocusScopeNode] ([FocusManager.rootScope])
/// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for /// will be given a parent when they are added to the focus tree, which is
/// that scope and all its ancestor scopes. /// done using [FocusAttachment.reparent].
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.
/// ///
/// To request focus, find the [FocusScopeNode] for the current [BuildContext] /// Will always return null in release builds.
/// and call the [FocusScopeNode.requestFocus] method: String get debugLabel => _debugLabel;
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.
/// ///
/// ```dart /// A [FocusNode] has focus when it is an ancestor of a node that returns true
/// FocusScope.of(context).requestFocus(focusNode); /// from [hasPrimaryFocus], or it has the primary focus itself.
/// ``` ///
/// 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 hasFocus => _manager?._currentFocus == this; bool get hasPrimaryFocus => _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.
/// ///
...@@ -68,330 +512,360 @@ class FocusNode extends ChangeNotifier { ...@@ -68,330 +512,360 @@ class FocusNode extends 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 whether this function successfully consumes a keyboard token. /// Returns true if this method successfully consumes the keyboard token.
bool consumeKeyboardToken() { bool consumeKeyboardToken() {
if (!_hasKeyboardToken) if (!_hasKeyboardToken) {
return false; return false;
}
_hasKeyboardToken = false; _hasKeyboardToken = false;
return true; return true;
} }
/// Cancels any outstanding requests for focus. // Marks the node as dirty, meaning that it needs to notify listeners of a
// 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.
/// ///
/// This method is safe to call regardless of whether this node has ever /// In order to attach a [FocusNode] to the widget tree, call [attach],
/// requested focus. /// typically from the [StatefulWidget]'s [State.initState] method.
void unfocus() { ///
_parent?._resignFocus(this); /// If the focus node in the host widget is swapped out, the new node will
assert(_parent == null); /// need to be attached. [FocusAttachment.detach] should be called on the old
assert(_manager == null); /// node, and then [attach] called on the new node. This typically happens in
/// the [State.didUpdateWidget] method.
@mustCallSuper
FocusAttachment attach(BuildContext context, {FocusOnKeyCallback onKey}) {
_context = context;
_onKey = onKey;
_attachment = FocusAttachment._(this);
return _attachment;
} }
@override @override
void dispose() { void dispose() {
_manager?._willDisposeFocusNode(this); _manager?._willDisposeFocusNode(this);
_parent?._resignFocus(this); _attachment?.detach();
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
String toString() => '${describeIdentity(this)}${hasFocus ? '(FOCUSED)' : ''}'; void debugFillProperties(DiagnosticPropertiesBuilder properties) {
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();
}
} }
/// An interior node in the focus tree. /// A subclass of [FocusNode] that acts as a scope for its descendants,
/// maintaining information about which descendant is currently or was last
/// focused.
/// ///
/// The focus tree keeps track of which widget is the user's current focus. The /// _Please see the [FocusScope] and [Focus] widgets, which are utility widgets
/// focused widget often listens for keyboard events. /// that manage their own [FocusScopeNode]s and [FocusNode]s, respectively. If
/// they aren't appropriate, [FocusScopeNode]s can be managed directly._
/// ///
/// The interior nodes in the focus tree cannot themselves be focused but /// [FocusScopeNode] organizes [FocusNodes] into _scopes_. Scopes form sub-trees
/// instead remember previous focus states. A scope is currently active in its /// of nodes that can be traversed as a group. Within a scope, the most recent
/// parent whenever [isFirstFocus] is true. If that scope is detached from its /// nodes to have focus are remembered, and if a node is focused and then
/// parent, its previous sibling becomes the parent's first focus. /// removed, the original node receives focus again.
/// ///
/// A [FocusNode] has the overall focus when the node is focused in its /// From a [FocusScopeNode], calling [setFirstFocus], sets the given focus scope
/// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for /// as the [focusedChild] of this node, adopting if it isn't already part of the
/// that scope and all its ancestor scopes. /// focus tree.
/// ///
/// If a [FocusScopeNode] is removed, then the next sibling node will be set as /// {@macro flutter.widgets.focusManager.lifecycle}
/// the focused node by the [FocusManager]. /// {@macro flutter.widgets.focus_manager.focus.keyEvents}
/// ///
/// See also: /// See also:
/// ///
/// * [FocusNode], which is a leaf node in the focus tree that can receive /// * [Focus], a widget that manages a [FocusNode] and provides access to
/// focus. /// 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.
/// * [FocusScope], which is a widget that associates a [FocusScopeNode] with /// * [FocusAttachment], a widget that connects a [FocusScopeNode] to the
/// its location in the tree. /// focus tree.
class FocusScopeNode extends Object with DiagnosticableTreeMixin { /// * [FocusManager], a singleton that manages the focus and distributes key
FocusManager _manager; /// events to focused nodes.
FocusScopeNode _parent; class FocusScopeNode extends FocusNode {
/// Creates a FocusScope node.
FocusScopeNode _nextSibling; ///
FocusScopeNode _previousSibling; /// All parameters are optional.
FocusScopeNode({
FocusScopeNode _firstChild; String debugLabel,
FocusScopeNode _lastChild; FocusOnKeyCallback onKey,
}) : super(debugLabel: debugLabel, onKey: onKey);
FocusNode _focus;
List<FocusScopeNode> _focusPath;
/// Whether this scope is currently active in its parent scope.
bool get isFirstFocus => _parent == null || _parent._firstChild == this;
// Returns this FocusScopeNode's ancestors, starting with the node
// below the FocusManager's rootScope.
List<FocusScopeNode> _getFocusPath() {
final List<FocusScopeNode> nodes = <FocusScopeNode>[this];
FocusScopeNode node = _parent;
while (node != null && node != _manager?.rootScope) {
nodes.add(node);
node = node._parent;
}
return nodes;
}
void _prepend(FocusScopeNode child) { @override
assert(child != this); FocusScopeNode get nearestScope => this;
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);
}
void _updateManager(FocusManager manager) { /// Returns true if this scope is the focused child of its parent scope.
void update(FocusScopeNode child) { bool get isFirstFocus => enclosingScope.focusedChild == this;
if (child._manager == manager)
return;
child._manager = manager;
// We don't proactively null out the manager for FocusNodes because the
// manager holds the currently active focus node until the end of the
// microtask, even if that node is detached from the focus tree.
if (manager != null)
child._focus?._manager = manager;
child._visitChildren(update);
}
update(this); /// Returns the child of this node that should receive focus if this scope
/// node receives focus.
///
/// If [hasFocus] is true, then this points to the child of this node that is
/// currently focused.
///
/// Returns null if there is no currently focused child.
FocusNode get focusedChild {
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this,
'Focused child does not have the same idea of its enclosing scope as the scope does.');
return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
} }
void _visitChildren(void visitor(FocusScopeNode child)) { // A stack of the children that have been set as the focusedChild, most recent
FocusScopeNode child = _firstChild; // last (which is the top of the stack).
while (child != null) { final List<FocusNode> _focusedChildren = <FocusNode>[];
visitor(child);
child = child._nextSibling;
}
}
bool _debugUltimatePreviousSiblingOf(FocusScopeNode child, { FocusScopeNode equals }) { @override
while (child._previousSibling != null) { void _reparent(FocusNode child) {
assert(child._previousSibling != child); final bool hadChildren = _children.isNotEmpty;
child = child._previousSibling; super._reparent(child);
final FocusScopeNode currentEnclosingScope = child.enclosingScope;
// If we just added our first child to this scope, and this scope had the
// focus, then focus the child.
if (!hadChildren && currentEnclosingScope.focusedChild == null && currentEnclosingScope.hasFocus) {
child.requestFocus();
} }
return child == equals;
} }
bool _debugUltimateNextSiblingOf(FocusScopeNode child, { FocusScopeNode equals }) { /// Make the given [scope] the active child scope for this scope.
while (child._nextSibling != null) { ///
assert(child._nextSibling != child); /// If the given [scope] is not yet a part of the focus tree, then add it to
child = child._nextSibling; /// the tree as a child of this scope.
///
/// The given scope must be a descendant of this scope.
void setFirstFocus(FocusScopeNode scope) {
assert(scope != null);
if (scope._parent == null) {
_reparent(scope);
} }
return child == equals; 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
void _remove(FocusScopeNode child) { // focus on that node.
assert(child._parent == this); FocusNode descendantFocus = scope.focusedChild;
assert(child._manager == _manager); while (descendantFocus is FocusScopeNode && descendantFocus != null) {
assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild)); final FocusScopeNode descendantScope = descendantFocus;
assert(_debugUltimateNextSiblingOf(child, equals: _lastChild)); descendantFocus = descendantScope.focusedChild;
if (child._previousSibling == null) {
assert(_firstChild == child);
_firstChild = child._nextSibling;
} else {
child._previousSibling._nextSibling = child._nextSibling;
} }
if (child._nextSibling == null) { if (descendantFocus != null) {
assert(_lastChild == child); descendantFocus?._doRequestFocus(isFromPolicy: false);
_lastChild = child._previousSibling;
} else { } else {
child._nextSibling._previousSibling = child._previousSibling; scope._doRequestFocus(isFromPolicy: false);
} }
child._previousSibling = null;
child._nextSibling = null;
child._parent = null;
child._updateManager(null);
} }
void _didChangeFocusChain() { /// If this scope lacks a focus, request that the given node become the focus.
if (isFirstFocus)
_manager?._markNeedsUpdate();
}
/// Requests that the given node becomes the focus for this scope.
/// ///
/// If the given node is currently focused in another scope, the node will /// If the given node is not yet part of the focus tree, then add it as a
/// first be unfocused in that scope. /// child of this node.
///
/// 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 overall focus in a /// The node is notified that it has received the primary focus in a
/// microtask. /// microtask, so notification may lag the request by up to one frame.
void autofocus(FocusNode node) { void autofocus(FocusNode node) {
assert(node != null); if (focusedChild == null) {
if (_focus == null) { if (node._parent == null) {
node._hasKeyboardToken = true; _reparent(node);
_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();
} }
} }
/// Adopts the given node if it is focused in another scope. @override
/// void _doRequestFocus({@required bool isFromPolicy}) {
/// A widget that requests that a node is focused should call this method assert(isFromPolicy != null);
/// during its `build` method in case the widget is moved from one location // Start with the primary focus as the focused child of this scope, if there
/// in the tree to another location that has a different focus scope. // is one. Otherwise start with this node itself.
void reparentIfNeeded(FocusNode node) { FocusNode primaryFocus = focusedChild ?? this;
assert(node != null); // Keep going down through scopes until the ultimately focusable item is
if (node._parent == null || node._parent == this) // found, a scope doesn't have a focusedChild, or a non-scope is
return; // encountered.
node.unfocus(); while (primaryFocus is FocusScopeNode && primaryFocus.focusedChild != null) {
assert(node._parent == null); final FocusScopeNode scope = primaryFocus;
if (_focus == null) primaryFocus = scope.focusedChild;
_setFocus(node); }
} if (primaryFocus is FocusScopeNode) {
// We didn't find a FocusNode at the leaf, so we're focusing the scope.
void _setFocus(FocusNode node) { _markAsDirty(newFocus: primaryFocus);
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 {
child.detach(); primaryFocus.requestFocus();
} }
} }
/// 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);
if (_focus != null) properties.add(DiagnosticsProperty<FocusNode>('focusedChild', focusedChild, defaultValue: 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;
} }
} }
...@@ -418,70 +892,175 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { ...@@ -418,70 +892,175 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin {
/// ///
/// See also: /// See also:
/// ///
/// * [FocusNode], which is a leaf node in the focus tree that can receive /// * [FocusNode], which is a node in the focus tree that can receive focus.
/// focus. /// * [FocusScopeNode], which is an node in the focus tree used to collect
/// * [FocusScopeNode], which is an interior node in the focus tree. /// subtrees into groups.
/// * [FocusScope.of], which provides the [FocusScopeNode] for a given /// * [Focus.of], which provides the nearest ancestor [FocusNode] for a given
/// [BuildContext]. /// [BuildContext].
class FocusManager { /// * [FocusScope.of], which provides the nearest ancestor [FocusScopeNode] for
/// 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;
assert(rootScope._firstChild == null); RawKeyboard.instance.addListener(_handleRawKeyEvent);
assert(rootScope._lastChild == null);
} }
/// The root [FocusScopeNode] in the focus tree. /// The root [FocusScopeNode] in the focus tree.
/// ///
/// This field is rarely used directly. Instead, to find the /// This field is rarely used directly. To find the nearest [FocusScopeNode]
/// [FocusScopeNode] for a given [BuildContext], use [FocusScope.of]. /// for a given [FocusNode], call [FocusNode.nearestScope].
final FocusScopeNode rootScope = FocusScopeNode(); FocusScopeNode get rootScope => _rootScope;
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);
if (_currentFocus == node) _willUnfocusNode(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() {
if (_haveScheduledUpdate) // Request that an update be scheduled, optionally requesting focus for the
// 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(_update); scheduleMicrotask(_applyFocusChange);
} }
FocusNode _findNextFocus() { void _applyFocusChange() {
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;
_currentFocus = nextFocus; if (_currentFocus == null && _nextFocus == null) {
previousFocus?._notify(); _nextFocus = rootScope;
_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();
} }
List<FocusScopeNode> _getCurrentFocusPath() => _currentFocus?._parent?._getFocusPath(); @override
List<DiagnosticsNode> debugDescribeChildren() {
return <DiagnosticsNode>[
rootScope.toDiagnosticsNode(name: 'rootScope'),
];
}
@override @override
String toString() { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
final String status = _haveScheduledUpdate ? ' UPDATE SCHEDULED' : ''; properties.add(FlagProperty('haveScheduledUpdate', value: _haveScheduledUpdate, ifTrue: 'UPDATE SCHEDULED'));
const String indent = ' '; properties.add(DiagnosticsProperty<FocusNode>('currentFocus', _currentFocus, defaultValue: null));
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';
class _FocusScopeMarker extends InheritedWidget { /// A widget that manages a [FocusNode] to allow keyboard focus to be given
const _FocusScopeMarker({ /// to this widget and its descendants.
Key key,
@required this.node,
Widget child,
}) : assert(node != null),
super(key: key, child: child);
final FocusScopeNode node;
@override
bool updateShouldNotify(_FocusScopeMarker oldWidget) {
return node != oldWidget.node;
}
}
/// Establishes a scope in which widgets can receive focus.
/// ///
/// The focus tree keeps track of which widget is the user's current focus. The /// When the focus is gained or lost, [onFocusChanged] is called.
/// focused widget often listens for keyboard events.
/// ///
/// A focus scope does not itself receive focus but instead helps remember /// For keyboard events, [onKey] is called if [FocusNode.hasFocus] is true for
/// previous focus states. A scope is currently active when its [node] is the /// this widget's [focusNode], unless a focused descendant's [onKey] callback
/// first focus of its parent scope. To activate a [FocusScope], either use the /// returns false when called.
/// [autofocus] property or explicitly make the [node] the first focus in the
/// parent scope:
/// ///
/// ```dart /// This widget does not provide any visual indication that the focus has
/// FocusScope.of(context).setFirstFocus(node); /// changed. Any desired visual changes should be made when [onFocusChanged] is
/// called.
///
/// 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.
///
/// 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}
/// This example shows how to manage focus using the [Focus] and [FocusScope]
/// widgets. See [FocusNode] for a similar example that doesn't use [Focus] or
/// [FocusScope].
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ``` /// ```
/// ///
/// If a [FocusScope] is removed from the widget tree, then the previously /// ```dart
/// focused node will be focused, but only if the [node] is the same [node] /// Color _color = Colors.white;
/// 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, /// bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
/// or pass in the [node] from a parent that is not rebuilt. If there is no next /// if (event is RawKeyDownEvent) {
/// sibling, then the parent scope node will be focused. /// 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}
/// ///
/// See also: /// See also:
/// ///
/// * [FocusScopeNode], which is the associated node in the focus tree. /// * [FocusNode], which represents a node in the focus hierarchy and
/// * [FocusNode], which is a leaf node in the focus tree that can receive /// [FocusNode]'s API documentation includes a detailed explanation of its
/// focus. /// role in the overall focus system.
class FocusScope extends StatefulWidget { /// * [FocusScope], a widget that manages a group of focusable widgets using a
/// Creates a scope in which widgets can receive focus. /// [FocusScopeNode].
/// * [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 [node] argument must not be null. /// The [child] argument is required and must not be null.
const FocusScope({ ///
/// The [autofocus] argument must not be null.
const Focus({
Key key, Key key,
@required this.node, @required this.child,
this.focusNode,
this.autofocus = false, this.autofocus = false,
this.child, this.onFocusChange,
}) : assert(node != null), this.onKey,
assert(autofocus != null), this.debugLabel,
super(key: key); }) : assert(child != null),
assert(autofocus != null),
super(key: key);
/// Controls whether this scope is currently active. /// A debug label for this widget.
final FocusScopeNode node; ///
/// Not used for anything except to be printed in the diagnostic output from
/// Whether this scope should attempt to become active when first added to /// [toString] or [toStringDeep]. Also unused if a [focusNode] is provided,
/// the tree. /// since that node can have its own [FocusNode.debugLabel].
final bool autofocus; ///
/// 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 widget below this widget in the tree. /// The child widget of this [Focus].
/// ///
/// {@macro flutter.widgets.child} /// {@macro flutter.widgets.child}
final Widget child; final Widget child;
/// Returns the [node] of the [FocusScope] that most tightly encloses the /// Handler for keys pressed when this object or one of its children has
/// given [BuildContext]. /// focus.
/// ///
/// The [context] argument must not be null. /// Key events are first given to the [FocusNode] that has primary focus, and
static FocusScopeNode of(BuildContext context) { /// if its [onKey] method return false, then they are given to each ancestor
assert(context != null); /// node up the focus hierarchy in turn. If an event reaches the root of the
final _FocusScopeMarker scope = context.inheritFromWidgetOfExactType(_FocusScopeMarker); /// hierarchy, it is discarded.
return scope?.node ?? context.owner.focusManager.rootScope; ///
} /// 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;
/// A list of the [FocusScopeNode]s for each [FocusScope] ancestor of /// Handler called when the focus changes.
/// the given [BuildContext]. The first element of the list is the
/// nearest ancestor's [FocusScopeNode].
/// ///
/// The returned list does not include the [FocusManager]'s `rootScope`. /// Called with true if this node gains focus, and false if it loses
/// Only the [FocusScopeNode]s that belong to [FocusScope] widgets are /// focus.
/// returned. 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;
/// An optional focus node to use as the focus node for this [Focus] widget.
///
/// If one is not supplied, then one will be allocated and owned by this
/// widget.
///
/// 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
/// [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 List<FocusScopeNode> ancestorsOf(BuildContext context) { static FocusNode of(BuildContext context) {
assert(context != null); assert(context != null);
final List<FocusScopeNode> ancestors = <FocusScopeNode>[]; final _FocusMarker marker = context.inheritFromWidgetOfExactType(_FocusMarker);
while (true) { return marker?.notifier ?? context.owner.focusManager.rootScope;
context = context.ancestorInheritedElementForWidgetOfExactType(_FocusScopeMarker);
if (context == null)
return ancestors;
final _FocusScopeMarker scope = context.widget;
ancestors.add(scope.node);
context.visitAncestorElements((Element parent) {
context = parent;
return false;
});
}
} }
/// Returns true if the nearest enclosing [Focus] widget's node is focused.
///
/// A convenience method to allow build methods to write:
/// `Focus.isAt(context)` to get whether or not the nearest [Focus] or
/// [FocusScope] above them in the widget hierarchy currently has the keyboard
/// focus.
static bool isAt(BuildContext context) => Focus.of(context).hasFocus;
@override @override
_FocusScopeState createState() => _FocusScopeState(); void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false));
properties.add(DiagnosticsProperty<FocusScopeNode>('node', focusNode, defaultValue: null));
}
@override
_FocusState createState() => _FocusState();
} }
class _FocusScopeState extends State<FocusScope> { class _FocusState extends State<Focus> {
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).setFirstFocus(widget.node); FocusScope.of(context).autofocus(node);
_didAutofocus = true; _didAutofocus = true;
} }
} }
@override @override
void dispose() { void didUpdateWidget(Focus oldWidget) {
widget.node.detach(); super.didUpdateWidget(oldWidget);
super.dispose(); if (oldWidget.debugLabel != widget.debugLabel && _internalNode != null) {
_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) {
FocusScope.of(context).reparentScopeIfNeeded(widget.node); _focusAttachment.reparent();
return Semantics( return Semantics(
explicitChildNodes: true, explicitChildNodes: true,
child: _FocusScopeMarker( child: _FocusMarker(
node: widget.node, node: 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.
final FocusManager focusManager = FocusManager(); FocusManager focusManager = FocusManager();
/// Adds an element to the dirty elements list so that it will be rebuilt /// Adds an element to the dirty elements list so that it will be rebuilt
/// when [WidgetsBinding.drawFrame] calls [buildScope]. /// when [WidgetsBinding.drawFrame] calls [buildScope].
......
...@@ -1468,7 +1468,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1468,7 +1468,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final Set<Route<dynamic>> _poppedRoutes = <Route<dynamic>>{}; final Set<Route<dynamic>> _poppedRoutes = <Route<dynamic>>{};
/// The [FocusScopeNode] for the [FocusScope] that encloses the routes. /// The [FocusScopeNode] for the [FocusScope] that encloses the routes.
final FocusScopeNode focusScopeNode = FocusScopeNode(); final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope');
final List<OverlayEntry> _initialOverlayEntries = <OverlayEntry>[]; final List<OverlayEntry> _initialOverlayEntries = <OverlayEntry>[];
...@@ -1556,7 +1556,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1556,7 +1556,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
route.dispose(); route.dispose();
_poppedRoutes.clear(); _poppedRoutes.clear();
_history.clear(); _history.clear();
focusScopeNode.detach(); focusScopeNode.dispose();
super.dispose(); super.dispose();
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
} }
......
...@@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
export 'package:flutter/services.dart' show RawKeyEvent; export 'package:flutter/services.dart' show RawKeyEvent;
...@@ -112,5 +113,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> { ...@@ -112,5 +113,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> {
} }
@override @override
Widget build(BuildContext context) => widget.child; Widget build(BuildContext context) => Focus(focusNode: widget.focusNode, child: widget.child);
} }
...@@ -583,6 +583,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -583,6 +583,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
// This is the combination of the two animations for the route. // This is the combination of the two animations for the route.
Listenable _listenable; Listenable _listenable;
/// The node this scope will use for its root [FocusScope] widget.
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope');
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -592,12 +595,14 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -592,12 +595,14 @@ 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
...@@ -612,6 +617,12 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -612,6 +617,12 @@ 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) {
...@@ -629,7 +640,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -629,7 +640,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
child: PageStorage( child: PageStorage(
bucket: widget.route._storageBucket, // immutable bucket: widget.route._storageBucket, // immutable
child: FocusScope( child: FocusScope(
node: widget.route.focusScopeNode, // immutable node: focusScopeNode, // immutable
child: RepaintBoundary( child: RepaintBoundary(
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _listenable, // immutable animation: _listenable, // immutable
...@@ -887,9 +898,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -887,9 +898,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return child; return child;
} }
/// The node this route will use for its root [FocusScope] widget.
final FocusScopeNode focusScopeNode = FocusScopeNode();
@override @override
void install(OverlayEntry insertionPoint) { void install(OverlayEntry insertionPoint) {
super.install(insertionPoint); super.install(insertionPoint);
...@@ -897,16 +905,18 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -897,16 +905,18 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation); _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
} }
@override bool _wantsFocus = false;
TickerFuture didPush() { void _grabFocusIfNeeded(FocusScopeNode node) {
navigator.focusScopeNode.setFirstFocus(focusScopeNode); if (_wantsFocus) {
return super.didPush(); _wantsFocus = false;
navigator.focusScopeNode.setFirstFocus(node);
}
} }
@override @override
void dispose() { TickerFuture didPush() {
focusScopeNode.detach(); _wantsFocus = true;
super.dispose(); return super.didPush();
} }
// The API for subclasses to override - used by this class // The API for subclasses to override - used by this class
......
...@@ -106,7 +106,10 @@ void main() { ...@@ -106,7 +106,10 @@ void main() {
testWidgets('Last tab gets focus', (WidgetTester tester) async { testWidgets('Last tab gets focus', (WidgetTester tester) async {
// 2 nodes for 2 tabs // 2 nodes for 2 tabs
final List<FocusNode> focusNodes = <FocusNode>[FocusNode(), FocusNode()]; final List<FocusNode> focusNodes = <FocusNode>[
FocusNode(debugLabel: 'Node 1'),
FocusNode(debugLabel: 'Node 2'),
];
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
...@@ -139,7 +142,10 @@ void main() { ...@@ -139,7 +142,10 @@ void main() {
testWidgets('Do not affect focus order in the route', (WidgetTester tester) async { testWidgets('Do not affect focus order in the route', (WidgetTester tester) async {
final List<FocusNode> focusNodes = <FocusNode>[ final List<FocusNode> focusNodes = <FocusNode>[
FocusNode(), FocusNode(), FocusNode(), FocusNode(), FocusNode(debugLabel: 'Node 1'),
FocusNode(debugLabel: 'Node 2'),
FocusNode(debugLabel: 'Node 3'),
FocusNode(debugLabel: 'Node 4'),
]; ];
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -9,11 +9,14 @@ void main() { ...@@ -9,11 +9,14 @@ void main() {
testWidgets('Dialog interaction', (WidgetTester tester) async { testWidgets('Dialog interaction', (WidgetTester tester) async {
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
final FocusNode focusNode = FocusNode(debugLabel: 'Editable Text Node');
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( MaterialApp(
home: Material( home: Material(
child: Center( child: Center(
child: TextField( child: TextField(
focusNode: focusNode,
autofocus: true, autofocus: true,
), ),
), ),
...@@ -130,7 +133,7 @@ void main() { ...@@ -130,7 +133,7 @@ void main() {
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
}, skip: true); // https://github.com/flutter/flutter/issues/29384. });
testWidgets('Focus triggers keep-alive', (WidgetTester tester) async { testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
......
...@@ -2754,29 +2754,31 @@ void main() { ...@@ -2754,29 +2754,31 @@ void main() {
controller = TextEditingController(); controller = TextEditingController();
}); });
MaterialApp setupWidget() { Future<void> setupWidget(WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
controller = TextEditingController(); controller = TextEditingController();
return MaterialApp( await tester.pumpWidget(
home: Material( MaterialApp(
child: RawKeyboardListener( home: Material(
focusNode: focusNode, child: RawKeyboardListener(
onKey: null, focusNode: focusNode,
child: TextField( onKey: null,
controller: controller, child: TextField(
maxLines: 3, controller: controller,
strutStyle: StrutStyle.disabled, maxLines: 3,
strutStyle: StrutStyle.disabled,
),
), ),
) , ),
), ),
); );
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);
...@@ -2789,7 +2791,7 @@ void main() { ...@@ -2789,7 +2791,7 @@ void main() {
}); });
testWidgets('Control Shift test', (WidgetTester tester) async { testWidgets('Control Shift test', (WidgetTester tester) async {
await tester.pumpWidget(setupWidget()); await setupWidget(tester);
const String testValue = 'their big house'; const String testValue = 'their big house';
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2805,7 +2807,7 @@ void main() { ...@@ -2805,7 +2807,7 @@ void main() {
}); });
testWidgets('Down and up test', (WidgetTester tester) async { testWidgets('Down and up test', (WidgetTester tester) async {
await tester.pumpWidget(setupWidget()); await setupWidget(tester);
const String testValue = 'a big house'; const String testValue = 'a big house';
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2827,7 +2829,7 @@ void main() { ...@@ -2827,7 +2829,7 @@ void main() {
}); });
testWidgets('Down and up test 2', (WidgetTester tester) async { testWidgets('Down and up test 2', (WidgetTester tester) async {
await tester.pumpWidget(setupWidget()); await setupWidget(tester);
const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2914,6 +2916,8 @@ void main() { ...@@ -2914,6 +2916,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2984,6 +2988,8 @@ void main() { ...@@ -2984,6 +2988,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -3093,6 +3099,8 @@ void main() { ...@@ -3093,6 +3099,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
......
...@@ -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(); final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node');
final FocusScopeNode focusScopeNode = FocusScopeNode(); final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'EditableText Scope Node');
const TextStyle textStyle = TextStyle(); const TextStyle textStyle = TextStyle();
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
...@@ -975,6 +975,9 @@ void main() { ...@@ -975,6 +975,9 @@ void main() {
), ),
)); ));
focusNode.requestFocus();
await tester.pump();
expect( expect(
semantics, semantics,
includesNodeWith( includesNodeWith(
...@@ -1532,6 +1535,8 @@ void main() { ...@@ -1532,6 +1535,8 @@ void main() {
), ),
); );
focusNode.requestFocus();
// Now change it to make it obscure text. // Now change it to make it obscure text.
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
...@@ -1906,7 +1911,7 @@ void main() { ...@@ -1906,7 +1911,7 @@ void main() {
); );
final GlobalKey<EditableTextState> editableTextKey = final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>(); GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode(debugLabel: 'Test Focus Node');
await tester.pumpWidget(MaterialApp( // So we can show overlays. await tester.pumpWidget(MaterialApp( // So we can show overlays.
home: EditableText( home: EditableText(
......
// 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,22 +5,25 @@ ...@@ -5,22 +5,25 @@
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 TestFocusable extends StatefulWidget { class TestFocus extends StatefulWidget {
const TestFocusable({ const TestFocus({
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
TestFocusableState createState() => TestFocusableState(); TestFocusState createState() => TestFocusState();
} }
class TestFocusableState extends State<TestFocusable> { class TestFocusState extends State<TestFocus> {
final FocusNode focusNode = FocusNode(); FocusNode focusNode = FocusNode();
FocusAttachment focusAttachment;
bool _didAutofocus = false; bool _didAutofocus = false;
@override @override
...@@ -29,9 +32,16 @@ class TestFocusableState extends State<TestFocusable> { ...@@ -29,9 +32,16 @@ class TestFocusableState extends State<TestFocusable> {
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) {
FocusScope.of(context).reparentIfNeeded(focusNode); focusAttachment.reparent();
if (!_didAutofocus && widget.autofocus) { if (!_didAutofocus && widget.autofocus) {
_didAutofocus = true; _didAutofocus = true;
FocusScope.of(context).autofocus(focusNode); FocusScope.of(context).autofocus(focusNode);
...@@ -54,676 +64,899 @@ class TestFocusableState extends State<TestFocusable> { ...@@ -54,676 +64,899 @@ class TestFocusableState extends State<TestFocusable> {
} }
void main() { void main() {
testWidgets('Can focus', (WidgetTester tester) async { group(FocusScope, () {
final GlobalKey<TestFocusableState> key = GlobalKey(); testWidgets('Can focus', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey();
await tester.pumpWidget(
TestFocusable(key: key, name: 'a'),
);
expect(key.currentState.focusNode.hasFocus, isFalse);
FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode);
await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
});
testWidgets('Can unfocus', (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey();
await tester.pumpWidget(
Column(
children: <Widget>[
TestFocusable(key: keyA, name: 'a'),
TestFocusable(key: keyB, name: 'b'),
],
),
);
expect(keyA.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); await tester.pumpWidget(
await tester.pumpAndSettle(); TestFocus(key: key, name: 'a'),
);
expect(keyA.currentState.focusNode.hasFocus, isTrue); expect(key.currentState.focusNode.hasFocus, isFalse);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
// Set focus to the "B" node to unfocus the "A" node. FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode);
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); await tester.pumpAndSettle();
await tester.pumpAndSettle();
expect(keyA.currentState.focusNode.hasFocus, isFalse); expect(key.currentState.focusNode.hasFocus, isTrue);
expect(find.text('a'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isTrue); });
expect(find.text('B FOCUSED'), findsOneWidget);
});
testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey();
await tester.pumpWidget( testWidgets('Can unfocus', (WidgetTester tester) async {
Column( final GlobalKey<TestFocusState> keyA = GlobalKey();
children: <Widget>[ final GlobalKey<TestFocusState> keyB = GlobalKey();
TestFocusable( await tester.pumpWidget(
key: keyA, Column(
name: 'a',
autofocus: true,
),
TestFocusable(
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();
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>[ children: <Widget>[
TestFocusable(key: key, name: 'a'), TestFocus(key: keyA, name: 'a'),
TestFocus(key: keyB, name: 'b'),
], ],
), ),
), );
);
expect(key.currentState.focusNode.hasFocus, isFalse); expect(keyA.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget); expect(find.text('a'), findsOneWidget);
FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode); expect(keyB.currentState.focusNode.hasFocus, isFalse);
await tester.pumpAndSettle(); expect(find.text('b'), findsOneWidget);
expect(key.currentState.focusNode.hasFocus, isTrue); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
expect(find.text('A FOCUSED'), findsOneWidget); await tester.pumpAndSettle();
expect(parentFocusScope, hasAGoodToStringDeep); expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect( expect(find.text('A FOCUSED'), findsOneWidget);
parentFocusScope.toStringDeep(minLevel: DiagnosticLevel.info), expect(keyB.currentState.focusNode.hasFocus, isFalse);
equalsIgnoringHashCodes('FocusScopeNode#00000\n' expect(find.text('b'), findsOneWidget);
' focus: FocusNode#00000(FOCUSED)\n'),
);
expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep); // Set focus to the "B" node to unfocus the "A" node.
expect( FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info), await tester.pumpAndSettle();
equalsIgnoringHashCodes('FocusScopeNode#00000\n'
' └─child 1: FocusScopeNode#00000\n'
' focus: FocusNode#00000(FOCUSED)\n'),
);
// Add the child focus scope to the focus tree. expect(keyA.currentState.focusNode.hasFocus, isFalse);
parentFocusScope.setFirstFocus(childFocusScope); expect(find.text('a'), findsOneWidget);
expect(childFocusScope.isFirstFocus, isTrue); expect(keyB.currentState.focusNode.hasFocus, isTrue);
expect(find.text('B FOCUSED'), findsOneWidget);
});
// Now add the child focus scope with no focus node in it to the tree. testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async {
await tester.pumpWidget( final GlobalKey<TestFocusState> keyA = GlobalKey();
FocusScope( final GlobalKey<TestFocusState> keyB = GlobalKey();
node: parentFocusScope,
child: Column( await tester.pumpWidget(
Column(
children: <Widget>[ children: <Widget>[
TestFocusable(key: key), TestFocus(
FocusScope( key: keyA,
node: childFocusScope, name: 'a',
child: Container(), 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',
),
),
],
),
),
);
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',
),
],
),
),
);
expect(key.currentState.focusNode.hasFocus, isFalse); await tester.pumpAndSettle();
expect(find.text('a'), findsOneWidget); expect(key.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
// Now move the existing focus node into the child focus scope. // Must detach the child because we had to attach it in order to call
await tester.pumpWidget( // setFirstFocus before adding to the widget.
FocusScope( childAttachment.detach();
node: parentFocusScope, });
child: Column(
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets("Removing focused widget doesn't move focus to next widget", (WidgetTester tester) async {
final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusState> keyB = GlobalKey();
await tester.pumpWidget(
Column(
children: <Widget>[ children: <Widget>[
FocusScope( TestFocus(
node: childFocusScope, key: keyA,
child: TestFocusable(key: key), name: 'a',
),
TestFocus(
key: keyB,
name: 'b',
), ),
], ],
), ),
), );
);
await tester.pumpAndSettle(); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
expect(key.currentState.focusNode.hasFocus, isTrue); await tester.pumpAndSettle();
expect(find.text('A FOCUSED'), findsOneWidget);
// Now remove the child focus scope. expect(keyA.currentState.focusNode.hasFocus, isTrue);
await tester.pumpWidget( expect(find.text('A FOCUSED'), findsOneWidget);
FocusScope( expect(keyB.currentState.focusNode.hasFocus, isFalse);
node: parentFocusScope, expect(find.text('b'), findsOneWidget);
child: Column(
await tester.pumpWidget(
Column(
children: <Widget>[ children: <Widget>[
TestFocusable(key: key), TestFocus(
key: keyB,
name: 'b',
),
], ],
), ),
), );
);
await tester.pumpAndSettle(); await tester.pump();
expect(key.currentState.focusNode.hasFocus, isTrue); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('b'), findsOneWidget);
}); });
// Arguably, this isn't correct behavior, but it is what happens now. testWidgets('Adding a new FocusScope attaches the child it to its parent.', (WidgetTester tester) async {
testWidgets("Removing focused widget doesn't move focus to next widget", (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node');
await tester.pumpWidget( await tester.pumpWidget(
Column( FocusScope(
children: <Widget>[ node: childFocusScope,
TestFocusable( child: TestFocus(
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(keyB.currentState.focusNode.hasFocus, isFalse); expect(childFocusScope.isFirstFocus, isTrue);
expect(find.text('b'), findsOneWidget);
await tester.pumpWidget( await tester.pumpWidget(
Column( FocusScope(
children: <Widget>[ node: parentFocusScope,
TestFocusable( child: FocusScope(
key: keyB, node: childFocusScope,
name: 'b', child: TestFocus(
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('Adding a new FocusScope attaches the child it to its parent.', (WidgetTester tester) async { testWidgets('Removing a FocusScope removes its node from the tree', (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final FocusScopeNode childFocusScope = FocusScopeNode(); final GlobalKey<TestFocusState> scopeKeyA = GlobalKey();
final GlobalKey<TestFocusState> scopeKeyB = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope');
await tester.pumpWidget( // This checks both FocusScopes that have their own nodes, as well as those
FocusScope( // that use external nodes.
node: childFocusScope, await tester.pumpWidget(
child: TestFocusable( Column(
key: keyA, children: <Widget>[
name: 'a', FocusScope(
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(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
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(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(childFocusScope.isFirstFocus, isTrue); expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
await tester.pumpWidget( await tester.pumpWidget(Container());
FocusScope(
node: parentFocusScope,
child: FocusScope(
node: childFocusScope,
child: TestFocusable(
key: keyA,
name: 'a',
),
),
),
);
await tester.pump();
expect(childFocusScope.isFirstFocus, isTrue); expect(WidgetsBinding.instance.focusManager.rootScope.children, isEmpty);
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 focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async { testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(); final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1');
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( Column(
node: parentFocusScope,
autofocus: true,
child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( FocusScope(
key: keyA, node: parentFocusScope1,
name: 'a', child: Column(
children: <Widget>[
TestFocus(
debugLabel: 'Child A',
key: keyA,
name: 'a',
),
],
),
), ),
TestFocusable( FocusScope(
key: keyB, node: parentFocusScope2,
name: 'b', 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 aScope = FocusScope.of(keyA.currentContext);
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
await tester.pumpAndSettle(); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget); await tester.pumpAndSettle();
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget); expect(FocusScope.of(keyA.currentContext).isFirstFocus, isTrue);
expect(keyA.currentState.focusNode.hasFocus, isTrue);
await tester.pumpWidget( expect(find.text('A FOCUSED'), findsOneWidget);
FocusScope( expect(keyB.currentState.focusNode.hasFocus, isFalse);
node: parentFocusScope, expect(find.text('b'), findsOneWidget);
child: Column(
// 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>[ children: <Widget>[
TestFocusable( FocusScope(
key: keyB, node: parentFocusScope2,
name: 'b', child: Column(
children: <Widget>[
TestFocus(
key: keyB,
name: 'b',
autofocus: true,
),
],
),
), ),
], ],
), ),
), );
); await tester.pump();
await tester.pump(); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
});
expect(keyB.currentState.focusNode.hasFocus, isFalse); testWidgets('Moving widget from one scope to another retains focus', (WidgetTester tester) async {
expect(find.text('b'), findsOneWidget); final FocusScopeNode parentFocusScope1 = FocusScopeNode();
}); final FocusScopeNode parentFocusScope2 = FocusScopeNode();
final GlobalKey<TestFocusState> keyA = GlobalKey();
// By "pinned", it means kept in the tree by a GlobalKey. final GlobalKey<TestFocusState> keyB = GlobalKey();
testWidgets('Removing pinned focused scope moves focus to focused widget within next FocusScope', (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey();
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(
key: scopeKeyA, node: parentFocusScope1,
node: parentFocusScope1, child: Column(
child: Column( children: <Widget>[
children: <Widget>[ TestFocus(
TestFocusable( key: keyA,
key: keyA, name: 'a',
name: 'a', ),
), ],
], ),
), ),
), FocusScope(
FocusScope( node: parentFocusScope2,
key: scopeKeyB, child: Column(
node: parentFocusScope2, children: <Widget>[
child: Column( TestFocus(
children: <Widget>[ key: keyB,
TestFocusable( name: '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); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyB.currentContext)); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
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(keyB.currentState.focusNode.hasFocus, isFalse);
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget);
expect(find.text('b'), findsOneWidget);
// Since the FocusScope widgets are pinned with GlobalKeys, when the first await tester.pumpWidget(
// one gets removed, the second one stays registered with the focus Column(
// manager and ends up getting the focus since it remains as part of the children: <Widget>[
// focus tree. FocusScope(
await tester.pumpWidget( node: parentFocusScope1,
Column( child: Column(
children: <Widget>[ children: <Widget>[
FocusScope( TestFocus(
key: scopeKeyB, key: keyB,
node: parentFocusScope2, name: 'b',
child: Column( ),
children: <Widget>[ ],
TestFocusable( ),
key: keyB,
name: 'b',
autofocus: true,
),
],
), ),
), FocusScope(
], node: parentFocusScope2,
), child: Column(
); children: <Widget>[
TestFocus(
key: keyA,
name: 'a',
),
],
),
),
],
),
);
await tester.pump(); await tester.pump();
expect(keyB.currentState.focusNode.hasFocus, isTrue); expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('B FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
}); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
});
// Arguably, this isn't correct behavior, but it is what happens now. testWidgets('Moving FocusScopeNodes retains focus', (WidgetTester tester) async {
testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1');
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Scope 2');
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final FocusScopeNode parentFocusScope1 = FocusScopeNode(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final FocusScopeNode parentFocusScope2 = FocusScopeNode();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
FocusScope( FocusScope(
node: parentFocusScope1, node: parentFocusScope1,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyA, debugLabel: 'Child A',
name: 'a', key: keyA,
), name: 'a',
], ),
],
),
), ),
), FocusScope(
FocusScope( node: parentFocusScope2,
node: parentFocusScope2, child: Column(
child: Column( children: <Widget>[
children: <Widget>[ TestFocus(
TestFocusable( debugLabel: 'Child B',
key: keyB, key: keyB,
name: 'b', 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); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyB.currentContext)); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
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(keyB.currentState.focusNode.hasFocus, isFalse);
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(find.text('b'), findsOneWidget);
expect(find.text('b'), findsOneWidget);
// If the FocusScope widgets are not pinned with GlobalKeys, then the first // This just swaps the FocusScopeNodes that the FocusScopes have in them.
// one remains and gets its guts replaced with the parentFocusScope2 and the await tester.pumpWidget(
// "B" test widget, and in the process, the focus manager loses track of the Column(
// focus. children: <Widget>[
await tester.pumpWidget( FocusScope(
Column( node: parentFocusScope2,
children: <Widget>[ child: Column(
FocusScope( children: <Widget>[
node: parentFocusScope2, TestFocus(
child: Column( debugLabel: 'Child A',
children: <Widget>[ key: keyA,
TestFocusable( name: 'a',
key: keyB, ),
name: 'b', ],
autofocus: true, ),
),
],
), ),
), FocusScope(
], node: parentFocusScope1,
), child: Column(
); children: <Widget>[
await tester.pump(); TestFocus(
debugLabel: 'Child B',
expect(keyB.currentState.focusNode.hasFocus, isFalse); key: keyB,
expect(find.text('b'), findsOneWidget); name: 'b',
}); ),
],
),
),
],
),
);
// Arguably, this isn't correct behavior, but it is what happens now. await tester.pump();
testWidgets('Moving widget from one scope to another 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( expect(keyA.currentState.focusNode.hasFocus, isTrue);
Column( expect(find.text('A FOCUSED'), findsOneWidget);
children: <Widget>[ expect(keyB.currentState.focusNode.hasFocus, isFalse);
FocusScope( expect(find.text('b'), findsOneWidget);
node: parentFocusScope1, });
child: Column( });
children: <Widget>[ group(Focus, () {
TestFocusable( testWidgets('Focus.of stops at the nearest FocusScope.', (WidgetTester tester) async {
key: keyA, final GlobalKey key1 = GlobalKey(debugLabel: '1');
name: 'a', 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');
await tester.pumpWidget(
Focus(
key: key1,
debugLabel: 'Key 1',
child: Container(
key: key2,
child: Focus(
debugLabel: 'Key 3',
key: key3,
child: Container(
key: key4,
child: Focus(
debugLabel: 'Key 5',
key: key5,
child: Container(
key: key6,
),
), ),
], ),
), ),
), ),
FocusScope( ),
node: parentFocusScope2, );
child: Column( final Element element1 = tester.element(find.byKey(key1));
children: <Widget>[ final Element element2 = tester.element(find.byKey(key2));
TestFocusable( final Element element3 = tester.element(find.byKey(key3));
key: keyB, final Element element4 = tester.element(find.byKey(key4));
name: 'b', final Element element5 = tester.element(find.byKey(key5));
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,
FocusScope( child: Column(
node: parentFocusScope2, children: <Widget>[
child: Column( Focus(
children: <Widget>[ key: key7,
TestFocusable( child: Container(),
key: keyA, ),
name: 'a', Focus(
key: key8,
child: Container(),
),
],
), ),
], ),
), ],
), ),
], ),
), );
);
final Element firstScope = tester.element(find.byKey(key1));
await tester.pump(); 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(),
),
);
expect(keyA.currentState.focusNode.hasFocus, isFalse); final Element firstElement = tester.element(find.byKey(key1));
expect(find.text('a'), findsOneWidget); final FocusNode rootNode = Focus.of(firstElement);
expect(keyB.currentState.focusNode.hasFocus, isFalse); rootNode.requestFocus();
expect(find.text('b'), findsOneWidget);
});
// Arguably, this isn't correct behavior, but it is what happens now. await tester.pump();
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();
expect(rootNode.hasFocus, isTrue);
expect(rootNode, equals(firstElement.owner.focusManager.rootScope));
});
});
testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
bool gotFocus;
await tester.pumpWidget( await tester.pumpWidget(
Column( FocusScope(
children: <Widget>[ child: Focus(
FocusScope( onFocusChange: (bool focused) => gotFocus = focused,
node: parentFocusScope1, child: Container(key: key1),
child: Column( ),
children: <Widget>[
TestFocusable(
key: keyA,
name: 'a',
),
],
),
),
FocusScope(
node: parentFocusScope2,
child: Column(
children: <Widget>[
TestFocusable(
key: keyB,
name: 'b',
),
],
),
),
],
), ),
); );
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); final Element firstNode = tester.element(find.byKey(key1));
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); final FocusNode node = Focus.of(firstNode);
node.requestFocus();
await tester.pumpAndSettle(); 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 just swaps the FocusScopeNodes that the FocusScopes have in them. expect(gotFocus, isTrue);
await tester.pumpWidget( expect(node.hasFocus, isTrue);
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.pump(); await tester.pumpWidget(Container());
expect(keyA.currentState.focusNode.hasFocus, isFalse); expect(WidgetsBinding.instance.focusManager.rootScope.descendants, isEmpty);
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,13 +29,15 @@ void main() { ...@@ -29,13 +29,15 @@ void main() {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget(RawKeyboardListener( await tester.pumpWidget(
focusNode: focusNode, RawKeyboardListener(
onKey: events.add, focusNode: focusNode,
child: Container(), onKey: events.add,
)); child: Container(),
),
);
tester.binding.focusManager.rootScope.requestFocus(focusNode); focusNode.requestFocus();
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ sendFakeKeyEvent(<String, dynamic>{
...@@ -65,13 +67,15 @@ void main() { ...@@ -65,13 +67,15 @@ void main() {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget(RawKeyboardListener( await tester.pumpWidget(
focusNode: focusNode, RawKeyboardListener(
onKey: events.add, focusNode: focusNode,
child: Container(), onKey: events.add,
)); child: Container(),
),
);
tester.binding.focusManager.rootScope.requestFocus(focusNode); focusNode.requestFocus();
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ sendFakeKeyEvent(<String, dynamic>{
......
...@@ -692,6 +692,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -692,6 +692,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
FlutterError.onError = _oldExceptionHandler; FlutterError.onError = _oldExceptionHandler;
_pendingExceptionDetails = null; _pendingExceptionDetails = null;
_parentZone = null; _parentZone = null;
buildOwner.focusManager = FocusManager();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment