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

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).
parent 27eee14c
...@@ -407,16 +407,20 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -407,16 +407,20 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// ///
/// The [debugLabel] is ignored on release builds. /// 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({ FocusNode({
String debugLabel, String debugLabel,
FocusOnKeyCallback onKey, FocusOnKeyCallback onKey,
bool skipTraversal = false, bool skipTraversal = false,
bool canRequestFocus = true, bool canRequestFocus = true,
bool descendantsAreFocusable = true,
}) : assert(skipTraversal != null), }) : assert(skipTraversal != null),
assert(canRequestFocus != null), assert(canRequestFocus != null),
assert(descendantsAreFocusable != null),
_skipTraversal = skipTraversal, _skipTraversal = skipTraversal,
_canRequestFocus = canRequestFocus, _canRequestFocus = canRequestFocus,
_descendantsAreFocusable = descendantsAreFocusable,
_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;
...@@ -469,22 +473,71 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -469,22 +473,71 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// * [FocusTraversalPolicy], a class that can be extended to describe a /// * [FocusTraversalPolicy], a class that can be extended to describe a
/// traversal policy. /// traversal policy.
bool get canRequestFocus { bool get canRequestFocus {
if (!_canRequestFocus) {
return false;
}
final FocusScopeNode scope = enclosingScope; 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; bool _canRequestFocus;
@mustCallSuper @mustCallSuper
set canRequestFocus(bool value) { set canRequestFocus(bool value) {
if (value != _canRequestFocus) { 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); unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
} }
_canRequestFocus = value;
_manager?._markPropertiesChanged(this); _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]. /// The context that was supplied to [attach].
/// ///
/// This is typically the context for the widget that is being focused, as it /// This is typically the context for the widget that is being focused, as it
...@@ -1082,6 +1135,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -1082,6 +1135,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('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true));
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', 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('hasFocus', value: hasFocus && !hasPrimaryFocus, ifTrue: 'IN FOCUS PATH', defaultValue: false));
properties.add(FlagProperty('hasPrimaryFocus', value: hasPrimaryFocus, ifTrue: 'PRIMARY FOCUS', defaultValue: false)); properties.add(FlagProperty('hasPrimaryFocus', value: hasPrimaryFocus, ifTrue: 'PRIMARY FOCUS', defaultValue: false));
...@@ -1147,6 +1201,7 @@ class FocusScopeNode extends FocusNode { ...@@ -1147,6 +1201,7 @@ class FocusScopeNode extends FocusNode {
debugLabel: debugLabel, debugLabel: debugLabel,
onKey: onKey, onKey: onKey,
canRequestFocus: canRequestFocus, canRequestFocus: canRequestFocus,
descendantsAreFocusable: true,
skipTraversal: skipTraversal, skipTraversal: skipTraversal,
); );
......
...@@ -9,8 +9,6 @@ import 'focus_manager.dart'; ...@@ -9,8 +9,6 @@ import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
import 'inherited_notifier.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 /// A widget that manages a [FocusNode] to allow keyboard focus to be given
/// to this widget and its descendants. /// to this widget and its descendants.
/// ///
...@@ -282,10 +280,12 @@ class Focus extends StatefulWidget { ...@@ -282,10 +280,12 @@ class Focus extends StatefulWidget {
this.onKey, this.onKey,
this.debugLabel, this.debugLabel,
this.canRequestFocus, this.canRequestFocus,
this.descendantsAreFocusable = true,
this.skipTraversal, this.skipTraversal,
this.includeSemantics = true, this.includeSemantics = true,
}) : assert(child != null), }) : assert(child != null),
assert(autofocus != null), assert(autofocus != null),
assert(descendantsAreFocusable != null),
assert(includeSemantics != null), assert(includeSemantics != null),
super(key: key); super(key: key);
...@@ -401,6 +401,29 @@ class Focus extends StatefulWidget { ...@@ -401,6 +401,29 @@ class Focus extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final bool canRequestFocus; 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 /// Returns the [focusNode] of the [Focus] that most tightly encloses the
/// given [BuildContext]. /// given [BuildContext].
/// ///
...@@ -469,7 +492,9 @@ class Focus extends StatefulWidget { ...@@ -469,7 +492,9 @@ class Focus extends StatefulWidget {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null)); properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false)); 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 @override
...@@ -481,6 +506,7 @@ class _FocusState extends State<Focus> { ...@@ -481,6 +506,7 @@ class _FocusState extends State<Focus> {
FocusNode get focusNode => widget.focusNode ?? _internalNode; FocusNode get focusNode => widget.focusNode ?? _internalNode;
bool _hasPrimaryFocus; bool _hasPrimaryFocus;
bool _canRequestFocus; bool _canRequestFocus;
bool _descendantsAreFocusable;
bool _didAutofocus = false; bool _didAutofocus = false;
FocusAttachment _focusAttachment; FocusAttachment _focusAttachment;
...@@ -497,6 +523,9 @@ class _FocusState extends State<Focus> { ...@@ -497,6 +523,9 @@ class _FocusState extends State<Focus> {
// _createNode is overridden in _FocusScopeState. // _createNode is overridden in _FocusScopeState.
_internalNode ??= _createNode(); _internalNode ??= _createNode();
} }
if (widget.descendantsAreFocusable != null) {
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
}
if (widget.skipTraversal != null) { if (widget.skipTraversal != null) {
focusNode.skipTraversal = widget.skipTraversal; focusNode.skipTraversal = widget.skipTraversal;
} }
...@@ -504,6 +533,7 @@ class _FocusState extends State<Focus> { ...@@ -504,6 +533,7 @@ class _FocusState extends State<Focus> {
focusNode.canRequestFocus = widget.canRequestFocus; focusNode.canRequestFocus = widget.canRequestFocus;
} }
_canRequestFocus = focusNode.canRequestFocus; _canRequestFocus = focusNode.canRequestFocus;
_descendantsAreFocusable = focusNode.descendantsAreFocusable;
_hasPrimaryFocus = focusNode.hasPrimaryFocus; _hasPrimaryFocus = focusNode.hasPrimaryFocus;
_focusAttachment = focusNode.attach(context, onKey: widget.onKey); _focusAttachment = focusNode.attach(context, onKey: widget.onKey);
...@@ -517,6 +547,7 @@ class _FocusState extends State<Focus> { ...@@ -517,6 +547,7 @@ class _FocusState extends State<Focus> {
return FocusNode( return FocusNode(
debugLabel: widget.debugLabel, debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus ?? true, canRequestFocus: widget.canRequestFocus ?? true,
descendantsAreFocusable: widget.descendantsAreFocusable ?? false,
skipTraversal: widget.skipTraversal ?? false, skipTraversal: widget.skipTraversal ?? false,
); );
} }
...@@ -580,6 +611,9 @@ class _FocusState extends State<Focus> { ...@@ -580,6 +611,9 @@ class _FocusState extends State<Focus> {
if (widget.canRequestFocus != null) { if (widget.canRequestFocus != null) {
focusNode.canRequestFocus = widget.canRequestFocus; focusNode.canRequestFocus = widget.canRequestFocus;
} }
if (widget.descendantsAreFocusable != null) {
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
}
} else { } else {
_focusAttachment.detach(); _focusAttachment.detach();
focusNode.removeListener(_handleFocusChanged); focusNode.removeListener(_handleFocusChanged);
...@@ -594,6 +628,7 @@ class _FocusState extends State<Focus> { ...@@ -594,6 +628,7 @@ class _FocusState extends State<Focus> {
void _handleFocusChanged() { void _handleFocusChanged() {
final bool hasPrimaryFocus = focusNode.hasPrimaryFocus; final bool hasPrimaryFocus = focusNode.hasPrimaryFocus;
final bool canRequestFocus = focusNode.canRequestFocus; final bool canRequestFocus = focusNode.canRequestFocus;
final bool descendantsAreFocusable = focusNode.descendantsAreFocusable;
if (widget.onFocusChange != null) { if (widget.onFocusChange != null) {
widget.onFocusChange(focusNode.hasFocus); widget.onFocusChange(focusNode.hasFocus);
} }
...@@ -607,6 +642,11 @@ class _FocusState extends State<Focus> { ...@@ -607,6 +642,11 @@ class _FocusState extends State<Focus> {
_canRequestFocus = canRequestFocus; _canRequestFocus = canRequestFocus;
}); });
} }
if (_descendantsAreFocusable != descendantsAreFocusable) {
setState(() {
_descendantsAreFocusable = descendantsAreFocusable;
});
}
} }
@override @override
...@@ -901,3 +941,64 @@ class _FocusMarker extends InheritedNotifier<FocusNode> { ...@@ -901,3 +941,64 @@ class _FocusMarker extends InheritedNotifier<FocusNode> {
assert(child != null), assert(child != null),
super(key: key, notifier: node, child: child); 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,
);
}
}
...@@ -1421,6 +1421,9 @@ class FocusTraversalOrder extends InheritedWidget { ...@@ -1421,6 +1421,9 @@ class FocusTraversalOrder extends InheritedWidget {
/// ///
/// By default, traverses in reading order using [ReadingOrderTraversalPolicy]. /// 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} /// {@tool dartpad --template=stateless_widget_material}
/// This sample shows three rows of buttons, each grouped by a /// This sample shows three rows of buttons, each grouped by a
/// [FocusTraversalGroup], each with different traversal order policies. Use tab /// [FocusTraversalGroup], each with different traversal order policies. Use tab
...@@ -1583,19 +1586,16 @@ class FocusTraversalOrder extends InheritedWidget { ...@@ -1583,19 +1586,16 @@ class FocusTraversalOrder extends InheritedWidget {
class FocusTraversalGroup extends StatefulWidget { class FocusTraversalGroup extends StatefulWidget {
/// Creates a [FocusTraversalGroup] object. /// Creates a [FocusTraversalGroup] object.
/// ///
/// The [child] argument must not be null. /// The [child] and [descendantsAreFocusable] arguments must not be null.
FocusTraversalGroup({ FocusTraversalGroup({
Key key, Key key,
FocusTraversalPolicy policy, FocusTraversalPolicy policy,
this.descendantsAreFocusable = true,
@required this.child, @required this.child,
}) : policy = policy ?? ReadingOrderTraversalPolicy(), }) : assert(descendantsAreFocusable != null),
policy = policy ?? ReadingOrderTraversalPolicy(),
super(key: key); 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 /// The policy used to move the focus from one focus node to another when
/// traversing them using a keyboard. /// traversing them using a keyboard.
/// ///
...@@ -1613,6 +1613,14 @@ class FocusTraversalGroup extends StatefulWidget { ...@@ -1613,6 +1613,14 @@ class FocusTraversalGroup extends StatefulWidget {
/// bottom. /// bottom.
final FocusTraversalPolicy policy; 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 /// Returns the focus policy set by the [FocusTraversalGroup] that most
/// tightly encloses the given [BuildContext]. /// tightly encloses the given [BuildContext].
/// ///
...@@ -1691,6 +1699,7 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> { ...@@ -1691,6 +1699,7 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
canRequestFocus: false, canRequestFocus: false,
skipTraversal: true, skipTraversal: true,
includeSemantics: false, includeSemantics: false,
descendantsAreFocusable: widget.descendantsAreFocusable,
child: widget.child, child: widget.child,
), ),
); );
......
...@@ -97,6 +97,53 @@ void main() { ...@@ -97,6 +97,53 @@ void main() {
expect(focusNode1.offset, equals(const Offset(300.0, 8.0))); expect(focusNode1.offset, equals(const Offset(300.0, 8.0)));
expect(focusNode2.offset, equals(const Offset(443.0, 194.5))); 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 { testWidgets('implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
FocusNode( FocusNode(
...@@ -105,9 +152,10 @@ void main() { ...@@ -105,9 +152,10 @@ void main() {
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList(); final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[ expect(description, <String>[
'context: null', 'context: null',
'descendantsAreFocusable: true',
'canRequestFocus: true', 'canRequestFocus: true',
'hasFocus: false', 'hasFocus: false',
'hasPrimaryFocus: false' 'hasPrimaryFocus: false',
]); ]);
}); });
}); });
...@@ -949,6 +997,7 @@ void main() { ...@@ -949,6 +997,7 @@ void main() {
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList(); final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[ expect(description, <String>[
'context: null', 'context: null',
'descendantsAreFocusable: true',
'canRequestFocus: true', 'canRequestFocus: true',
'hasFocus: false', 'hasFocus: false',
'hasPrimaryFocus: false' 'hasPrimaryFocus: false'
......
...@@ -1962,6 +1962,44 @@ void main() { ...@@ -1962,6 +1962,44 @@ void main() {
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics)); 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);
});
}); });
} }
......
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