Add ExcludeFocus widget, and a way to prevent focusability for a subtree. (#55756)

This adds an ExcludeFocus widget that prevents widgets in a subtree from having or obtaining focus. It also adds the ability for a FocusNode to conditionally prevent its children from being focusable when it isn't focusable (i.e. when canRequestFocus is false).

It does this by adding an descendantsAreFocusable attribute to the FocusNode, which, when false, prevents the descendants of the node from being focusable (and removes focus from them if they are currently focused).
This commit is contained in:
Greg Spencer 2020-05-01 14:36:46 -07:00 committed by GitHub
parent 27eee14c6e
commit fdc4d21b79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 562 additions and 221 deletions

View file

@ -407,16 +407,20 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
///
/// The [debugLabel] is ignored on release builds.
///
/// The [skipTraversal] and [canRequestFocus] arguments must not be null.
/// The [skipTraversal], [descendantsAreFocusable], and [canRequestFocus]
/// arguments must not be null.
FocusNode({
String debugLabel,
FocusOnKeyCallback onKey,
bool skipTraversal = false,
bool canRequestFocus = true,
bool descendantsAreFocusable = true,
}) : assert(skipTraversal != null),
assert(canRequestFocus != null),
assert(descendantsAreFocusable != null),
_skipTraversal = skipTraversal,
_canRequestFocus = canRequestFocus,
_descendantsAreFocusable = descendantsAreFocusable,
_onKey = onKey {
// Set it via the setter so that it does nothing on release builds.
this.debugLabel = debugLabel;
@ -469,22 +473,71 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// * [FocusTraversalPolicy], a class that can be extended to describe a
/// traversal policy.
bool get canRequestFocus {
if (!_canRequestFocus) {
return false;
}
final FocusScopeNode scope = enclosingScope;
return _canRequestFocus && (scope == null || scope.canRequestFocus);
if (scope != null && !scope.canRequestFocus) {
return false;
}
for (final FocusNode ancestor in ancestors) {
if (!ancestor.descendantsAreFocusable) {
return false;
}
}
return true;
}
bool _canRequestFocus;
@mustCallSuper
set canRequestFocus(bool value) {
if (value != _canRequestFocus) {
if (!value) {
// Have to set this first before unfocusing, since it checks this to cull
// unfocusable, previously-focused children.
_canRequestFocus = value;
if (hasFocus && !value) {
unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
}
_canRequestFocus = value;
_manager?._markPropertiesChanged(this);
}
}
/// If false, will disable focus for all of this node's descendants.
///
/// Defaults to true. Does not affect focusability of this node: for that,
/// use [canRequestFocus].
///
/// If any descendants are focused when this is set to false, they will be
/// unfocused. When `descendantsAreFocusable` is set to true again, they will
/// not be refocused, although they will be able to accept focus again.
///
/// Does not affect the value of [canRequestFocus] on the descendants.
///
/// See also:
///
/// * [ExcludeFocus], a widget that uses this property to conditionally
/// exclude focus for a subtree.
/// * [Focus], a widget that exposes this setting as a parameter.
/// * [FocusTraversalGroup], a widget used to group together and configure
/// the focus traversal policy for a widget subtree that also has an
/// `descendantsAreFocusable` parameter that prevents its children from
/// being focused.
bool get descendantsAreFocusable => _descendantsAreFocusable;
bool _descendantsAreFocusable;
@mustCallSuper
set descendantsAreFocusable(bool value) {
if (value == _descendantsAreFocusable) {
return;
}
if (!value && hasFocus) {
for (final FocusNode child in children) {
child.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
}
}
_descendantsAreFocusable = value;
_manager?._markPropertiesChanged(this);
}
/// The context that was supplied to [attach].
///
/// This is typically the context for the widget that is being focused, as it
@ -1082,6 +1135,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
properties.add(FlagProperty('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true));
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: true));
properties.add(FlagProperty('hasFocus', value: hasFocus && !hasPrimaryFocus, ifTrue: 'IN FOCUS PATH', defaultValue: false));
properties.add(FlagProperty('hasPrimaryFocus', value: hasPrimaryFocus, ifTrue: 'PRIMARY FOCUS', defaultValue: false));
@ -1147,6 +1201,7 @@ class FocusScopeNode extends FocusNode {
debugLabel: debugLabel,
onKey: onKey,
canRequestFocus: canRequestFocus,
descendantsAreFocusable: true,
skipTraversal: skipTraversal,
);

View file

@ -9,8 +9,6 @@ import 'focus_manager.dart';
import 'framework.dart';
import 'inherited_notifier.dart';
// TODO(gspencergoog): Add more information about unfocus here once https://github.com/flutter/flutter/pull/50831 lands.
/// A widget that manages a [FocusNode] to allow keyboard focus to be given
/// to this widget and its descendants.
///
@ -282,10 +280,12 @@ class Focus extends StatefulWidget {
this.onKey,
this.debugLabel,
this.canRequestFocus,
this.descendantsAreFocusable = true,
this.skipTraversal,
this.includeSemantics = true,
}) : assert(child != null),
assert(autofocus != null),
assert(descendantsAreFocusable != null),
assert(includeSemantics != null),
super(key: key);
@ -401,6 +401,29 @@ class Focus extends StatefulWidget {
/// {@endtemplate}
final bool canRequestFocus;
/// {@template flutter.widgets.Focus.descendantsAreFocusable}
/// If false, will make this widget's descendants unfocusable.
///
/// Defaults to true. Does not affect focusability of this node (just its
/// descendants): for that, use [canRequestFocus].
///
/// If any descendants are focused when this is set to false, they will be
/// unfocused. When `descendantsAreFocusable` is set to true again, they will
/// not be refocused, although they will be able to accept focus again.
///
/// Does not affect the value of [canRequestFocus] on the descendants.
///
/// See also:
///
/// * [ExcludeFocus], a widget that uses this property to conditionally
/// exclude focus for a subtree.
/// * [FocusTraversalGroup], a widget used to group together and configure
/// the focus traversal policy for a widget subtree that has a
/// `descendantsAreFocusable` parameter to conditionally block focus for a
/// subtree.
/// {@endtemplate}
final bool descendantsAreFocusable;
/// Returns the [focusNode] of the [Focus] that most tightly encloses the
/// given [BuildContext].
///
@ -469,7 +492,9 @@ class Focus extends StatefulWidget {
super.debugFillProperties(properties);
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false));
properties.add(DiagnosticsProperty<FocusNode>('node', focusNode, defaultValue: null));
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: false));
properties.add(FlagProperty('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
}
@override
@ -481,6 +506,7 @@ class _FocusState extends State<Focus> {
FocusNode get focusNode => widget.focusNode ?? _internalNode;
bool _hasPrimaryFocus;
bool _canRequestFocus;
bool _descendantsAreFocusable;
bool _didAutofocus = false;
FocusAttachment _focusAttachment;
@ -497,6 +523,9 @@ class _FocusState extends State<Focus> {
// _createNode is overridden in _FocusScopeState.
_internalNode ??= _createNode();
}
if (widget.descendantsAreFocusable != null) {
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
}
if (widget.skipTraversal != null) {
focusNode.skipTraversal = widget.skipTraversal;
}
@ -504,6 +533,7 @@ class _FocusState extends State<Focus> {
focusNode.canRequestFocus = widget.canRequestFocus;
}
_canRequestFocus = focusNode.canRequestFocus;
_descendantsAreFocusable = focusNode.descendantsAreFocusable;
_hasPrimaryFocus = focusNode.hasPrimaryFocus;
_focusAttachment = focusNode.attach(context, onKey: widget.onKey);
@ -517,6 +547,7 @@ class _FocusState extends State<Focus> {
return FocusNode(
debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus ?? true,
descendantsAreFocusable: widget.descendantsAreFocusable ?? false,
skipTraversal: widget.skipTraversal ?? false,
);
}
@ -580,6 +611,9 @@ class _FocusState extends State<Focus> {
if (widget.canRequestFocus != null) {
focusNode.canRequestFocus = widget.canRequestFocus;
}
if (widget.descendantsAreFocusable != null) {
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
}
} else {
_focusAttachment.detach();
focusNode.removeListener(_handleFocusChanged);
@ -594,6 +628,7 @@ class _FocusState extends State<Focus> {
void _handleFocusChanged() {
final bool hasPrimaryFocus = focusNode.hasPrimaryFocus;
final bool canRequestFocus = focusNode.canRequestFocus;
final bool descendantsAreFocusable = focusNode.descendantsAreFocusable;
if (widget.onFocusChange != null) {
widget.onFocusChange(focusNode.hasFocus);
}
@ -607,6 +642,11 @@ class _FocusState extends State<Focus> {
_canRequestFocus = canRequestFocus;
});
}
if (_descendantsAreFocusable != descendantsAreFocusable) {
setState(() {
_descendantsAreFocusable = descendantsAreFocusable;
});
}
}
@override
@ -901,3 +941,64 @@ class _FocusMarker extends InheritedNotifier<FocusNode> {
assert(child != null),
super(key: key, notifier: node, child: child);
}
/// A widget that controls whether or not the descendants of this widget are
/// focusable.
///
/// Does not affect the value of [canRequestFocus] on the descendants.
///
/// See also:
///
/// * [Focus], a widget for adding and managing a [FocusNode] in the widget tree.
/// * [FocusTraversalGroup], a widget that groups widgets for focus traversal,
/// and can also be used in the same way as this widget by setting its
/// `descendantsAreFocusable` attribute.
class ExcludeFocus extends StatelessWidget {
/// Const constructor for [ExcludeFocus] widget.
///
/// The [excluding] argument must not be null.
///
/// The [child] argument is required, and must not be null.
const ExcludeFocus({
Key key,
this.excluding = true,
@required this.child,
}) : assert(excluding != null),
assert(child != null),
super(key: key);
/// If true, will make this widget's descendants unfocusable.
///
/// Defaults to true.
///
/// If any descendants are focused when this is set to true, they will be
/// unfocused. When `excluding` is set to false again, they will not be
/// refocused, although they will be able to accept focus again.
///
/// Does not affect the value of [canRequestFocus] on the descendants.
///
/// See also:
///
/// * [Focus.descendantsAreFocusable], the attribute of a [Focus] widget that
/// controls this same property for focus widgets.
/// * [FocusTraversalGroup], a widget used to group together and configure
/// the focus traversal policy for a widget subtree that has a
/// `descendantsAreFocusable` parameter to conditionally block focus for a
/// subtree.
final bool excluding;
/// The child widget of this [ExcludeFocus].
///
/// {@macro flutter.widgets.child}
final Widget child;
@override
Widget build(BuildContext context) {
return Focus(
canRequestFocus: false,
skipTraversal: true,
descendantsAreFocusable: !excluding,
child: child,
);
}
}

View file

@ -1421,6 +1421,9 @@ class FocusTraversalOrder extends InheritedWidget {
///
/// By default, traverses in reading order using [ReadingOrderTraversalPolicy].
///
/// To prevent the members of the group from being focused, set the
/// [descendantsAreFocusable] attribute to true.
///
/// {@tool dartpad --template=stateless_widget_material}
/// This sample shows three rows of buttons, each grouped by a
/// [FocusTraversalGroup], each with different traversal order policies. Use tab
@ -1583,19 +1586,16 @@ class FocusTraversalOrder extends InheritedWidget {
class FocusTraversalGroup extends StatefulWidget {
/// Creates a [FocusTraversalGroup] object.
///
/// The [child] argument must not be null.
/// The [child] and [descendantsAreFocusable] arguments must not be null.
FocusTraversalGroup({
Key key,
FocusTraversalPolicy policy,
this.descendantsAreFocusable = true,
@required this.child,
}) : policy = policy ?? ReadingOrderTraversalPolicy(),
}) : assert(descendantsAreFocusable != null),
policy = policy ?? ReadingOrderTraversalPolicy(),
super(key: key);
/// The child widget of this [FocusTraversalGroup].
///
/// {@macro flutter.widgets.child}
final Widget child;
/// The policy used to move the focus from one focus node to another when
/// traversing them using a keyboard.
///
@ -1613,6 +1613,14 @@ class FocusTraversalGroup extends StatefulWidget {
/// bottom.
final FocusTraversalPolicy policy;
/// {@macro flutter.widgets.Focus.descendantsAreFocusable}
final bool descendantsAreFocusable;
/// The child widget of this [FocusTraversalGroup].
///
/// {@macro flutter.widgets.child}
final Widget child;
/// Returns the focus policy set by the [FocusTraversalGroup] that most
/// tightly encloses the given [BuildContext].
///
@ -1691,6 +1699,7 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
canRequestFocus: false,
skipTraversal: true,
includeSemantics: false,
descendantsAreFocusable: widget.descendantsAreFocusable,
child: widget.child,
),
);

View file

@ -97,6 +97,53 @@ void main() {
expect(focusNode1.offset, equals(const Offset(300.0, 8.0)));
expect(focusNode2.offset, equals(const Offset(443.0, 194.5)));
});
testWidgets('descendantsAreFocusable disables focus for descendants.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope);
parent2Attachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent2);
child1.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, equals(child1));
expect(scope.focusedChild, equals(child1));
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isTrue);
parent2.descendantsAreFocusable = false;
// Node should still be focusable, even if descendants are not.
parent2.requestFocus();
await tester.pump();
expect(parent2.hasPrimaryFocus, isTrue);
child2.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child2)));
expect(tester.binding.focusManager.primaryFocus, equals(parent2));
expect(scope.focusedChild, equals(parent2));
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isFalse);
parent1.descendantsAreFocusable = false;
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child2)));
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child1)));
expect(scope.focusedChild, equals(parent2));
expect(scope.traversalDescendants.contains(child1), isFalse);
expect(scope.traversalDescendants.contains(child2), isFalse);
});
testWidgets('implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
FocusNode(
@ -105,9 +152,10 @@ void main() {
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'context: null',
'descendantsAreFocusable: true',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false'
'hasPrimaryFocus: false',
]);
});
});
@ -949,6 +997,7 @@ void main() {
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'context: null',
'descendantsAreFocusable: true',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false'

View file

@ -990,6 +990,7 @@ void main() {
expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget);
});
testWidgets('Can focus root node.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
await tester.pumpWidget(
@ -1008,6 +1009,7 @@ void main() {
expect(rootNode.hasFocus, isTrue);
expect(rootNode, equals(firstElement.owner.focusManager.rootScope));
});
testWidgets('Can autofocus a node.', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
await tester.pumpWidget(
@ -1031,6 +1033,7 @@ void main() {
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
});
testWidgets("Won't autofocus a node if one is already focused.", (WidgetTester tester) async {
final FocusNode focusNodeA = FocusNode(debugLabel: 'Test Node A');
final FocusNode focusNodeB = FocusNode(debugLabel: 'Test Node B');
@ -1070,6 +1073,7 @@ void main() {
expect(focusNodeA.hasPrimaryFocus, isTrue);
});
});
group(Focus, () {
testWidgets('Focus.of stops at the nearest Focus widget.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
@ -1195,6 +1199,7 @@ void main() {
expect(nodes.length, equals(2));
expect(keys, equals(<Key>[key7, key8]));
});
testWidgets('Can set focus.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
bool gotFocus;
@ -1214,6 +1219,7 @@ 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;
@ -1234,6 +1240,7 @@ void main() {
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;
@ -1273,6 +1280,7 @@ void main() {
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');
@ -1304,237 +1312,318 @@ void main() {
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');
bool gotFocus;
await tester.pumpWidget(
FocusScope(
child: Focus(
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, isTrue);
expect(node.hasFocus, isTrue);
await tester.pumpWidget(Container());
expect(FocusManager.instance.rootScope.descendants, isEmpty);
});
testWidgets('Focus widgets set Semantics information about focus', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey();
await tester.pumpWidget(
TestFocus(key: key, name: 'a'),
);
final SemanticsNode semantics = tester.getSemantics(find.byKey(key));
expect(key.currentState.focusNode.hasFocus, isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue);
FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode);
await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isTrue);
expect(semantics.hasFlag(SemanticsFlag.isFocused), isTrue);
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue);
key.currentState.focusNode.canRequestFocus = false;
await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isFalse);
expect(key.currentState.focusNode.canRequestFocus, isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isFalse);
});
testWidgets('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey();
final TestFocus testFocus = TestFocus(key: key, name: 'a');
await tester.pumpWidget(
testFocus,
);
await tester.pumpAndSettle();
key.currentState.built = false;
key.currentState.focusNode.canRequestFocus = false;
await tester.pumpAndSettle();
key.currentState.built = true;
expect(key.currentState.focusNode.canRequestFocus, isFalse);
});
testWidgets('canRequestFocus causes descendants of scope to be skipped.', (WidgetTester tester) async {
final GlobalKey scope1 = GlobalKey(debugLabel: 'scope1');
final GlobalKey scope2 = GlobalKey(debugLabel: 'scope2');
final GlobalKey focus1 = GlobalKey(debugLabel: 'focus1');
final GlobalKey focus2 = GlobalKey(debugLabel: 'focus2');
final GlobalKey container1 = GlobalKey(debugLabel: 'container');
Future<void> pumpTest({
bool allowScope1 = true,
bool allowScope2 = true,
bool allowFocus1 = true,
bool allowFocus2 = true,
}) async {
testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
bool gotFocus;
await tester.pumpWidget(
FocusScope(
key: scope1,
canRequestFocus: allowScope1,
child: FocusScope(
key: scope2,
canRequestFocus: allowScope2,
child: Focus(
key: focus1,
canRequestFocus: allowFocus1,
child: Focus(
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, isTrue);
expect(node.hasFocus, isTrue);
await tester.pumpWidget(Container());
expect(FocusManager.instance.rootScope.descendants, isEmpty);
});
testWidgets('Focus widgets set Semantics information about focus', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey();
await tester.pumpWidget(
TestFocus(key: key, name: 'a'),
);
final SemanticsNode semantics = tester.getSemantics(find.byKey(key));
expect(key.currentState.focusNode.hasFocus, isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue);
FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode);
await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isTrue);
expect(semantics.hasFlag(SemanticsFlag.isFocused), isTrue);
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue);
key.currentState.focusNode.canRequestFocus = false;
await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isFalse);
expect(key.currentState.focusNode.canRequestFocus, isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isFalse);
});
testWidgets('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey();
final TestFocus testFocus = TestFocus(key: key, name: 'a');
await tester.pumpWidget(
testFocus,
);
await tester.pumpAndSettle();
key.currentState.built = false;
key.currentState.focusNode.canRequestFocus = false;
await tester.pumpAndSettle();
key.currentState.built = true;
expect(key.currentState.focusNode.canRequestFocus, isFalse);
});
testWidgets('canRequestFocus causes descendants of scope to be skipped.', (WidgetTester tester) async {
final GlobalKey scope1 = GlobalKey(debugLabel: 'scope1');
final GlobalKey scope2 = GlobalKey(debugLabel: 'scope2');
final GlobalKey focus1 = GlobalKey(debugLabel: 'focus1');
final GlobalKey focus2 = GlobalKey(debugLabel: 'focus2');
final GlobalKey container1 = GlobalKey(debugLabel: 'container');
Future<void> pumpTest({
bool allowScope1 = true,
bool allowScope2 = true,
bool allowFocus1 = true,
bool allowFocus2 = true,
}) async {
await tester.pumpWidget(
FocusScope(
key: scope1,
canRequestFocus: allowScope1,
child: FocusScope(
key: scope2,
canRequestFocus: allowScope2,
child: Focus(
key: focus2,
canRequestFocus: allowFocus2,
child: Container(
key: container1,
key: focus1,
canRequestFocus: allowFocus1,
child: Focus(
key: focus2,
canRequestFocus: allowFocus2,
child: Container(
key: container1,
),
),
),
),
),
),
);
);
await tester.pump();
}
// Check childless node (focus2).
await pumpTest();
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
}
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
await pumpTest(allowFocus2: false);
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
await pumpTest();
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
// Check childless node (focus2).
await pumpTest();
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
await pumpTest(allowFocus2: false);
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
await pumpTest();
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
// Check FocusNode with child (focus1). Shouldn't affect children.
await pumpTest(allowFocus1: false);
expect(Focus.of(container1.currentContext).hasFocus, isTrue); // focus2 has focus.
Focus.of(focus2.currentContext).requestFocus(); // Try to focus focus1
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue); // focus2 still has focus.
Focus.of(container1.currentContext).requestFocus(); // Now try to focus focus2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
await pumpTest();
// Try again, now that we've set focus1's canRequestFocus to true again.
Focus.of(container1.currentContext).unfocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
// Check FocusNode with child (focus1). Shouldn't affect children.
await pumpTest(allowFocus1: false);
expect(Focus.of(container1.currentContext).hasFocus, isTrue); // focus2 has focus.
Focus.of(focus2.currentContext).requestFocus(); // Try to focus focus1
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue); // focus2 still has focus.
Focus.of(container1.currentContext).requestFocus(); // Now try to focus focus2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
await pumpTest();
// Try again, now that we've set focus1's canRequestFocus to true again.
Focus.of(container1.currentContext).unfocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
// Check FocusScopeNode with only FocusNode children (scope2). Should affect children.
await pumpTest(allowScope2: false);
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
FocusScope.of(focus1.currentContext).requestFocus(); // Try to focus scope2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(focus2.currentContext).requestFocus(); // Try to focus focus1
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(container1.currentContext).requestFocus(); // Try to focus focus2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
await pumpTest();
// Try again, now that we've set scope2's canRequestFocus to true again.
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
// Check FocusScopeNode with only FocusNode children (scope2). Should affect children.
await pumpTest(allowScope2: false);
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
FocusScope.of(focus1.currentContext).requestFocus(); // Try to focus scope2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(focus2.currentContext).requestFocus(); // Try to focus focus1
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(container1.currentContext).requestFocus(); // Try to focus focus2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
await pumpTest();
// Try again, now that we've set scope2's canRequestFocus to true again.
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
// Check FocusScopeNode with both FocusNode children and FocusScope children (scope1). Should affect children.
await pumpTest(allowScope1: false);
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
FocusScope.of(scope2.currentContext).requestFocus(); // Try to focus scope1
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
FocusScope.of(focus1.currentContext).requestFocus(); // Try to focus scope2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(focus2.currentContext).requestFocus(); // Try to focus focus1
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(container1.currentContext).requestFocus(); // Try to focus focus2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
await pumpTest();
// Try again, now that we've set scope1's canRequestFocus to true again.
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
});
// Check FocusScopeNode with both FocusNode children and FocusScope children (scope1). Should affect children.
await pumpTest(allowScope1: false);
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
FocusScope.of(scope2.currentContext).requestFocus(); // Try to focus scope1
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
FocusScope.of(focus1.currentContext).requestFocus(); // Try to focus scope2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(focus2.currentContext).requestFocus(); // Try to focus focus1
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
Focus.of(container1.currentContext).requestFocus(); // Try to focus focus2
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isFalse);
await pumpTest();
// Try again, now that we've set scope1's canRequestFocus to true again.
Focus.of(container1.currentContext).requestFocus();
await tester.pump();
expect(Focus.of(container1.currentContext).hasFocus, isTrue);
});
testWidgets('skipTraversal works as expected.', (WidgetTester tester) async {
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
final FocusNode focus1 = FocusNode(debugLabel: 'focus1');
final FocusNode focus2 = FocusNode(debugLabel: 'focus2');
testWidgets('skipTraversal works as expected.', (WidgetTester tester) async {
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
final FocusNode focus1 = FocusNode(debugLabel: 'focus1');
final FocusNode focus2 = FocusNode(debugLabel: 'focus2');
Future<void> pumpTest({
bool traverseScope1 = false,
bool traverseScope2 = false,
bool traverseFocus1 = false,
bool traverseFocus2 = false,
}) async {
await tester.pumpWidget(
FocusScope(
node: scope1,
skipTraversal: traverseScope1,
child: FocusScope(
node: scope2,
skipTraversal: traverseScope2,
child: Focus(
focusNode: focus1,
skipTraversal: traverseFocus1,
Future<void> pumpTest({
bool traverseScope1 = false,
bool traverseScope2 = false,
bool traverseFocus1 = false,
bool traverseFocus2 = false,
}) async {
await tester.pumpWidget(
FocusScope(
node: scope1,
skipTraversal: traverseScope1,
child: FocusScope(
node: scope2,
skipTraversal: traverseScope2,
child: Focus(
focusNode: focus2,
skipTraversal: traverseFocus2,
child: Container(),
focusNode: focus1,
skipTraversal: traverseFocus1,
child: Focus(
focusNode: focus2,
skipTraversal: traverseFocus2,
child: Container(),
),
),
),
),
);
await tester.pump();
}
await pumpTest();
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2]));
// Check childless node (focus2).
await pumpTest(traverseFocus2: true);
expect(scope1.traversalDescendants, equals(<FocusNode>[focus1, scope2]));
// Check FocusNode with child (focus1). Shouldn't affect children.
await pumpTest(traverseFocus1: true);
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, scope2]));
// Check FocusScopeNode with only FocusNode children (scope2). Should affect children.
await pumpTest(traverseScope2: true);
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1]));
// Check FocusScopeNode with both FocusNode children and FocusScope children (scope1). Should affect children.
await pumpTest(traverseScope1: true);
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2]));
});
testWidgets('descendantsAreFocusable works as expected.', (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(
descendantsAreFocusable: false,
child: Focus(
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);
final Element containerWidget = tester.element(find.byKey(key2));
final FocusNode containerNode = Focus.of(containerWidget);
unfocusableNode.requestFocus();
await tester.pump();
}
await pumpTest();
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2]));
expect(gotFocus, isNull);
expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse);
// Check childless node (focus2).
await pumpTest(traverseFocus2: true);
expect(scope1.traversalDescendants, equals(<FocusNode>[focus1, scope2]));
containerNode.requestFocus();
await tester.pump();
// Check FocusNode with child (focus1). Shouldn't affect children.
await pumpTest(traverseFocus1: true);
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, scope2]));
expect(gotFocus, isNull);
expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse);
});
});
group(ExcludeFocus, () {
testWidgets("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode focusNode = FocusNode();
bool gotFocus;
await tester.pumpWidget(
ExcludeFocus(
excluding: true,
child: Focus(
onFocusChange: (bool focused) => gotFocus = focused,
child: Focus(
key: key1,
focusNode: focusNode,
child: Container(key: key2),
),
),
),
);
// Check FocusScopeNode with only FocusNode children (scope2). Should affect children.
await pumpTest(traverseScope2: true);
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1]));
final Element childWidget = tester.element(find.byKey(key1));
final FocusNode unfocusableNode = Focus.of(childWidget);
final Element containerWidget = tester.element(find.byKey(key2));
final FocusNode containerNode = Focus.of(containerWidget);
// Check FocusScopeNode with both FocusNode children and FocusScope children (scope1). Should affect children.
await pumpTest(traverseScope1: true);
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2]));
unfocusableNode.requestFocus();
await tester.pump();
expect(gotFocus, isNull);
expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse);
containerNode.requestFocus();
await tester.pump();
expect(gotFocus, isNull);
expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse);
});
});
}

View file

@ -1962,6 +1962,44 @@ void main() {
final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics));
});
testWidgets("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode focusNode = FocusNode();
bool gotFocus;
await tester.pumpWidget(
FocusTraversalGroup(
descendantsAreFocusable: false,
child: Focus(
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);
final Element containerWidget = tester.element(find.byKey(key2));
final FocusNode containerNode = Focus.of(containerWidget);
unfocusableNode.requestFocus();
await tester.pump();
expect(gotFocus, isNull);
expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse);
containerNode.requestFocus();
await tester.pump();
expect(gotFocus, isNull);
expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse);
});
});
}