Unverified Commit d6938c56 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Adds canRequestFocus toggle to FocusNode (#38704)

* Add an 'unfocusable' focus node to allow developers to indicate when they don't want a Focus widget to be active

* more unfocusable changes. not working.

* Switch to focusable attribute

* Rename to canRequestFocus

* Turn off debug output

* Update docs

* Removed unused import
parent 34c69265
......@@ -13,22 +13,56 @@ void main() {
));
}
class DemoButton extends StatelessWidget {
const DemoButton({this.name});
class DemoButton extends StatefulWidget {
const DemoButton({this.name, this.canRequestFocus = true, this.autofocus = false});
final String name;
final bool canRequestFocus;
final bool autofocus;
@override
_DemoButtonState createState() => _DemoButtonState();
}
class _DemoButtonState extends State<DemoButton> {
FocusNode focusNode;
@override
void initState() {
super.initState();
focusNode = FocusNode(
debugLabel: widget.name,
canRequestFocus: widget.canRequestFocus,
);
}
@override
void dispose() {
focusNode?.dispose();
super.dispose();
}
@override
void didUpdateWidget(DemoButton oldWidget) {
super.didUpdateWidget(oldWidget);
focusNode.canRequestFocus = widget.canRequestFocus;
}
void _handleOnPressed() {
print('Button $name pressed.');
focusNode.requestFocus();
print('Button ${widget.name} pressed.');
debugDumpFocusTree();
}
@override
Widget build(BuildContext context) {
return FlatButton(
focusNode: focusNode,
autofocus: widget.autofocus,
focusColor: Colors.red,
hoverColor: Colors.blue,
onPressed: () => _handleOnPressed(),
child: Text(name),
child: Text(widget.name),
);
}
}
......@@ -119,14 +153,20 @@ class _FocusDemoState extends State<FocusDemo> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
DemoButton(name: 'One'),
DemoButton(
name: 'One',
autofocus: true,
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
DemoButton(name: 'Two'),
DemoButton(name: 'Three'),
DemoButton(
name: 'Three',
canRequestFocus: false,
),
],
),
Row(
......
......@@ -367,8 +367,12 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
FocusNode({
String debugLabel,
FocusOnKeyCallback onKey,
this.skipTraversal = false,
bool skipTraversal = false,
bool canRequestFocus = true,
}) : assert(skipTraversal != null),
assert(canRequestFocus != null),
_skipTraversal = skipTraversal,
_canRequestFocus = canRequestFocus,
_onKey = onKey {
// Set it via the setter so that it does nothing on release builds.
this.debugLabel = debugLabel;
......@@ -380,7 +384,50 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// This may be used to place nodes in the focus tree that may be focused, but
/// not traversed, allowing them to receive key events as part of the focus
/// chain, but not be traversed to via focus traversal.
bool skipTraversal;
///
/// This is different from [canRequestFocus] because it only implies that the
/// node can't be reached via traversal, not that it can't be focused. It may
/// still be focused explicitly.
bool get skipTraversal => _skipTraversal;
bool _skipTraversal;
set skipTraversal(bool value) {
if (value != _skipTraversal) {
_skipTraversal = value;
_notify();
}
}
/// If true, this focus node may request the primary focus.
///
/// Defaults to true. Set to false if you want this node to do nothing when
/// [requestFocus] is called on it. Does not affect the children of this node,
/// and [hasFocus] can still return true if this node is the ancestor of a
/// node with primary focus.
///
/// This is different than [skipTraversal] because [skipTraversal] still
/// allows the node to be focused, just not traversed to via the
/// [FocusTraversalPolicy]
///
/// Setting [canRequestFocus] to false implies that the node will also be
/// skipped for traversal purposes.
///
/// See also:
///
/// - [DefaultFocusTraversal], a widget that sets the traversal policy for
/// its descendants.
/// - [FocusTraversalPolicy], a class that can be extended to describe a
/// traversal policy.
bool get canRequestFocus => _canRequestFocus;
bool _canRequestFocus;
set canRequestFocus(bool value) {
if (value != _canRequestFocus) {
_canRequestFocus = value;
if (!_canRequestFocus) {
unfocus();
}
_notify();
}
}
/// The context that was supplied to [attach].
///
......@@ -413,7 +460,11 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// An iterator over the children that are allowed to be traversed by the
/// [FocusTraversalPolicy].
Iterable<FocusNode> get traversalChildren => children.where((FocusNode node) => !node.skipTraversal);
Iterable<FocusNode> get traversalChildren {
return children.where(
(FocusNode node) => !node.skipTraversal && node.canRequestFocus,
);
}
/// A debug label that is used for diagnostic output.
///
......@@ -440,7 +491,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
}
/// Returns all descendants which do not have the [skipTraversal] flag set.
Iterable<FocusNode> get traversalDescendants => descendants.where((FocusNode node) => !node.skipTraversal);
Iterable<FocusNode> get traversalDescendants => descendants.where((FocusNode node) => !node.skipTraversal && node.canRequestFocus);
/// An [Iterable] over the ancestors of this node.
///
......@@ -733,8 +784,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
if (node._parent == null) {
_reparent(node);
}
assert(node.ancestors.contains(this),
'Focus was requested for a node that is not a descendant of the scope from which it was requested.');
assert(node.ancestors.contains(this), 'Focus was requested for a node that is not a descendant of the scope from which it was requested.');
node._doRequestFocus();
return;
}
......@@ -743,6 +793,9 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
// Note that this is overridden in FocusScopeNode.
void _doRequestFocus() {
if (!canRequestFocus) {
return;
}
_setAsFocusedChild();
if (hasPrimaryFocus) {
return;
......@@ -795,6 +848,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: true));
properties.add(FlagProperty('hasFocus', value: hasFocus, ifTrue: 'FOCUSED', defaultValue: false));
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
}
......@@ -861,8 +915,7 @@ class FocusScopeNode extends FocusNode {
///
/// Returns null if there is no currently focused child.
FocusNode get focusedChild {
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this,
'Focused child does not have the same idea of its enclosing scope as the scope does.');
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this, 'Focused child does not have the same idea of its enclosing scope as the scope does.');
return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
}
......@@ -904,14 +957,17 @@ class FocusScopeNode extends FocusNode {
if (node._parent == null) {
_reparent(node);
}
assert(node.ancestors.contains(this),
'Autofocus was requested for a node that is not a descendant of the scope from which it was requested.');
assert(node.ancestors.contains(this), 'Autofocus was requested for a node that is not a descendant of the scope from which it was requested.');
node._doRequestFocus();
}
}
@override
void _doRequestFocus() {
if (!canRequestFocus) {
return;
}
// Start with the primary focus as the focused child of this scope, if there
// is one. Otherwise start with this node itself.
FocusNode primaryFocus = focusedChild ?? this;
......
......@@ -146,10 +146,10 @@ class Focus extends StatefulWidget {
this.onFocusChange,
this.onKey,
this.debugLabel,
this.skipTraversal = false,
this.canRequestFocus,
this.skipTraversal,
}) : assert(child != null),
assert(autofocus != null),
assert(skipTraversal != null),
super(key: key);
/// A debug label for this widget.
......@@ -224,8 +224,33 @@ class Focus extends StatefulWidget {
///
/// This is sometimes useful if a Focus widget should receive key events as
/// part of the focus chain, but shouldn't be accessible via focus traversal.
///
/// This is different from [canRequestFocus] because it only implies that the
/// widget can't be reached via traversal, not that it can't be focused. It may
/// still be focused explicitly.
final bool skipTraversal;
/// If true, this widget may request the primary focus.
///
/// Defaults to true. Set to false if you want the [FocusNode] this widget
/// manages to do nothing when [requestFocus] is called on it. Does not affect
/// the children of this node, and [FocusNode.hasFocus] can still return true
/// if this node is the ancestor of the primary focus.
///
/// This is different than [skipTraversal] because [skipTraversal] still
/// allows the widget to be focused, just not traversed to.
///
/// Setting [canRequestFocus] to false implies that the widget will also be
/// skipped for traversal purposes.
///
/// See also:
///
/// - [DefaultFocusTraversal], a widget that sets the traversal policy for
/// its descendants.
/// - [FocusTraversalPolicy], a class that can be extended to describe a
/// traversal policy.
final bool canRequestFocus;
/// Returns the [focusNode] of the [Focus] that most tightly encloses the
/// given [BuildContext].
///
......@@ -314,7 +339,8 @@ class _FocusState extends State<Focus> {
// _createNode is overridden in _FocusScopeState.
_internalNode ??= _createNode();
}
focusNode.skipTraversal = widget.skipTraversal;
focusNode.skipTraversal = widget.skipTraversal ?? focusNode.skipTraversal;
focusNode.canRequestFocus = widget.canRequestFocus ?? focusNode.canRequestFocus;
_focusAttachment = focusNode.attach(context, onKey: widget.onKey);
_hasFocus = focusNode.hasFocus;
......@@ -324,7 +350,13 @@ class _FocusState extends State<Focus> {
focusNode.addListener(_handleFocusChanged);
}
FocusNode _createNode() => FocusNode(debugLabel: widget.debugLabel);
FocusNode _createNode() {
return FocusNode(
debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus ?? true,
skipTraversal: widget.skipTraversal ?? false,
);
}
@override
void dispose() {
......@@ -332,6 +364,7 @@ class _FocusState extends State<Focus> {
// listening to it.
focusNode.removeListener(_handleFocusChanged);
_focusAttachment.detach();
// Don't manage the lifetime of external nodes given to the widget, just the
// internal node.
_internalNode?.dispose();
......@@ -367,14 +400,14 @@ class _FocusState extends State<Focus> {
}());
if (oldWidget.focusNode == widget.focusNode) {
focusNode.skipTraversal = widget.skipTraversal ?? focusNode.skipTraversal;
focusNode.canRequestFocus = widget.canRequestFocus ?? focusNode.canRequestFocus;
return;
}
_focusAttachment.detach();
focusNode.removeListener(_handleFocusChanged);
_initNode();
_hasFocus = focusNode.hasFocus;
}
void _handleFocusChanged() {
......
......@@ -1151,6 +1151,96 @@ void main() {
expect(gotFocus, isTrue);
expect(node.hasFocus, isTrue);
});
testWidgets('Focus is ignored when set to not focusable.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
bool gotFocus;
await tester.pumpWidget(
Focus(
canRequestFocus: false,
onFocusChange: (bool focused) => gotFocus = focused,
child: Container(key: key1),
),
);
final Element firstNode = tester.element(find.byKey(key1));
final FocusNode node = Focus.of(firstNode);
node.requestFocus();
await tester.pump();
expect(gotFocus, isNull);
expect(node.hasFocus, isFalse);
});
testWidgets('Focus is lost when set to not focusable.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
bool gotFocus;
await tester.pumpWidget(
Focus(
autofocus: true,
canRequestFocus: true,
onFocusChange: (bool focused) => gotFocus = focused,
child: Container(key: key1),
),
);
Element firstNode = tester.element(find.byKey(key1));
FocusNode node = Focus.of(firstNode);
node.requestFocus();
await tester.pump();
expect(gotFocus, isTrue);
expect(node.hasFocus, isTrue);
gotFocus = null;
await tester.pumpWidget(
Focus(
canRequestFocus: false,
onFocusChange: (bool focused) => gotFocus = focused,
child: Container(key: key1),
),
);
firstNode = tester.element(find.byKey(key1));
node = Focus.of(firstNode);
node.requestFocus();
await tester.pump();
expect(gotFocus, false);
expect(node.hasFocus, isFalse);
});
testWidgets('Child of unfocusable Focus can get focus.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode focusNode = FocusNode();
bool gotFocus;
await tester.pumpWidget(
Focus(
canRequestFocus: false,
onFocusChange: (bool focused) => gotFocus = focused,
child: Focus(key: key1, focusNode: focusNode, child: Container(key: key2)),
),
);
final Element childWidget = tester.element(find.byKey(key1));
final FocusNode unfocusableNode = Focus.of(childWidget);
unfocusableNode.requestFocus();
await tester.pump();
expect(gotFocus, isNull);
expect(unfocusableNode.hasFocus, isFalse);
final Element containerWidget = tester.element(find.byKey(key2));
final FocusNode focusableNode = Focus.of(containerWidget);
focusableNode.requestFocus();
await tester.pump();
expect(gotFocus, isTrue);
expect(unfocusableNode.hasFocus, isTrue);
});
});
testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
......
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