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
This commit is contained in:
parent
37bc48f26e
commit
4218c0bc38
@ -62,7 +62,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
|
||||
if (!_focusNode.hasFocus) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
child: Text('Tap to focus', style: textTheme.display1),
|
||||
);
|
||||
|
@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
|
||||
return MaterialApp(
|
||||
title: _title,
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: Text(_title)),
|
||||
appBar: AppBar(title: const Text(_title)),
|
||||
body: MyStatefulWidget(),
|
||||
),
|
||||
);
|
||||
|
@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
|
||||
return MaterialApp(
|
||||
title: _title,
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: Text(_title)),
|
||||
appBar: AppBar(title: const Text(_title)),
|
||||
body: MyStatelessWidget(),
|
||||
),
|
||||
);
|
||||
|
@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
|
||||
/// onTap: () {
|
||||
/// FocusScope.of(context).requestFocus(_focusNode);
|
||||
/// },
|
||||
/// child: Text('Tap to focus'),
|
||||
/// child: const Text('Tap to focus'),
|
||||
/// );
|
||||
/// }
|
||||
/// return Text(_message ?? 'Press a key');
|
||||
|
@ -304,7 +304,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
|
||||
tabs = List<Widget>(widget.tabNumber);
|
||||
tabFocusNodes = List<FocusScopeNode>.generate(
|
||||
widget.tabNumber,
|
||||
(int index) => FocusScopeNode(),
|
||||
(int index) => FocusScopeNode(debugLabel: 'Tab Focus Scope $index'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -327,7 +327,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
|
||||
@override
|
||||
void dispose() {
|
||||
for (FocusScopeNode focusScopeNode in tabFocusNodes) {
|
||||
focusScopeNode.detach();
|
||||
focusScopeNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ abstract class SearchDelegate<T> {
|
||||
///
|
||||
/// * [showSuggestions] to show the search suggestions again.
|
||||
void showResults(BuildContext context) {
|
||||
_focusNode.unfocus();
|
||||
_focusNode?.unfocus();
|
||||
_currentBody = _SearchBody.results;
|
||||
}
|
||||
|
||||
@ -208,7 +208,8 @@ abstract class SearchDelegate<T> {
|
||||
///
|
||||
/// * [showResults] to show the search results.
|
||||
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;
|
||||
}
|
||||
|
||||
@ -218,7 +219,7 @@ abstract class SearchDelegate<T> {
|
||||
/// to [showSearch] that launched the search initially.
|
||||
void close(BuildContext context, T result) {
|
||||
_currentBody = null;
|
||||
_focusNode.unfocus();
|
||||
_focusNode?.unfocus();
|
||||
Navigator.of(context)
|
||||
..popUntil((Route<dynamic> route) => route == _route)
|
||||
..pop(result);
|
||||
@ -232,7 +233,9 @@ abstract class SearchDelegate<T> {
|
||||
/// page.
|
||||
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();
|
||||
|
||||
@ -246,7 +249,6 @@ abstract class SearchDelegate<T> {
|
||||
}
|
||||
|
||||
_SearchPageRoute<T> _route;
|
||||
|
||||
}
|
||||
|
||||
/// Describes the body that is currently shown under the [AppBar] in the
|
||||
@ -346,13 +348,18 @@ class _SearchPage<T> extends StatefulWidget {
|
||||
}
|
||||
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
queryTextController.addListener(_onQueryChanged);
|
||||
widget.animation.addStatusListener(_onAnimationStatusChanged);
|
||||
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
|
||||
widget.delegate._focusNode.addListener(_onFocusChanged);
|
||||
focusNode.addListener(_onFocusChanged);
|
||||
widget.delegate._focusNode = focusNode;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -361,7 +368,8 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
|
||||
queryTextController.removeListener(_onQueryChanged);
|
||||
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
||||
widget.delegate._focusNode.removeListener(_onFocusChanged);
|
||||
widget.delegate._focusNode = null;
|
||||
focusNode.dispose();
|
||||
}
|
||||
|
||||
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||
@ -370,12 +378,12 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
|
||||
}
|
||||
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||
if (widget.delegate._currentBody == _SearchBody.suggestions) {
|
||||
FocusScope.of(context).requestFocus(widget.delegate._focusNode);
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (widget.delegate._focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
|
||||
if (focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
|
||||
widget.delegate.showSuggestions(context);
|
||||
}
|
||||
}
|
||||
@ -436,7 +444,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
|
||||
leading: widget.delegate.buildLeading(context),
|
||||
title: TextField(
|
||||
controller: queryTextController,
|
||||
focusNode: widget.delegate._focusNode,
|
||||
focusNode: focusNode,
|
||||
style: theme.textTheme.title,
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (String _) {
|
||||
|
@ -1455,7 +1455,8 @@ class RenderEditable extends RenderBox {
|
||||
}
|
||||
|
||||
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);
|
||||
// When long-pressing past the end of the text, we want a collapsed cursor.
|
||||
if (position.offset >= word.end)
|
||||
@ -1527,7 +1528,8 @@ class RenderEditable extends RenderBox {
|
||||
}
|
||||
|
||||
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
|
||||
// the floating cursor's color is _cursorColor;
|
||||
@ -1593,7 +1595,8 @@ class RenderEditable extends RenderBox {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// We always want the floating cursor to render at full opacity.
|
||||
@ -1680,7 +1683,8 @@ class RenderEditable extends RenderBox {
|
||||
}
|
||||
|
||||
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);
|
||||
final Paint paint = Paint()..color = _selectionColor;
|
||||
for (ui.TextBox box in _selectionRects)
|
||||
@ -1688,7 +1692,8 @@ class RenderEditable extends RenderBox {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
bool showSelection = false;
|
||||
|
@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
|
||||
/// onTap: () {
|
||||
/// FocusScope.of(context).requestFocus(_focusNode);
|
||||
/// },
|
||||
/// child: Text('Tap to focus'),
|
||||
/// child: const Text('Tap to focus'),
|
||||
/// );
|
||||
/// }
|
||||
/// return Text(_message ?? 'Press a key');
|
||||
|
@ -787,6 +787,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
bool _didAutoFocus = false;
|
||||
FocusAttachment _focusAttachment;
|
||||
|
||||
// This value is an eyeball estimation of the time it takes for the iOS cursor
|
||||
// to ease in and out.
|
||||
@ -809,6 +810,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_didChangeTextEditingValue);
|
||||
_focusAttachment = widget.focusNode.attach(context);
|
||||
widget.focusNode.addListener(_handleFocusChanged);
|
||||
_scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
|
||||
_cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration);
|
||||
@ -836,6 +838,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
}
|
||||
if (widget.focusNode != oldWidget.focusNode) {
|
||||
oldWidget.focusNode.removeListener(_handleFocusChanged);
|
||||
_focusAttachment?.detach();
|
||||
_focusAttachment = widget.focusNode.attach(context);
|
||||
widget.focusNode.addListener(_handleFocusChanged);
|
||||
updateKeepAlive();
|
||||
}
|
||||
@ -852,6 +856,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
assert(_cursorTimer == null);
|
||||
_selectionOverlay?.dispose();
|
||||
_selectionOverlay = null;
|
||||
_focusAttachment.detach();
|
||||
widget.focusNode.removeListener(_handleFocusChanged);
|
||||
super.dispose();
|
||||
}
|
||||
@ -1091,10 +1096,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
if (_hasFocus) {
|
||||
_openInputConnection();
|
||||
} else {
|
||||
final List<FocusScopeNode> ancestorScopes = FocusScope.ancestorsOf(context);
|
||||
for (int i = ancestorScopes.length - 1; i >= 1; i -= 1)
|
||||
ancestorScopes[i].setFirstFocus(ancestorScopes[i - 1]);
|
||||
FocusScope.of(context).requestFocus(widget.focusNode);
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1400,7 +1402,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
FocusScope.of(context).reparentIfNeeded(widget.focusNode);
|
||||
_focusAttachment.reparent();
|
||||
super.build(context); // See AutomaticKeepAliveClientMixin.
|
||||
|
||||
final TextSelectionControls controls = widget.selectionControls;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,145 +1,454 @@
|
||||
// Copyright 2015 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 'package:flutter/foundation.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'framework.dart';
|
||||
import 'inherited_notifier.dart';
|
||||
|
||||
class _FocusScopeMarker extends InheritedWidget {
|
||||
const _FocusScopeMarker({
|
||||
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.
|
||||
/// A widget that manages a [FocusNode] to allow keyboard focus to be given
|
||||
/// to this widget and its descendants.
|
||||
///
|
||||
/// The focus tree keeps track of which widget is the user's current focus. The
|
||||
/// focused widget often listens for keyboard events.
|
||||
/// When the focus is gained or lost, [onFocusChanged] is called.
|
||||
///
|
||||
/// A focus scope does not itself receive focus but instead helps remember
|
||||
/// previous focus states. A scope is currently active when its [node] is the
|
||||
/// first focus of its parent scope. To activate a [FocusScope], either use the
|
||||
/// [autofocus] property or explicitly make the [node] the first focus in the
|
||||
/// parent scope:
|
||||
/// For keyboard events, [onKey] is called if [FocusNode.hasFocus] is true for
|
||||
/// this widget's [focusNode], unless a focused descendant's [onKey] callback
|
||||
/// returns false when called.
|
||||
///
|
||||
/// ```dart
|
||||
/// FocusScope.of(context).setFirstFocus(node);
|
||||
/// This widget does not provide any visual indication that the focus has
|
||||
/// 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
|
||||
/// focused node will be focused, but only if the [node] is the same [node]
|
||||
/// object as in the previous frame. To assure this, you can use a GlobalKey to
|
||||
/// keep the [FocusScope] widget from being rebuilt from one frame to the next,
|
||||
/// or pass in the [node] from a parent that is not rebuilt. If there is no next
|
||||
/// sibling, then the parent scope node will be focused.
|
||||
/// ```dart
|
||||
/// Color _color = Colors.white;
|
||||
///
|
||||
/// bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
|
||||
/// if (event is RawKeyDownEvent) {
|
||||
/// print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}');
|
||||
/// if (event.logicalKey == LogicalKeyboardKey.keyR) {
|
||||
/// print('Changing color to red.');
|
||||
/// setState(() {
|
||||
/// _color = Colors.red;
|
||||
/// });
|
||||
/// return true;
|
||||
/// } else if (event.logicalKey == LogicalKeyboardKey.keyG) {
|
||||
/// print('Changing color to green.');
|
||||
/// setState(() {
|
||||
/// _color = Colors.green;
|
||||
/// });
|
||||
/// return true;
|
||||
/// } else if (event.logicalKey == LogicalKeyboardKey.keyB) {
|
||||
/// print('Changing color to blue.');
|
||||
/// setState(() {
|
||||
/// _color = Colors.blue;
|
||||
/// });
|
||||
/// return true;
|
||||
/// }
|
||||
/// }
|
||||
/// return false;
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// final TextTheme textTheme = Theme.of(context).textTheme;
|
||||
/// return FocusScope(
|
||||
/// debugLabel: 'Scope',
|
||||
/// autofocus: true,
|
||||
/// child: DefaultTextStyle(
|
||||
/// style: textTheme.display1,
|
||||
/// child: Focus(
|
||||
/// onKey: _handleKeyPress,
|
||||
/// debugLabel: 'Button',
|
||||
/// child: Builder(
|
||||
/// builder: (BuildContext context) {
|
||||
/// final FocusNode focusNode = Focus.of(context);
|
||||
/// final bool hasFocus = focusNode.hasFocus;
|
||||
/// return GestureDetector(
|
||||
/// onTap: () {
|
||||
/// if (hasFocus) {
|
||||
/// setState(() {
|
||||
/// focusNode.unfocus();
|
||||
/// });
|
||||
/// } else {
|
||||
/// setState(() {
|
||||
/// focusNode.requestFocus();
|
||||
/// });
|
||||
/// }
|
||||
/// },
|
||||
/// child: Center(
|
||||
/// child: Container(
|
||||
/// width: 400,
|
||||
/// height: 100,
|
||||
/// alignment: Alignment.center,
|
||||
/// color: hasFocus ? _color : Colors.white,
|
||||
/// child: Text(hasFocus ? "I'm in color! Press R,G,B!" : 'Press to focus'),
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [FocusScopeNode], which is the associated node in the focus tree.
|
||||
/// * [FocusNode], which is a leaf node in the focus tree that can receive
|
||||
/// focus.
|
||||
class FocusScope extends StatefulWidget {
|
||||
/// Creates a scope in which widgets can receive focus.
|
||||
/// * [FocusNode], which represents a node in the focus hierarchy and
|
||||
/// [FocusNode]'s API documentation includes a detailed explanation of its
|
||||
/// role in the overall focus system.
|
||||
/// * [FocusScope], a widget that manages a group of focusable widgets using a
|
||||
/// [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.
|
||||
const FocusScope({
|
||||
/// The [child] argument is required and must not be null.
|
||||
///
|
||||
/// The [autofocus] argument must not be null.
|
||||
const Focus({
|
||||
Key key,
|
||||
@required this.node,
|
||||
@required this.child,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.child,
|
||||
}) : assert(node != null),
|
||||
assert(autofocus != null),
|
||||
super(key: key);
|
||||
this.onFocusChange,
|
||||
this.onKey,
|
||||
this.debugLabel,
|
||||
}) : assert(child != null),
|
||||
assert(autofocus != null),
|
||||
super(key: key);
|
||||
|
||||
/// Controls whether this scope is currently active.
|
||||
final FocusScopeNode node;
|
||||
/// A debug label for this widget.
|
||||
///
|
||||
/// Not used for anything except to be printed in the diagnostic output from
|
||||
/// [toString] or [toStringDeep]. Also unused if a [focusNode] is provided,
|
||||
/// since that node can have its own [FocusNode.debugLabel].
|
||||
///
|
||||
/// To get a string with the entire tree, call [debugDescribeFocusTree]. To
|
||||
/// print it to the console call [debugDumpFocusTree].
|
||||
///
|
||||
/// Defaults to null.
|
||||
final String debugLabel;
|
||||
|
||||
/// Whether this scope should attempt to become active when first added to
|
||||
/// the tree.
|
||||
final bool autofocus;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
/// The child widget of this [Focus].
|
||||
///
|
||||
/// {@macro flutter.widgets.child}
|
||||
final Widget child;
|
||||
|
||||
/// Returns the [node] of the [FocusScope] that most tightly encloses the
|
||||
/// given [BuildContext].
|
||||
/// Handler for keys pressed when this object or one of its children has
|
||||
/// focus.
|
||||
///
|
||||
/// Key events are first given to the [FocusNode] that has primary focus, and
|
||||
/// if its [onKey] method return false, then they are given to each ancestor
|
||||
/// node up the focus hierarchy in turn. If an event reaches the root of the
|
||||
/// hierarchy, it is discarded.
|
||||
///
|
||||
/// This is not the way to get text input in the manner of a text field: it
|
||||
/// leaves out support for input method editors, and doesn't support soft
|
||||
/// keyboards in general. For text input, consider [TextField],
|
||||
/// [EditableText], or [CupertinoTextField] instead, which do support these
|
||||
/// things.
|
||||
final FocusOnKeyCallback onKey;
|
||||
|
||||
/// Handler called when the focus changes.
|
||||
///
|
||||
/// Called with true if this node gains focus, and false if it loses
|
||||
/// focus.
|
||||
final ValueChanged<bool> onFocusChange;
|
||||
|
||||
/// True if this widget will be selected as the initial focus when no other
|
||||
/// node in its scope is currently focused.
|
||||
///
|
||||
/// Ideally, there is only one [Focus] with autofocus set in each
|
||||
/// [FocusScope]. If there is more than one [Focus] with autofocus set, then
|
||||
/// the first one added to the tree will get focus.
|
||||
final bool autofocus;
|
||||
|
||||
/// 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.
|
||||
static FocusNode of(BuildContext context) {
|
||||
assert(context != null);
|
||||
final _FocusMarker marker = context.inheritFromWidgetOfExactType(_FocusMarker);
|
||||
return marker?.notifier ?? context.owner.focusManager.rootScope;
|
||||
}
|
||||
|
||||
/// 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
|
||||
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 _FocusState extends State<Focus> {
|
||||
FocusNode _internalNode;
|
||||
FocusNode get node => widget.focusNode ?? _internalNode;
|
||||
bool _hasFocus;
|
||||
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
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_focusAttachment?.reparent();
|
||||
if (!_didAutofocus && widget.autofocus) {
|
||||
FocusScope.of(context).autofocus(node);
|
||||
_didAutofocus = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Focus oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
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 _FocusScopeMarker scope = context.inheritFromWidgetOfExactType(_FocusScopeMarker);
|
||||
return scope?.node ?? context.owner.focusManager.rootScope;
|
||||
}
|
||||
|
||||
/// A list of the [FocusScopeNode]s for each [FocusScope] ancestor of
|
||||
/// the given [BuildContext]. The first element of the list is the
|
||||
/// nearest ancestor's [FocusScopeNode].
|
||||
///
|
||||
/// The returned list does not include the [FocusManager]'s `rootScope`.
|
||||
/// Only the [FocusScopeNode]s that belong to [FocusScope] widgets are
|
||||
/// returned.
|
||||
///
|
||||
/// The [context] argument must not be null.
|
||||
static List<FocusScopeNode> ancestorsOf(BuildContext context) {
|
||||
assert(context != null);
|
||||
final List<FocusScopeNode> ancestors = <FocusScopeNode>[];
|
||||
while (true) {
|
||||
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;
|
||||
});
|
||||
}
|
||||
final _FocusMarker marker = context.inheritFromWidgetOfExactType(_FocusMarker);
|
||||
return marker?.notifier?.nearestScope ?? context.owner.focusManager.rootScope;
|
||||
}
|
||||
|
||||
@override
|
||||
_FocusScopeState createState() => _FocusScopeState();
|
||||
}
|
||||
|
||||
class _FocusScopeState extends State<FocusScope> {
|
||||
bool _didAutofocus = false;
|
||||
|
||||
class _FocusScopeState extends _FocusState {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_didAutofocus && widget.autofocus) {
|
||||
FocusScope.of(context).setFirstFocus(widget.node);
|
||||
_didAutofocus = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.node.detach();
|
||||
super.dispose();
|
||||
FocusScopeNode _createNode() {
|
||||
return FocusScopeNode(
|
||||
debugLabel: widget.debugLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
FocusScope.of(context).reparentScopeIfNeeded(widget.node);
|
||||
_focusAttachment.reparent();
|
||||
return Semantics(
|
||||
explicitChildNodes: true,
|
||||
child: _FocusScopeMarker(
|
||||
node: widget.node,
|
||||
child: _FocusMarker(
|
||||
node: node,
|
||||
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 {
|
||||
/// the [FocusScopeNode] for a given [BuildContext].
|
||||
///
|
||||
/// 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
|
||||
/// when [WidgetsBinding.drawFrame] calls [buildScope].
|
||||
|
@ -1468,7 +1468,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
|
||||
final Set<Route<dynamic>> _poppedRoutes = <Route<dynamic>>{};
|
||||
|
||||
/// 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>[];
|
||||
|
||||
@ -1556,7 +1556,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
|
||||
route.dispose();
|
||||
_poppedRoutes.clear();
|
||||
_history.clear();
|
||||
focusScopeNode.detach();
|
||||
focusScopeNode.dispose();
|
||||
super.dispose();
|
||||
assert(() { _debugLocked = false; return true; }());
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'focus_scope.dart';
|
||||
import 'framework.dart';
|
||||
|
||||
export 'package:flutter/services.dart' show RawKeyEvent;
|
||||
@ -112,5 +113,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> {
|
||||
}
|
||||
|
||||
@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>> {
|
||||
// This is the combination of the two animations for the route.
|
||||
Listenable _listenable;
|
||||
|
||||
/// The node this scope will use for its root [FocusScope] widget.
|
||||
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -592,12 +595,14 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
|
||||
if (widget.route.secondaryAnimation != null)
|
||||
animations.add(widget.route.secondaryAnimation);
|
||||
_listenable = Listenable.merge(animations);
|
||||
widget.route._grabFocusIfNeeded(focusScopeNode);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_ModalScope<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
assert(widget.route == oldWidget.route);
|
||||
widget.route._grabFocusIfNeeded(focusScopeNode);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -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,
|
||||
// and route.offstage.
|
||||
void _routeSetState(VoidCallback fn) {
|
||||
@ -629,7 +640,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
|
||||
child: PageStorage(
|
||||
bucket: widget.route._storageBucket, // immutable
|
||||
child: FocusScope(
|
||||
node: widget.route.focusScopeNode, // immutable
|
||||
node: focusScopeNode, // immutable
|
||||
child: RepaintBoundary(
|
||||
child: AnimatedBuilder(
|
||||
animation: _listenable, // immutable
|
||||
@ -887,9 +898,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
return child;
|
||||
}
|
||||
|
||||
/// The node this route will use for its root [FocusScope] widget.
|
||||
final FocusScopeNode focusScopeNode = FocusScopeNode();
|
||||
|
||||
@override
|
||||
void install(OverlayEntry insertionPoint) {
|
||||
super.install(insertionPoint);
|
||||
@ -897,16 +905,18 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
|
||||
}
|
||||
|
||||
@override
|
||||
TickerFuture didPush() {
|
||||
navigator.focusScopeNode.setFirstFocus(focusScopeNode);
|
||||
return super.didPush();
|
||||
bool _wantsFocus = false;
|
||||
void _grabFocusIfNeeded(FocusScopeNode node) {
|
||||
if (_wantsFocus) {
|
||||
_wantsFocus = false;
|
||||
navigator.focusScopeNode.setFirstFocus(node);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusScopeNode.detach();
|
||||
super.dispose();
|
||||
TickerFuture didPush() {
|
||||
_wantsFocus = true;
|
||||
return super.didPush();
|
||||
}
|
||||
|
||||
// The API for subclasses to override - used by this class
|
||||
|
@ -106,7 +106,10 @@ void main() {
|
||||
|
||||
testWidgets('Last tab gets focus', (WidgetTester tester) async {
|
||||
// 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(
|
||||
CupertinoApp(
|
||||
@ -139,7 +142,10 @@ void main() {
|
||||
|
||||
testWidgets('Do not affect focus order in the route', (WidgetTester tester) async {
|
||||
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(
|
||||
|
@ -9,11 +9,14 @@ void main() {
|
||||
testWidgets('Dialog interaction', (WidgetTester tester) async {
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'Editable Text Node');
|
||||
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextField(
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
@ -130,7 +133,7 @@ void main() {
|
||||
await tester.pumpWidget(Container());
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
}, skip: true); // https://github.com/flutter/flutter/issues/29384.
|
||||
});
|
||||
|
||||
testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
@ -2754,29 +2754,31 @@ void main() {
|
||||
controller = TextEditingController();
|
||||
});
|
||||
|
||||
MaterialApp setupWidget() {
|
||||
|
||||
Future<void> setupWidget(WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
controller = TextEditingController();
|
||||
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: null,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
strutStyle: StrutStyle.disabled,
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: null,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
strutStyle: StrutStyle.disabled,
|
||||
),
|
||||
),
|
||||
) ,
|
||||
),
|
||||
),
|
||||
);
|
||||
focusNode.requestFocus();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('Shift test 1', (WidgetTester tester) async {
|
||||
|
||||
await tester.pumpWidget(setupWidget());
|
||||
await setupWidget(tester);
|
||||
const String testValue = 'a big house';
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
@ -2789,7 +2791,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Control Shift test', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(setupWidget());
|
||||
await setupWidget(tester);
|
||||
const String testValue = 'their big house';
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
@ -2805,7 +2807,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Down and up test', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(setupWidget());
|
||||
await setupWidget(tester);
|
||||
const String testValue = 'a big house';
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
@ -2827,7 +2829,7 @@ void main() {
|
||||
});
|
||||
|
||||
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
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
@ -2914,6 +2916,8 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
focusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
@ -2984,6 +2988,8 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
focusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
@ -3093,6 +3099,8 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
focusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
@ -16,8 +16,8 @@ import 'editable_text_utils.dart';
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
final TextEditingController controller = TextEditingController();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final FocusScopeNode focusScopeNode = FocusScopeNode();
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node');
|
||||
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'EditableText Scope Node');
|
||||
const TextStyle textStyle = TextStyle();
|
||||
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
|
||||
|
||||
@ -975,6 +975,9 @@ void main() {
|
||||
),
|
||||
));
|
||||
|
||||
focusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
semantics,
|
||||
includesNodeWith(
|
||||
@ -1532,6 +1535,8 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
focusNode.requestFocus();
|
||||
|
||||
// Now change it to make it obscure text.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: EditableText(
|
||||
@ -1906,7 +1911,7 @@ void main() {
|
||||
);
|
||||
final GlobalKey<EditableTextState> editableTextKey =
|
||||
GlobalKey<EditableTextState>();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'Test Focus Node');
|
||||
|
||||
await tester.pumpWidget(MaterialApp( // So we can show overlays.
|
||||
home: EditableText(
|
||||
|
523
packages/flutter/test/widgets/focus_manager_test.dart
Normal file
523
packages/flutter/test/widgets/focus_manager_test.dart
Normal file
@ -0,0 +1,523 @@
|
||||
// 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'
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,7 @@ void sendFakeKeyEvent(Map<String, dynamic> data) {
|
||||
BinaryMessages.handlePlatformMessage(
|
||||
SystemChannels.keyEvent.name,
|
||||
SystemChannels.keyEvent.codec.encodeMessage(data),
|
||||
(ByteData data) { },
|
||||
(ByteData data) {},
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,13 +29,15 @@ void main() {
|
||||
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
await tester.pumpWidget(RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: events.add,
|
||||
child: Container(),
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: events.add,
|
||||
child: Container(),
|
||||
),
|
||||
);
|
||||
|
||||
tester.binding.focusManager.rootScope.requestFocus(focusNode);
|
||||
focusNode.requestFocus();
|
||||
await tester.idle();
|
||||
|
||||
sendFakeKeyEvent(<String, dynamic>{
|
||||
@ -65,13 +67,15 @@ void main() {
|
||||
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
await tester.pumpWidget(RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: events.add,
|
||||
child: Container(),
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: events.add,
|
||||
child: Container(),
|
||||
),
|
||||
);
|
||||
|
||||
tester.binding.focusManager.rootScope.requestFocus(focusNode);
|
||||
focusNode.requestFocus();
|
||||
await tester.idle();
|
||||
|
||||
sendFakeKeyEvent(<String, dynamic>{
|
||||
|
@ -692,6 +692,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
||||
FlutterError.onError = _oldExceptionHandler;
|
||||
_pendingExceptionDetails = null;
|
||||
_parentZone = null;
|
||||
buildOwner.focusManager = FocusManager();
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user