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
This commit is contained in:
parent
34c692659e
commit
d6938c56d9
@ -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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user