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 {
|
class DemoButton extends StatefulWidget {
|
||||||
const DemoButton({this.name});
|
const DemoButton({this.name, this.canRequestFocus = true, this.autofocus = false});
|
||||||
|
|
||||||
final String name;
|
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() {
|
void _handleOnPressed() {
|
||||||
print('Button $name pressed.');
|
focusNode.requestFocus();
|
||||||
|
print('Button ${widget.name} pressed.');
|
||||||
|
debugDumpFocusTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlatButton(
|
return FlatButton(
|
||||||
|
focusNode: focusNode,
|
||||||
|
autofocus: widget.autofocus,
|
||||||
focusColor: Colors.red,
|
focusColor: Colors.red,
|
||||||
hoverColor: Colors.blue,
|
hoverColor: Colors.blue,
|
||||||
onPressed: () => _handleOnPressed(),
|
onPressed: () => _handleOnPressed(),
|
||||||
child: Text(name),
|
child: Text(widget.name),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,14 +153,20 @@ class _FocusDemoState extends State<FocusDemo> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: const <Widget>[
|
children: const <Widget>[
|
||||||
DemoButton(name: 'One'),
|
DemoButton(
|
||||||
|
name: 'One',
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: const <Widget>[
|
children: const <Widget>[
|
||||||
DemoButton(name: 'Two'),
|
DemoButton(name: 'Two'),
|
||||||
DemoButton(name: 'Three'),
|
DemoButton(
|
||||||
|
name: 'Three',
|
||||||
|
canRequestFocus: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
@ -367,8 +367,12 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||||||
FocusNode({
|
FocusNode({
|
||||||
String debugLabel,
|
String debugLabel,
|
||||||
FocusOnKeyCallback onKey,
|
FocusOnKeyCallback onKey,
|
||||||
this.skipTraversal = false,
|
bool skipTraversal = false,
|
||||||
|
bool canRequestFocus = true,
|
||||||
}) : assert(skipTraversal != null),
|
}) : assert(skipTraversal != null),
|
||||||
|
assert(canRequestFocus != null),
|
||||||
|
_skipTraversal = skipTraversal,
|
||||||
|
_canRequestFocus = canRequestFocus,
|
||||||
_onKey = onKey {
|
_onKey = onKey {
|
||||||
// Set it via the setter so that it does nothing on release builds.
|
// Set it via the setter so that it does nothing on release builds.
|
||||||
this.debugLabel = debugLabel;
|
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
|
/// 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
|
/// not traversed, allowing them to receive key events as part of the focus
|
||||||
/// chain, but not be traversed to via focus traversal.
|
/// 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].
|
/// 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
|
/// An iterator over the children that are allowed to be traversed by the
|
||||||
/// [FocusTraversalPolicy].
|
/// [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.
|
/// 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.
|
/// 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.
|
/// An [Iterable] over the ancestors of this node.
|
||||||
///
|
///
|
||||||
@ -733,8 +784,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||||||
if (node._parent == null) {
|
if (node._parent == null) {
|
||||||
_reparent(node);
|
_reparent(node);
|
||||||
}
|
}
|
||||||
assert(node.ancestors.contains(this),
|
assert(node.ancestors.contains(this), 'Focus was requested for a node that is not a descendant of the scope from which it was requested.');
|
||||||
'Focus was requested for a node that is not a descendant of the scope from which it was requested.');
|
|
||||||
node._doRequestFocus();
|
node._doRequestFocus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -743,6 +793,9 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||||||
|
|
||||||
// Note that this is overridden in FocusScopeNode.
|
// Note that this is overridden in FocusScopeNode.
|
||||||
void _doRequestFocus() {
|
void _doRequestFocus() {
|
||||||
|
if (!canRequestFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_setAsFocusedChild();
|
_setAsFocusedChild();
|
||||||
if (hasPrimaryFocus) {
|
if (hasPrimaryFocus) {
|
||||||
return;
|
return;
|
||||||
@ -795,6 +848,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
|
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(FlagProperty('hasFocus', value: hasFocus, ifTrue: 'FOCUSED', defaultValue: false));
|
||||||
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
|
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
|
||||||
}
|
}
|
||||||
@ -861,8 +915,7 @@ class FocusScopeNode extends FocusNode {
|
|||||||
///
|
///
|
||||||
/// Returns null if there is no currently focused child.
|
/// Returns null if there is no currently focused child.
|
||||||
FocusNode get focusedChild {
|
FocusNode get focusedChild {
|
||||||
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this,
|
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this, 'Focused child does not have the same idea of its enclosing scope as the scope does.');
|
||||||
'Focused child does not have the same idea of its enclosing scope as the scope does.');
|
|
||||||
return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
|
return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -904,14 +957,17 @@ class FocusScopeNode extends FocusNode {
|
|||||||
if (node._parent == null) {
|
if (node._parent == null) {
|
||||||
_reparent(node);
|
_reparent(node);
|
||||||
}
|
}
|
||||||
assert(node.ancestors.contains(this),
|
assert(node.ancestors.contains(this), 'Autofocus was requested for a node that is not a descendant of the scope from which it was requested.');
|
||||||
'Autofocus was requested for a node that is not a descendant of the scope from which it was requested.');
|
|
||||||
node._doRequestFocus();
|
node._doRequestFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void _doRequestFocus() {
|
void _doRequestFocus() {
|
||||||
|
if (!canRequestFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Start with the primary focus as the focused child of this scope, if there
|
// Start with the primary focus as the focused child of this scope, if there
|
||||||
// is one. Otherwise start with this node itself.
|
// is one. Otherwise start with this node itself.
|
||||||
FocusNode primaryFocus = focusedChild ?? this;
|
FocusNode primaryFocus = focusedChild ?? this;
|
||||||
|
@ -146,10 +146,10 @@ class Focus extends StatefulWidget {
|
|||||||
this.onFocusChange,
|
this.onFocusChange,
|
||||||
this.onKey,
|
this.onKey,
|
||||||
this.debugLabel,
|
this.debugLabel,
|
||||||
this.skipTraversal = false,
|
this.canRequestFocus,
|
||||||
|
this.skipTraversal,
|
||||||
}) : assert(child != null),
|
}) : assert(child != null),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
assert(skipTraversal != null),
|
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// A debug label for this widget.
|
/// 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
|
/// 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.
|
/// 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;
|
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
|
/// Returns the [focusNode] of the [Focus] that most tightly encloses the
|
||||||
/// given [BuildContext].
|
/// given [BuildContext].
|
||||||
///
|
///
|
||||||
@ -314,7 +339,8 @@ class _FocusState extends State<Focus> {
|
|||||||
// _createNode is overridden in _FocusScopeState.
|
// _createNode is overridden in _FocusScopeState.
|
||||||
_internalNode ??= _createNode();
|
_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);
|
_focusAttachment = focusNode.attach(context, onKey: widget.onKey);
|
||||||
_hasFocus = focusNode.hasFocus;
|
_hasFocus = focusNode.hasFocus;
|
||||||
|
|
||||||
@ -324,7 +350,13 @@ class _FocusState extends State<Focus> {
|
|||||||
focusNode.addListener(_handleFocusChanged);
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -332,6 +364,7 @@ class _FocusState extends State<Focus> {
|
|||||||
// listening to it.
|
// listening to it.
|
||||||
focusNode.removeListener(_handleFocusChanged);
|
focusNode.removeListener(_handleFocusChanged);
|
||||||
_focusAttachment.detach();
|
_focusAttachment.detach();
|
||||||
|
|
||||||
// Don't manage the lifetime of external nodes given to the widget, just the
|
// Don't manage the lifetime of external nodes given to the widget, just the
|
||||||
// internal node.
|
// internal node.
|
||||||
_internalNode?.dispose();
|
_internalNode?.dispose();
|
||||||
@ -367,14 +400,14 @@ class _FocusState extends State<Focus> {
|
|||||||
}());
|
}());
|
||||||
|
|
||||||
if (oldWidget.focusNode == widget.focusNode) {
|
if (oldWidget.focusNode == widget.focusNode) {
|
||||||
|
focusNode.skipTraversal = widget.skipTraversal ?? focusNode.skipTraversal;
|
||||||
|
focusNode.canRequestFocus = widget.canRequestFocus ?? focusNode.canRequestFocus;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_focusAttachment.detach();
|
_focusAttachment.detach();
|
||||||
focusNode.removeListener(_handleFocusChanged);
|
focusNode.removeListener(_handleFocusChanged);
|
||||||
_initNode();
|
_initNode();
|
||||||
|
|
||||||
_hasFocus = focusNode.hasFocus;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleFocusChanged() {
|
void _handleFocusChanged() {
|
||||||
|
@ -1151,6 +1151,96 @@ void main() {
|
|||||||
expect(gotFocus, isTrue);
|
expect(gotFocus, isTrue);
|
||||||
expect(node.hasFocus, 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 {
|
testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async {
|
||||||
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user