Commit 89aaaa9c authored by Adam Barth's avatar Adam Barth Committed by GitHub

Improve focus management (#9074)

We now have an explicit focus tree that we manage. Instead of using
GlobalKeys to manage focus, we use FocusNode and FocusScopeNode objects.
The FocusNode is Listenable and notifies when its focus state changes.

Focus notifications trigger by tree mutations are now delayed by one
frame, which is necessary to handle certain complex tree mutations. In
the common case of focus changes being triggered by user input, the
focus notificiation still arives in the same frame.
parent 60e05e9a
...@@ -5,34 +5,37 @@ ...@@ -5,34 +5,37 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
GlobalKey _key = new GlobalKey();
void main() { void main() {
runApp(new MaterialApp( runApp(new MaterialApp(
title: "Hardware Key Demo", title: 'Hardware Key Demo',
home: new Scaffold( home: new Scaffold(
appBar: new AppBar( appBar: new AppBar(
title: new Text("Hardware Key Demo") title: new Text('Hardware Key Demo'),
), ),
body: new Center( body: new Center(
child: new RawKeyboardDemo( child: new RawKeyboardDemo(),
key: _key ),
) ),
)
)
)); ));
} }
class RawKeyboardDemo extends StatefulWidget { class RawKeyboardDemo extends StatefulWidget {
RawKeyboardDemo({ GlobalKey key }) : super(key: key); RawKeyboardDemo({ Key key }) : super(key: key);
@override @override
_HardwareKeyDemoState createState() => new _HardwareKeyDemoState(); _HardwareKeyDemoState createState() => new _HardwareKeyDemoState();
} }
class _HardwareKeyDemoState extends State<RawKeyboardDemo> { class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
final FocusNode _focusNode = new FocusNode();
RawKeyEvent _event; RawKeyEvent _event;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
void _handleKeyEvent(RawKeyEvent event) { void _handleKeyEvent(RawKeyEvent event) {
setState(() { setState(() {
_event = event; _event = event;
...@@ -42,18 +45,24 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> { ...@@ -42,18 +45,24 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme; final TextTheme textTheme = Theme.of(context).textTheme;
final bool focused = Focus.at(context); return new RawKeyboardListener(
Widget child; focusNode: _focusNode,
if (!focused) { onKey: _handleKeyEvent,
child = new GestureDetector( child: new AnimatedBuilder(
animation: _focusNode,
builder: (BuildContext context, Widget child) {
if (!_focusNode.hasFocus) {
return new GestureDetector(
onTap: () { onTap: () {
Focus.moveTo(config.key); FocusScope.of(context).requestFocus(_focusNode);
}, },
child: new Text('Tap to focus', style: textTheme.display1), child: new Text('Tap to focus', style: textTheme.display1),
); );
} else if (_event == null) { }
child = new Text('Press a key', style: textTheme.display1);
} else { if (_event == null)
return new Text('Press a key', style: textTheme.display1);
int codePoint; int codePoint;
int keyCode; int keyCode;
int hidUsage; int hidUsage;
...@@ -65,7 +74,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> { ...@@ -65,7 +74,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
codePoint = data.codePoint; codePoint = data.codePoint;
hidUsage = data.hidUsage; hidUsage = data.hidUsage;
} }
child = new Column( return new Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
new Text('${_event.runtimeType}', style: textTheme.body2), new Text('${_event.runtimeType}', style: textTheme.body2),
...@@ -74,12 +83,8 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> { ...@@ -74,12 +83,8 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
new Text('hidUsage: $hidUsage', style: textTheme.display4), new Text('hidUsage: $hidUsage', style: textTheme.display4),
], ],
); );
} },
return new RawKeyboardListener( ),
focused: focused,
onKey: _handleKeyEvent,
child: child,
); );
} }
} }
...@@ -26,3 +26,4 @@ export 'src/foundation/platform.dart'; ...@@ -26,3 +26,4 @@ export 'src/foundation/platform.dart';
export 'src/foundation/print.dart'; export 'src/foundation/print.dart';
export 'src/foundation/serialization.dart'; export 'src/foundation/serialization.dart';
export 'src/foundation/synchronous_future.dart'; export 'src/foundation/synchronous_future.dart';
export 'src/foundation/tree_diagnostics_mixin.dart';
// Copyright 2017 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:meta/meta.dart';
/// A mixin that helps dump string representations of trees.
abstract class TreeDiagnosticsMixin {
@override
String toString() => '$runtimeType#$hashCode';
/// Returns a string representation of this node and its descendants.
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n';
final String childrenDescription = debugDescribeChildren(prefixOtherLines);
final String descriptionPrefix = childrenDescription != '' ? '$prefixOtherLines \u2502 ' : '$prefixOtherLines ';
final List<String> description = <String>[];
debugFillDescription(description);
result += description.map((String description) => '$descriptionPrefix$description\n').join();
if (childrenDescription == '') {
final String prefix = prefixOtherLines.trimRight();
if (prefix != '')
result += '$prefix\n';
} else {
result += childrenDescription;
}
return result;
}
/// Add additional information to the given description for use by [toStringDeep].
@protected
@mustCallSuper
void debugFillDescription(List<String> description) { }
/// Returns a description of this node's children for use by [toStringDeep].
@protected
String debugDescribeChildren(String prefix) => '';
}
...@@ -158,8 +158,7 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -158,8 +158,7 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
} }
LocalHistoryEntry _historyEntry; LocalHistoryEntry _historyEntry;
// TODO(abarth): This should be a GlobalValueKey when those exist. final FocusScopeNode _focusScopeNode = new FocusScopeNode();
GlobalKey get _drawerKey => new GlobalObjectKey(config.key);
void _ensureHistoryEntry() { void _ensureHistoryEntry() {
if (_historyEntry == null) { if (_historyEntry == null) {
...@@ -167,7 +166,7 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -167,7 +166,7 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
if (route != null) { if (route != null) {
_historyEntry = new LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved); _historyEntry = new LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
route.addLocalHistoryEntry(_historyEntry); route.addLocalHistoryEntry(_historyEntry);
Focus.moveScopeTo(_drawerKey, context: context); FocusScope.of(context).setFirstFocus(_focusScopeNode);
} }
} }
} }
...@@ -210,10 +209,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -210,10 +209,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
} }
} }
final GlobalKey _drawerKey = new GlobalKey();
double get _width { double get _width {
final RenderBox drawerBox = _drawerKey.currentContext?.findRenderObject(); final RenderBox box = _drawerKey.currentContext?.findRenderObject();
if (drawerBox != null) if (box != null)
return drawerBox.size.width; return box.size.width;
return _kWidth; // drawer not being shown currently return _kWidth; // drawer not being shown currently
} }
...@@ -286,8 +287,9 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -286,8 +287,9 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
alignment: FractionalOffset.centerRight, alignment: FractionalOffset.centerRight,
widthFactor: _controller.value, widthFactor: _controller.value,
child: new RepaintBoundary( child: new RepaintBoundary(
child: new Focus( child: new FocusScope(
key: _drawerKey, key: _drawerKey,
node: _focusScopeNode,
child: config.child child: config.child
), ),
), ),
......
...@@ -43,7 +43,7 @@ const Curve _kTransitionCurve = Curves.fastOutSlowIn; ...@@ -43,7 +43,7 @@ const Curve _kTransitionCurve = Curves.fastOutSlowIn;
class InputField extends StatefulWidget { class InputField extends StatefulWidget {
InputField({ InputField({
Key key, Key key,
this.focusKey, this.focusNode,
this.value, this.value,
this.keyboardType: TextInputType.text, this.keyboardType: TextInputType.text,
this.hintText, this.hintText,
...@@ -56,7 +56,10 @@ class InputField extends StatefulWidget { ...@@ -56,7 +56,10 @@ class InputField extends StatefulWidget {
this.onSubmitted, this.onSubmitted,
}) : super(key: key); }) : super(key: key);
final GlobalKey focusKey; /// Controls whether this widget has keyboard focus.
///
/// If null, this widget will create its own [FocusNode].
final FocusNode focusNode;
/// The current state of text of the input field. This includes the selected /// The current state of text of the input field. This includes the selected
/// text, if any, among other things. /// text, if any, among other things.
...@@ -109,9 +112,15 @@ class InputField extends StatefulWidget { ...@@ -109,9 +112,15 @@ class InputField extends StatefulWidget {
class _InputFieldState extends State<InputField> { class _InputFieldState extends State<InputField> {
final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>(); final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();
final GlobalKey<EditableTextState> _focusKey = new GlobalKey(debugLabel: "_InputFieldState _focusKey");
GlobalKey get focusKey => config.focusKey ?? (config.key is GlobalKey ? config.key : _focusKey); FocusNode _focusNode;
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
@override
void dispose() {
_focusNode?.dispose();
super.dispose();
}
void requestKeyboard() { void requestKeyboard() {
_editableTextKey.currentState?.requestKeyboard(); _editableTextKey.currentState?.requestKeyboard();
...@@ -126,19 +135,12 @@ class _InputFieldState extends State<InputField> { ...@@ -126,19 +135,12 @@ class _InputFieldState extends State<InputField> {
final List<Widget> stackChildren = <Widget>[ final List<Widget> stackChildren = <Widget>[
new GestureDetector( new GestureDetector(
key: focusKey == _focusKey ? _focusKey : null,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: requestKeyboard, onTap: requestKeyboard,
// Since the focusKey may have been created here, defer building the child: new EditableText(
// EditableText until the focusKey's context has been set. This is
// necessary because the EditableText will check the focus, like
// Focus.at(focusContext), when it builds.
child: new Builder(
builder: (BuildContext context) {
return new EditableText(
key: _editableTextKey, key: _editableTextKey,
value: value, value: value,
focusKey: focusKey, focusNode: _effectiveFocusNode,
style: textStyle, style: textStyle,
obscureText: config.obscureText, obscureText: config.obscureText,
maxLines: config.maxLines, maxLines: config.maxLines,
...@@ -149,8 +151,6 @@ class _InputFieldState extends State<InputField> { ...@@ -149,8 +151,6 @@ class _InputFieldState extends State<InputField> {
keyboardType: config.keyboardType, keyboardType: config.keyboardType,
onChanged: config.onChanged, onChanged: config.onChanged,
onSubmitted: config.onSubmitted, onSubmitted: config.onSubmitted,
);
}
), ),
), ),
]; ];
...@@ -432,6 +432,7 @@ class Input extends StatefulWidget { ...@@ -432,6 +432,7 @@ class Input extends StatefulWidget {
Input({ Input({
Key key, Key key,
this.value, this.value,
this.focusNode,
this.keyboardType: TextInputType.text, this.keyboardType: TextInputType.text,
this.icon, this.icon,
this.labelText, this.labelText,
...@@ -451,6 +452,11 @@ class Input extends StatefulWidget { ...@@ -451,6 +452,11 @@ class Input extends StatefulWidget {
/// text, if any, among other things. /// text, if any, among other things.
final InputValue value; final InputValue value;
/// Controls whether this widget has keyboard focus.
///
/// If null, this widget will create its own [FocusNode].
final FocusNode focusNode;
/// The type of keyboard to use for editing the text. /// The type of keyboard to use for editing the text.
final TextInputType keyboardType; final TextInputType keyboardType;
...@@ -522,26 +528,30 @@ class Input extends StatefulWidget { ...@@ -522,26 +528,30 @@ class Input extends StatefulWidget {
class _InputState extends State<Input> { class _InputState extends State<Input> {
final GlobalKey<_InputFieldState> _inputFieldKey = new GlobalKey<_InputFieldState>(); final GlobalKey<_InputFieldState> _inputFieldKey = new GlobalKey<_InputFieldState>();
final GlobalKey _focusKey = new GlobalKey();
GlobalKey get focusKey => config.key is GlobalKey ? config.key : _focusKey; FocusNode _focusNode;
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
@override
void dispose() {
_focusNode?.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isEmpty = (config.value ?? InputValue.empty).text.isEmpty;
final FocusNode focusNode = _effectiveFocusNode;
return new GestureDetector( return new GestureDetector(
key: focusKey == _focusKey ? _focusKey : null,
onTap: () { onTap: () {
_inputFieldKey.currentState?.requestKeyboard(); _inputFieldKey.currentState?.requestKeyboard();
}, },
// Since the focusKey may have been created here, defer building the child: new AnimatedBuilder(
// InputContainer until the focusKey's context has been set. This is animation: focusNode,
// necessary because we're passing the value of Focus.at() along. builder: (BuildContext context, Widget child) {
child: new Builder(
builder: (BuildContext context) {
final bool focused = Focus.at(focusKey.currentContext, autofocus: config.autofocus);
final bool isEmpty = (config.value ?? InputValue.empty).text.isEmpty;
return new InputContainer( return new InputContainer(
focused: focused, focused: focusNode.hasFocus,
isEmpty: isEmpty, isEmpty: isEmpty,
icon: config.icon, icon: config.icon,
labelText: config.labelText, labelText: config.labelText,
...@@ -550,9 +560,12 @@ class _InputState extends State<Input> { ...@@ -550,9 +560,12 @@ class _InputState extends State<Input> {
style: config.style, style: config.style,
isDense: config.isDense, isDense: config.isDense,
showDivider: config.showDivider, showDivider: config.showDivider,
child: child,
);
},
child: new InputField( child: new InputField(
key: _inputFieldKey, key: _inputFieldKey,
focusKey: focusKey, focusNode: focusNode,
value: config.value, value: config.value,
style: config.style, style: config.style,
obscureText: config.obscureText, obscureText: config.obscureText,
...@@ -562,8 +575,6 @@ class _InputState extends State<Input> { ...@@ -562,8 +575,6 @@ class _InputState extends State<Input> {
onChanged: config.onChanged, onChanged: config.onChanged,
onSubmitted: config.onSubmitted, onSubmitted: config.onSubmitted,
), ),
);
},
), ),
); );
} }
...@@ -643,7 +654,7 @@ class _InputState extends State<Input> { ...@@ -643,7 +654,7 @@ class _InputState extends State<Input> {
class TextField extends FormField<InputValue> { class TextField extends FormField<InputValue> {
TextField({ TextField({
Key key, Key key,
GlobalKey focusKey, FocusNode focusNode,
TextInputType keyboardType: TextInputType.text, TextInputType keyboardType: TextInputType.text,
Icon icon, Icon icon,
String labelText, String labelText,
...@@ -664,7 +675,7 @@ class TextField extends FormField<InputValue> { ...@@ -664,7 +675,7 @@ class TextField extends FormField<InputValue> {
validator: validator, validator: validator,
builder: (FormFieldState<InputValue> field) { builder: (FormFieldState<InputValue> field) {
return new Input( return new Input(
key: focusKey, focusNode: focusNode,
keyboardType: keyboardType, keyboardType: keyboardType,
icon: icon, icon: icon,
labelText: labelText, labelText: labelText,
......
...@@ -16,7 +16,7 @@ import 'debug.dart'; ...@@ -16,7 +16,7 @@ import 'debug.dart';
/// During painting, the render tree generates a tree of composited layers that /// During painting, the render tree generates a tree of composited layers that
/// are uploaded into the engine and displayed by the compositor. This class is /// are uploaded into the engine and displayed by the compositor. This class is
/// the base class for all composited layers. /// the base class for all composited layers.
abstract class Layer { abstract class Layer extends Object with TreeDiagnosticsMixin {
/// This layer's parent in the layer tree /// This layer's parent in the layer tree
ContainerLayer get parent => _parent; ContainerLayer get parent => _parent;
ContainerLayer _parent; ContainerLayer _parent;
...@@ -70,44 +70,18 @@ abstract class Layer { ...@@ -70,44 +70,18 @@ abstract class Layer {
/// origin of the builder's coordinate system. /// origin of the builder's coordinate system.
void addToScene(ui.SceneBuilder builder, Offset layerOffset); void addToScene(ui.SceneBuilder builder, Offset layerOffset);
@override
String toString() => '$runtimeType';
/// The object responsible for creating this layer. /// The object responsible for creating this layer.
/// ///
/// Defaults to the value of [RenderObject.debugCreator] for the render object /// Defaults to the value of [RenderObject.debugCreator] for the render object
/// that created this layer. Used in debug messages. /// that created this layer. Used in debug messages.
dynamic debugCreator; dynamic debugCreator;
/// Returns a string representation of this layer and its descendants. @override
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n';
final String childrenDescription = debugDescribeChildren(prefixOtherLines);
final String descriptionPrefix = childrenDescription != '' ? '$prefixOtherLines \u2502 ' : '$prefixOtherLines ';
final List<String> description = <String>[];
debugFillDescription(description);
result += description.map((String description) => "$descriptionPrefix$description\n").join();
if (childrenDescription == '') {
final String prefix = prefixOtherLines.trimRight();
if (prefix != '')
result += '$prefix\n';
} else {
result += childrenDescription;
}
return result;
}
/// Add additional information to the given description for use by [toStringDeep].
@protected
@mustCallSuper
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (debugCreator != null) if (debugCreator != null)
description.add('creator: $debugCreator'); description.add('creator: $debugCreator');
} }
/// Returns a description of this layer's children for use by [toStringDeep].
@protected
String debugDescribeChildren(String prefix) => '';
} }
/// A composited layer containing a [Picture] /// A composited layer containing a [Picture]
......
...@@ -14,6 +14,7 @@ import 'package:flutter/scheduler.dart'; ...@@ -14,6 +14,7 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'app.dart'; import 'app.dart';
import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
export 'dart:ui' show AppLifecycleState, Locale; export 'dart:ui' show AppLifecycleState, Locale;
...@@ -119,6 +120,14 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren ...@@ -119,6 +120,14 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
BuildOwner get buildOwner => _buildOwner; BuildOwner get buildOwner => _buildOwner;
final BuildOwner _buildOwner = new BuildOwner(); final BuildOwner _buildOwner = new BuildOwner();
/// The object in charge of the focus tree.
///
/// Rarely used directly. Instead, consider using [FocusScope.of] to obtain
/// the [FocusScopeNode] for a given [BuildContext].
///
/// See [FocusManager] for more details.
final FocusManager focusManager = new FocusManager();
final List<WidgetsBindingObserver> _observers = <WidgetsBindingObserver>[]; final List<WidgetsBindingObserver> _observers = <WidgetsBindingObserver>[];
/// Registers the given object as a binding observer. Binding /// Registers the given object as a binding observer. Binding
......
...@@ -9,7 +9,8 @@ import 'package:flutter/rendering.dart'; ...@@ -9,7 +9,8 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus.dart'; import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
import 'media_query.dart'; import 'media_query.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
...@@ -125,11 +126,12 @@ class InputValue { ...@@ -125,11 +126,12 @@ class InputValue {
class EditableText extends StatefulWidget { class EditableText extends StatefulWidget {
/// Creates a basic text input control. /// Creates a basic text input control.
/// ///
/// The [value] argument must not be null. /// The [value], [focusNode], [style], and [cursorColor] arguments must not
/// be null.
EditableText({ EditableText({
Key key, Key key,
@required this.value, @required this.value,
@required this.focusKey, @required this.focusNode,
this.obscureText: false, this.obscureText: false,
@required this.style, @required this.style,
@required this.cursorColor, @required this.cursorColor,
...@@ -143,7 +145,7 @@ class EditableText extends StatefulWidget { ...@@ -143,7 +145,7 @@ class EditableText extends StatefulWidget {
this.onSubmitted, this.onSubmitted,
}) : super(key: key) { }) : super(key: key) {
assert(value != null); assert(value != null);
assert(focusKey != null); assert(focusNode != null);
assert(obscureText != null); assert(obscureText != null);
assert(style != null); assert(style != null);
assert(cursorColor != null); assert(cursorColor != null);
...@@ -154,8 +156,8 @@ class EditableText extends StatefulWidget { ...@@ -154,8 +156,8 @@ class EditableText extends StatefulWidget {
/// The string being displayed in this widget. /// The string being displayed in this widget.
final InputValue value; final InputValue value;
/// Key of the enclosing widget that holds the focus. /// Controls whether this widget has keyboard focus.
final GlobalKey focusKey; final FocusNode focusNode;
/// Whether to hide the text being edited (e.g., for passwords). /// Whether to hide the text being edited (e.g., for passwords).
/// ///
...@@ -217,11 +219,23 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -217,11 +219,23 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
TextSelectionOverlay _selectionOverlay; TextSelectionOverlay _selectionOverlay;
final ScrollController _scrollController = new ScrollController(); final ScrollController _scrollController = new ScrollController();
bool _didAutoFocus = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_currentValue = config.value; _currentValue = config.value;
config.focusNode.addListener(_handleFocusChanged);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_didAutoFocus && config.autofocus) {
_didRequestKeyboard = true;
FocusScope.of(context).autofocus(config.focusNode);
_didAutoFocus = true;
}
} }
@override @override
...@@ -231,6 +245,10 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -231,6 +245,10 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
if (_isAttachedToKeyboard) if (_isAttachedToKeyboard)
_textInputConnection.setEditingState(_getTextEditingValueFromInputValue(_currentValue)); _textInputConnection.setEditingState(_getTextEditingValueFromInputValue(_currentValue));
} }
if (config.focusNode != oldConfig.focusNode) {
oldConfig.focusNode.removeListener(_handleFocusChanged);
config.focusNode.addListener(_handleFocusChanged);
}
} }
bool get _isAttachedToKeyboard => _textInputConnection != null && _textInputConnection.attached; bool get _isAttachedToKeyboard => _textInputConnection != null && _textInputConnection.attached;
...@@ -250,12 +268,10 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -250,12 +268,10 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
return scrollOffset; return scrollOffset;
} }
// True if the focus was explicitly requested last frame. This ensures we bool _didRequestKeyboard = false;
// don't show the keyboard when focus defaults back to the EditableText.
bool _requestingFocus = false;
void _attachOrDetachKeyboard(bool focused) { void _attachOrDetachKeyboard(bool focused) {
if (focused && !_isAttachedToKeyboard && (_requestingFocus || config.autofocus)) { if (focused && !_isAttachedToKeyboard && _didRequestKeyboard) {
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType)) _textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType))
..setEditingState(_getTextEditingValueFromInputValue(_currentValue)) ..setEditingState(_getTextEditingValueFromInputValue(_currentValue))
..show(); ..show();
...@@ -266,7 +282,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -266,7 +282,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
} }
_clearComposing(); _clearComposing();
} }
_requestingFocus = false; _didRequestKeyboard = false;
} }
void _clearComposing() { void _clearComposing() {
...@@ -286,10 +302,11 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -286,10 +302,11 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
if (_isAttachedToKeyboard) { if (_isAttachedToKeyboard) {
_textInputConnection.show(); _textInputConnection.show();
} else { } else {
Focus.moveTo(config.focusKey); _didRequestKeyboard = true;
setState(() { if (config.focusNode.hasFocus)
_requestingFocus = true; _attachOrDetachKeyboard(true);
}); else
FocusScope.of(context).requestFocus(config.focusNode);
} }
} }
...@@ -307,7 +324,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -307,7 +324,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
@override @override
void performAction(TextInputAction action) { void performAction(TextInputAction action) {
_clearComposing(); _clearComposing();
Focus.clear(context); config.focusNode.unfocus();
if (config.onSubmitted != null) if (config.onSubmitted != null)
config.onSubmitted(_currentValue); config.onSubmitted(_currentValue);
} }
...@@ -369,6 +386,25 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -369,6 +386,25 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
_cursorTimer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick); _cursorTimer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
} }
void _handleFocusChanged() {
final bool focused = config.focusNode.hasFocus;
_attachOrDetachKeyboard(focused);
if (_cursorTimer == null && focused && config.value.selection.isCollapsed)
_startCursorTimer();
else if (_cursorTimer != null && (!focused || !config.value.selection.isCollapsed))
_stopCursorTimer();
if (_selectionOverlay != null) {
if (focused) {
_selectionOverlay.update(config.value);
} else {
_selectionOverlay?.dispose();
_selectionOverlay = null;
}
}
}
@override @override
void dispose() { void dispose() {
if (_isAttachedToKeyboard) { if (_isAttachedToKeyboard) {
...@@ -381,6 +417,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -381,6 +417,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
assert(_cursorTimer == null); assert(_cursorTimer == null);
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
_selectionOverlay = null; _selectionOverlay = null;
config.focusNode.removeListener(_handleFocusChanged);
super.dispose(); super.dispose();
} }
...@@ -392,23 +429,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -392,23 +429,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool focused = Focus.at(config.focusKey.currentContext); FocusScope.of(context).reparentIfNeeded(config.focusNode);
_attachOrDetachKeyboard(focused);
if (_cursorTimer == null && focused && config.value.selection.isCollapsed)
_startCursorTimer();
else if (_cursorTimer != null && (!focused || !config.value.selection.isCollapsed))
_stopCursorTimer();
if (_selectionOverlay != null) {
if (focused) {
_selectionOverlay.update(config.value);
} else {
_selectionOverlay?.dispose();
_selectionOverlay = null;
}
}
return new Scrollable( return new Scrollable(
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
controller: _scrollController, controller: _scrollController,
......
This diff is collapsed.
This diff is collapsed.
// 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 'binding.dart';
import 'focus_manager.dart';
import 'framework.dart';
class _FocusScopeMarker extends InheritedWidget {
_FocusScopeMarker({
Key key,
@required this.node,
Widget child,
}) : super(key: key, child: child) {
assert(node != null);
}
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
/// focused widget often listens for keyboard events.
///
/// The 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:
///
/// ```dart
/// FocusScope.of(context).setFirstFocus(node);
/// ```
///
/// When a [FocusScope] is removed from the tree, the previously active
/// [FocusScope] becomes active again.
///
/// 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.
///
/// The [node] argument must not be null.
FocusScope({
Key key,
@required this.node,
this.autofocus: false,
this.child,
}) : super(key: key) {
assert(node != null);
assert(autofocus != null);
}
/// Controls whether this scope is currently active.
final FocusScopeNode node;
/// 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.
final Widget child;
static FocusScopeNode of(BuildContext context) {
final _FocusScopeMarker scope = context.inheritFromWidgetOfExactType(_FocusScopeMarker);
return scope?.node ?? WidgetsBinding.instance.focusManager.rootScope;
}
@override
_FocusScopeState createState() => new _FocusScopeState();
}
class _FocusScopeState extends State<FocusScope> {
bool _didAutofocus = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_didAutofocus && config.autofocus) {
FocusScope.of(context).setFirstFocus(config.node);
_didAutofocus = true;
}
}
@override
void dispose() {
config.node.detach();
super.dispose();
}
@override
Widget build(BuildContext context) {
FocusScope.of(context).reparentScopeIfNeeded(config.node);
return new Semantics(
container: true,
child: new _FocusScopeMarker(
node: config.node,
child: config.child,
),
);
}
}
...@@ -9,7 +9,8 @@ import 'package:flutter/rendering.dart'; ...@@ -9,7 +9,8 @@ import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
import 'focus.dart'; import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
...@@ -28,12 +29,6 @@ abstract class Route<T> { ...@@ -28,12 +29,6 @@ abstract class Route<T> {
/// The overlay entries for this route. /// The overlay entries for this route.
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[]; List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
/// The key this route will use for its root [Focus] widget, if any.
///
/// If this route is the first route shown by the navigator, the navigator
/// will initialize its [Focus] to this key.
GlobalKey get focusKey => null;
/// A future that completes when this route is popped off the navigator. /// A future that completes when this route is popped off the navigator.
/// ///
/// The future completes with the value given to [Navigator.pop], if any. /// The future completes with the value given to [Navigator.pop], if any.
...@@ -700,6 +695,9 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -700,6 +695,9 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final List<Route<dynamic>> _history = <Route<dynamic>>[]; final List<Route<dynamic>> _history = <Route<dynamic>>[];
final Set<Route<dynamic>> _poppedRoutes = new Set<Route<dynamic>>(); final Set<Route<dynamic>> _poppedRoutes = new Set<Route<dynamic>>();
/// The [FocusScopeNode] for the [FocusScope] that encloses the routes.
final FocusScopeNode focusScopeNode = new FocusScopeNode();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -736,6 +734,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -736,6 +734,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
route.dispose(); route.dispose();
_poppedRoutes.clear(); _poppedRoutes.clear();
_history.clear(); _history.clear();
focusScopeNode.detach();
super.dispose(); super.dispose();
assert(() { _debugLocked = false; return true; }); assert(() { _debugLocked = false; return true; });
} }
...@@ -1112,10 +1111,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1112,10 +1111,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
WidgetsBinding.instance.cancelPointer(pointer); WidgetsBinding.instance.cancelPointer(pointer);
} }
// TODO(abarth): We should be able to take a focusScopeKey as configuration
// information in case our parent wants to control whether we are focused.
final GlobalKey _focusScopeKey = new GlobalKey();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(!_debugLocked); assert(!_debugLocked);
...@@ -1127,9 +1122,9 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1127,9 +1122,9 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
onPointerCancel: _handlePointerUpOrCancel, onPointerCancel: _handlePointerUpOrCancel,
child: new AbsorbPointer( child: new AbsorbPointer(
absorbing: false, absorbing: false,
child: new Focus( child: new FocusScope(
key: _focusScopeKey, node: focusScopeNode,
initiallyFocusedScope: initialRoute.focusKey, autofocus: true,
child: new Overlay( child: new Overlay(
key: _overlayKey, key: _overlayKey,
initialEntries: initialRoute.overlayEntries, initialEntries: initialRoute.overlayEntries,
......
...@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; ...@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
/// A widget that calls a callback whenever the user presses or releases a key /// A widget that calls a callback whenever the user presses or releases a key
...@@ -29,18 +30,16 @@ class RawKeyboardListener extends StatefulWidget { ...@@ -29,18 +30,16 @@ class RawKeyboardListener extends StatefulWidget {
/// on-screen keyboards and input method editors (IMEs). /// on-screen keyboards and input method editors (IMEs).
RawKeyboardListener({ RawKeyboardListener({
Key key, Key key,
this.focused: false, @required this.focusNode,
this.onKey, @required this.onKey,
@required this.child, @required this.child,
}) : super(key: key) { }) : super(key: key) {
assert(focusNode != null);
assert(child != null); assert(child != null);
} }
/// Whether this widget should actually listen for raw keyboard events. /// Controls whether this widget has keyboard focus.
/// final FocusNode focusNode;
/// Typically set to the value returned by [Focus.at] for the [GlobalKey] of
/// the widget that builds the raw keyboard listener.
final bool focused;
/// Called whenever this widget receives a raw keyboard event. /// Called whenever this widget receives a raw keyboard event.
final ValueChanged<RawKeyEvent> onKey; final ValueChanged<RawKeyEvent> onKey;
...@@ -56,22 +55,26 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> { ...@@ -56,22 +55,26 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_attachOrDetachKeyboard(); config.focusNode.addListener(_handleFocusChanged);
} }
@override @override
void didUpdateConfig(RawKeyboardListener oldConfig) { void didUpdateConfig(RawKeyboardListener oldConfig) {
_attachOrDetachKeyboard(); if (config.focusNode != oldConfig.focusNode) {
oldConfig.focusNode.removeListener(_handleFocusChanged);
config.focusNode.addListener(_handleFocusChanged);
}
} }
@override @override
void dispose() { void dispose() {
config.focusNode.removeListener(_handleFocusChanged);
_detachKeyboardIfAttached(); _detachKeyboardIfAttached();
super.dispose(); super.dispose();
} }
void _attachOrDetachKeyboard() { void _handleFocusChanged() {
if (config.focused) if (config.focusNode.hasFocus)
_attachKeyboardIfDetached(); _attachKeyboardIfDetached();
else else
_detachKeyboardIfAttached(); _detachKeyboardIfAttached();
......
...@@ -7,7 +7,8 @@ import 'dart:async'; ...@@ -7,7 +7,8 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus.dart'; import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
import 'modal_barrier.dart'; import 'modal_barrier.dart';
import 'navigator.dart'; import 'navigator.dart';
...@@ -449,8 +450,8 @@ class _ModalScopeState extends State<_ModalScope> { ...@@ -449,8 +450,8 @@ class _ModalScopeState extends State<_ModalScope> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Focus( return new FocusScope(
key: config.route.focusKey, node: config.route.focusScopeNode,
child: new Offstage( child: new Offstage(
offstage: config.route.offstage, offstage: config.route.offstage,
child: new IgnorePointer( child: new IgnorePointer(
...@@ -575,8 +576,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -575,8 +576,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return child; return child;
} }
@override /// The node this route will use for its root [FocusScope] widget.
GlobalKey get focusKey => new GlobalObjectKey(this); final FocusScopeNode focusScopeNode = new FocusScopeNode();
@override @override
void install(OverlayEntry insertionPoint) { void install(OverlayEntry insertionPoint) {
...@@ -587,27 +588,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -587,27 +588,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
@override @override
Future<Null> didPush() { Future<Null> didPush() {
if (!settings.isInitialRoute) { navigator.focusScopeNode.setFirstFocus(focusScopeNode);
final BuildContext overlayContext = navigator.overlay?.context;
assert(() {
if (overlayContext == null) {
throw new FlutterError(
'Unable to find the BuildContext for the Navigator\'s overlay.\n'
'Did you remember to pass the settings object to the route\'s '
'constructor in your onGenerateRoute callback?'
);
}
return true;
});
Focus.moveScopeTo(focusKey, context: overlayContext);
}
return super.didPush(); return super.didPush();
} }
@override @override
void didPopNext(Route<dynamic> nextRoute) { void dispose() {
Focus.moveScopeTo(focusKey, context: navigator.overlay.context); focusScopeNode.detach();
super.didPopNext(nextRoute); super.dispose();
} }
// The API for subclasses to override - used by this class // The API for subclasses to override - used by this class
......
...@@ -21,7 +21,8 @@ export 'src/widgets/debug.dart'; ...@@ -21,7 +21,8 @@ export 'src/widgets/debug.dart';
export 'src/widgets/dismissible.dart'; export 'src/widgets/dismissible.dart';
export 'src/widgets/drag_target.dart'; export 'src/widgets/drag_target.dart';
export 'src/widgets/editable_text.dart'; export 'src/widgets/editable_text.dart';
export 'src/widgets/focus.dart'; export 'src/widgets/focus_manager.dart';
export 'src/widgets/focus_scope.dart';
export 'src/widgets/form.dart'; export 'src/widgets/form.dart';
export 'src/widgets/framework.dart'; export 'src/widgets/framework.dart';
export 'src/widgets/gesture_detector.dart'; export 'src/widgets/gesture_detector.dart';
......
// Copyright 2017 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 'package:test/test.dart';
class TestTree extends Object with TreeDiagnosticsMixin {
TestTree({
this.name,
this.children: const <TestTree>[],
});
final String name;
final List<TestTree> children;
@override
String debugDescribeChildren(String prefix) {
final StringBuffer buffer = new StringBuffer();
for (TestTree child in children)
buffer.write(child.toStringDeep('$prefix \u251C\u2500child ${child.name}: ', '$prefix \u2502'));
return buffer.toString();
}
}
void main() {
test('TreeDiagnosticsMixin control test', () async {
final TestTree tree = new TestTree(
children: <TestTree>[
new TestTree(name: 'node A'),
new TestTree(
name: 'node B',
children: <TestTree>[
new TestTree(name: 'node B1'),
new TestTree(name: 'node B2'),
new TestTree(name: 'node B3'),
],
),
new TestTree(name: 'node C'),
],
);
final String dump = tree.toStringDeep().replaceAll(new RegExp(r'#\d+'), '');
expect(dump, equals('''TestTree
├─child node A: TestTree
├─child node B: TestTree
│ ├─child node B1: TestTree
│ │
│ ├─child node B2: TestTree
│ │
│ ├─child node B3: TestTree
│ │
├─child node C: TestTree
'''));
});
}
...@@ -39,16 +39,16 @@ void main() { ...@@ -39,16 +39,16 @@ void main() {
}); });
testWidgets('Focus handling', (WidgetTester tester) async { testWidgets('Focus handling', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final FocusNode focusNode = new FocusNode();
await tester.pumpWidget(new MaterialApp( await tester.pumpWidget(new MaterialApp(
home: new Material( home: new Material(
child: new Center( child: new Center(
child: new Input(key: inputKey, autofocus: true) child: new Input(focusNode: focusNode, autofocus: true)
) )
) )
)); ));
expect(Focus.at(inputKey.currentContext), isTrue); expect(focusNode.hasFocus, isTrue);
}); });
testWidgets('Can show grid without losing sync', (WidgetTester tester) async { testWidgets('Can show grid without losing sync', (WidgetTester tester) async {
......
...@@ -137,7 +137,7 @@ void main() { ...@@ -137,7 +137,7 @@ void main() {
return new SampleForm( return new SampleForm(
callback: () => new Future<bool>.value(willPopValue), callback: () => new Future<bool>.value(willPopValue),
); );
} },
)); ));
}, },
), ),
......
...@@ -5,75 +5,103 @@ ...@@ -5,75 +5,103 @@
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 StatelessWidget { class TestFocusable extends StatefulWidget {
TestFocusable({ TestFocusable({
GlobalKey key, Key key,
this.no, this.no,
this.yes, this.yes,
this.autofocus: true this.autofocus: true,
}) : super(key: key); }) : super(key: key);
final String no; final String no;
final String yes; final String yes;
final bool autofocus; final bool autofocus;
@override
TestFocusableState createState() => new TestFocusableState();
}
class TestFocusableState extends State<TestFocusable> {
final FocusNode focusNode = new FocusNode();
bool _didAutofocus = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_didAutofocus && config.autofocus) {
_didAutofocus = true;
FocusScope.of(context).autofocus(focusNode);
}
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool focused = Focus.at(context, autofocus: autofocus);
return new GestureDetector( return new GestureDetector(
onTap: () { Focus.moveTo(key); }, onTap: () { FocusScope.of(context).requestFocus(focusNode); },
child: new Text(focused ? yes : no) child: new AnimatedBuilder(
animation: focusNode,
builder: (BuildContext context, Widget child) {
// print('focusNode.hasFocus = ${focusNode.hasFocus} ${focusNode.hashCode} ${focusNode.hasFocus ? config.yes : config.no}');
return new Text(focusNode.hasFocus ? config.yes : config.no);
},
),
); );
} }
} }
void main() { void main() {
testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async { testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async {
final GlobalKey keyFocus = new GlobalKey();
final GlobalKey keyA = new GlobalKey();
final GlobalKey keyB = new GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
new Focus( new Column(
key: keyFocus,
child: new Column(
children: <Widget>[ children: <Widget>[
new TestFocusable( new TestFocusable(
key: keyA,
no: 'a', no: 'a',
yes: 'A FOCUSED' yes: 'A FOCUSED',
), ),
new TestFocusable( new TestFocusable(
key: keyB,
no: 'b', no: 'b',
yes: 'B FOCUSED' yes: 'B FOCUSED',
),
],
), ),
]
)
)
); );
// Autofocus is delayed one frame.
await tester.pump();
expect(find.text('a'), findsNothing); expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing); expect(find.text('B FOCUSED'), findsNothing);
await tester.tap(find.text('A FOCUSED')); await tester.tap(find.text('A FOCUSED'));
await tester.idle();
await tester.pump(); await tester.pump();
expect(find.text('a'), findsNothing); expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing); expect(find.text('B FOCUSED'), findsNothing);
await tester.tap(find.text('A FOCUSED')); await tester.tap(find.text('A FOCUSED'));
await tester.idle();
await tester.pump(); await tester.pump();
expect(find.text('a'), findsNothing); expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing); expect(find.text('B FOCUSED'), findsNothing);
await tester.tap(find.text('b')); await tester.tap(find.text('b'));
await tester.idle();
await tester.pump(); await tester.pump();
expect(find.text('a'), findsOneWidget); expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing); expect(find.text('A FOCUSED'), findsNothing);
expect(find.text('b'), findsNothing); expect(find.text('b'), findsNothing);
expect(find.text('B FOCUSED'), findsOneWidget); expect(find.text('B FOCUSED'), findsOneWidget);
await tester.tap(find.text('a')); await tester.tap(find.text('a'));
await tester.idle();
await tester.pump(); await tester.pump();
expect(find.text('a'), findsNothing); expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
...@@ -82,30 +110,27 @@ void main() { ...@@ -82,30 +110,27 @@ void main() {
}); });
testWidgets('Can blur', (WidgetTester tester) async { testWidgets('Can blur', (WidgetTester tester) async {
final GlobalKey keyFocus = new GlobalKey();
final GlobalKey keyA = new GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
new Focus( new TestFocusable(
key: keyFocus,
child: new TestFocusable(
key: keyA,
no: 'a', no: 'a',
yes: 'A FOCUSED', yes: 'A FOCUSED',
autofocus: false autofocus: false,
) ),
)
); );
expect(find.text('a'), findsOneWidget); expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing); expect(find.text('A FOCUSED'), findsNothing);
Focus.moveTo(keyA); final TestFocusableState state = tester.state(find.byType(TestFocusable));
FocusScope.of(state.context).requestFocus(state.focusNode);
await tester.idle();
await tester.pump(); await tester.pump();
expect(find.text('a'), findsNothing); expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
Focus.clear(keyA.currentContext); state.focusNode.unfocus();
await tester.idle();
await tester.pump(); await tester.pump();
expect(find.text('a'), findsOneWidget); expect(find.text('a'), findsOneWidget);
...@@ -113,76 +138,77 @@ void main() { ...@@ -113,76 +138,77 @@ void main() {
}); });
testWidgets('Can move focus to scope', (WidgetTester tester) async { testWidgets('Can move focus to scope', (WidgetTester tester) async {
final GlobalKey keyParentFocus = new GlobalKey(); final FocusScopeNode parentFocusScope = new FocusScopeNode();
final GlobalKey keyChildFocus = new GlobalKey(); final FocusScopeNode childFocusScope = new FocusScopeNode();
final GlobalKey keyA = new GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
new Focus( new FocusScope(
key: keyParentFocus, node: parentFocusScope,
autofocus: true,
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
new TestFocusable( new TestFocusable(
key: keyA,
no: 'a', no: 'a',
yes: 'A FOCUSED', yes: 'A FOCUSED',
autofocus: false autofocus: false,
) ),
] ],
) ),
) ),
); );
expect(find.text('a'), findsOneWidget); expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing); expect(find.text('A FOCUSED'), findsNothing);
Focus.moveTo(keyA); final TestFocusableState state = tester.state(find.byType(TestFocusable));
FocusScope.of(state.context).requestFocus(state.focusNode);
await tester.idle();
await tester.pump(); await tester.pump();
expect(find.text('a'), findsNothing); expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
Focus.moveScopeTo(keyChildFocus, context: keyA.currentContext); parentFocusScope.setFirstFocus(childFocusScope);
await tester.idle();
await tester.pumpWidget( await tester.pumpWidget(
new Focus( new FocusScope(
key: keyParentFocus, node: parentFocusScope,
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
new TestFocusable( new TestFocusable(
key: keyA,
no: 'a', no: 'a',
yes: 'A FOCUSED', yes: 'A FOCUSED',
autofocus: false autofocus: false,
), ),
new Focus( new FocusScope(
key: keyChildFocus, node: childFocusScope,
child: new Container( child: new Container(
width: 50.0, width: 50.0,
height: 50.0 height: 50.0,
) ),
) ),
] ],
) ),
) ),
); );
expect(find.text('a'), findsOneWidget); expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing); expect(find.text('A FOCUSED'), findsNothing);
await tester.pumpWidget( await tester.pumpWidget(
new Focus( new FocusScope(
key: keyParentFocus, node: parentFocusScope,
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
new TestFocusable( new TestFocusable(
key: keyA,
no: 'a', no: 'a',
yes: 'A FOCUSED', yes: 'A FOCUSED',
autofocus: false autofocus: false,
) ),
] ],
) ),
) ),
); );
// Focus has received the removal notification but we haven't rebuilt yet. // Focus has received the removal notification but we haven't rebuilt yet.
...@@ -193,5 +219,7 @@ void main() { ...@@ -193,5 +219,7 @@ void main() {
expect(find.text('a'), findsNothing); expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
parentFocusScope.detach();
}); });
} }
...@@ -120,7 +120,6 @@ void main() { ...@@ -120,7 +120,6 @@ void main() {
testWidgets('Multiple Inputs communicate', (WidgetTester tester) async { testWidgets('Multiple Inputs communicate', (WidgetTester tester) async {
final GlobalKey<FormState> formKey = new GlobalKey<FormState>(); final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
final GlobalKey<FormFieldState<InputValue>> fieldKey = new GlobalKey<FormFieldState<InputValue>>(); final GlobalKey<FormFieldState<InputValue>> fieldKey = new GlobalKey<FormFieldState<InputValue>>();
final GlobalKey focusKey = new GlobalKey();
// Input 2's validator depends on a input 1's value. // Input 2's validator depends on a input 1's value.
String errorText(InputValue input) => fieldKey.currentState.value?.text.toString() + '/error'; String errorText(InputValue input) => fieldKey.currentState.value?.text.toString() + '/error';
...@@ -130,21 +129,18 @@ void main() { ...@@ -130,21 +129,18 @@ void main() {
child: new Form( child: new Form(
key: formKey, key: formKey,
autovalidate: true, autovalidate: true,
child: new Focus(
key: focusKey,
child: new ListView( child: new ListView(
children: <Widget>[ children: <Widget>[
new TextField( new TextField(
key: fieldKey key: fieldKey,
), ),
new TextField( new TextField(
validator: errorText, validator: errorText,
), ),
] ],
) ),
),
), ),
)
)
); );
} }
......
...@@ -795,26 +795,22 @@ void main() { ...@@ -795,26 +795,22 @@ void main() {
testWidgets('Input label text animates', (WidgetTester tester) async { testWidgets('Input label text animates', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final GlobalKey inputKey = new GlobalKey();
final GlobalKey focusKey = new GlobalKey();
Widget innerBuilder() { Widget innerBuilder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Focus(
key: focusKey,
child: new Column( child: new Column(
children: <Widget>[ children: <Widget>[
new Input( new Input(
labelText: 'First' labelText: 'First',
), ),
new Input( new Input(
key: inputKey, key: inputKey,
labelText: 'Second' labelText: 'Second',
),
],
),
), ),
]
)
)
)
); );
} }
Widget builder() => overlay(innerBuilder()); Widget builder() => overlay(innerBuilder());
...@@ -825,6 +821,7 @@ void main() { ...@@ -825,6 +821,7 @@ void main() {
// Focus the Input. The label should start animating upwards. // Focus the Input. The label should start animating upwards.
await tester.tap(find.byKey(inputKey)); await tester.tap(find.byKey(inputKey));
await tester.idle();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
......
...@@ -177,18 +177,6 @@ void main() { ...@@ -177,18 +177,6 @@ void main() {
expect('$exception', startsWith('Navigator operation requested with a context')); expect('$exception', startsWith('Navigator operation requested with a context'));
}); });
testWidgets('Missing settings in onGenerateRoute throws exception', (WidgetTester tester) async {
await tester.pumpWidget(new Navigator(
onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<Null>(
builder: (BuildContext context) => new Container()
);
}
));
final Object exception = tester.takeException();
expect(exception is FlutterError, isTrue);
});
testWidgets('Gestures between push and build are ignored', (WidgetTester tester) async { testWidgets('Gestures between push and build are ignored', (WidgetTester tester) async {
final List<String> log = <String>[]; final List<String> log = <String>[];
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
......
...@@ -15,16 +15,20 @@ void sendFakeKeyEvent(Map<String, dynamic> data) { ...@@ -15,16 +15,20 @@ void sendFakeKeyEvent(Map<String, dynamic> data) {
void main() { void main() {
testWidgets('Can dispose without keyboard', (WidgetTester tester) async { testWidgets('Can dispose without keyboard', (WidgetTester tester) async {
await tester.pumpWidget(new RawKeyboardListener(child: new Container())); final FocusNode focusNode = new FocusNode();
await tester.pumpWidget(new RawKeyboardListener(child: new Container())); await tester.pumpWidget(new RawKeyboardListener(focusNode: focusNode, onKey: null, child: new Container()));
await tester.pumpWidget(new RawKeyboardListener(focusNode: focusNode, onKey: null, child: new Container()));
await tester.pumpWidget(new Container()); await tester.pumpWidget(new Container());
}); });
testWidgets('Fuchsia key event', (WidgetTester tester) async { testWidgets('Fuchsia key event', (WidgetTester tester) async {
final List<RawKeyEvent> events = <RawKeyEvent>[]; final List<RawKeyEvent> events = <RawKeyEvent>[];
final FocusNode focusNode = new FocusNode();
tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.pumpWidget(new RawKeyboardListener( await tester.pumpWidget(new RawKeyboardListener(
focused: true, focusNode: focusNode,
onKey: events.add, onKey: events.add,
child: new Container(), child: new Container(),
)); ));
...@@ -46,14 +50,20 @@ void main() { ...@@ -46,14 +50,20 @@ void main() {
expect(typedData.hidUsage, 0x04); expect(typedData.hidUsage, 0x04);
expect(typedData.codePoint, 0x64); expect(typedData.codePoint, 0x64);
expect(typedData.modifiers, 0x08); expect(typedData.modifiers, 0x08);
await tester.pumpWidget(new Container());
focusNode.dispose();
}); });
testWidgets('Defunct listeners do not receive events', testWidgets('Defunct listeners do not receive events',
(WidgetTester tester) async { (WidgetTester tester) async {
final List<RawKeyEvent> events = <RawKeyEvent>[]; final List<RawKeyEvent> events = <RawKeyEvent>[];
final FocusNode focusNode = new FocusNode();
tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.pumpWidget(new RawKeyboardListener( await tester.pumpWidget(new RawKeyboardListener(
focused: true, focusNode: focusNode,
onKey: events.add, onKey: events.add,
child: new Container(), child: new Container(),
)); ));
...@@ -84,5 +94,8 @@ void main() { ...@@ -84,5 +94,8 @@ void main() {
await tester.idle(); await tester.idle();
expect(events.length, 0); expect(events.length, 0);
await tester.pumpWidget(new Container());
focusNode.dispose();
}); });
} }
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