Unverified Commit 56a7c97f authored by Viren Khatri's avatar Viren Khatri Committed by GitHub

Adds ability to mark a subtree as not traversable (#94626)

parent 23e7449a
...@@ -1064,6 +1064,7 @@ class FocusableActionDetector extends StatefulWidget { ...@@ -1064,6 +1064,7 @@ class FocusableActionDetector extends StatefulWidget {
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
this.descendantsAreFocusable = true, this.descendantsAreFocusable = true,
this.descendantsAreTraversable = true,
this.shortcuts, this.shortcuts,
this.actions, this.actions,
this.onShowFocusHighlight, this.onShowFocusHighlight,
...@@ -1095,6 +1096,9 @@ class FocusableActionDetector extends StatefulWidget { ...@@ -1095,6 +1096,9 @@ class FocusableActionDetector extends StatefulWidget {
/// {@macro flutter.widgets.Focus.descendantsAreFocusable} /// {@macro flutter.widgets.Focus.descendantsAreFocusable}
final bool descendantsAreFocusable; final bool descendantsAreFocusable;
/// {@macro flutter.widgets.Focus.descendantsAreTraversable}
final bool descendantsAreTraversable;
/// {@macro flutter.widgets.actions.actions} /// {@macro flutter.widgets.actions.actions}
final Map<Type, Action<Intent>>? actions; final Map<Type, Action<Intent>>? actions;
...@@ -1281,6 +1285,7 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> { ...@@ -1281,6 +1285,7 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
focusNode: widget.focusNode, focusNode: widget.focusNode,
autofocus: widget.autofocus, autofocus: widget.autofocus,
descendantsAreFocusable: widget.descendantsAreFocusable, descendantsAreFocusable: widget.descendantsAreFocusable,
descendantsAreTraversable: widget.descendantsAreTraversable,
canRequestFocus: _canRequestFocus, canRequestFocus: _canRequestFocus,
onFocusChange: _handleFocusChange, onFocusChange: _handleFocusChange,
child: widget.child, child: widget.child,
......
...@@ -409,12 +409,14 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -409,12 +409,14 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
bool skipTraversal = false, bool skipTraversal = false,
bool canRequestFocus = true, bool canRequestFocus = true,
bool descendantsAreFocusable = true, bool descendantsAreFocusable = true,
bool descendantsAreTraversable = true,
}) : assert(skipTraversal != null), }) : assert(skipTraversal != null),
assert(canRequestFocus != null), assert(canRequestFocus != null),
assert(descendantsAreFocusable != null), assert(descendantsAreFocusable != null),
_skipTraversal = skipTraversal, _skipTraversal = skipTraversal,
_canRequestFocus = canRequestFocus, _canRequestFocus = canRequestFocus,
_descendantsAreFocusable = descendantsAreFocusable { _descendantsAreFocusable = descendantsAreFocusable,
_descendantsAreTraversable = descendantsAreTraversable {
// 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;
} }
...@@ -429,7 +431,17 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -429,7 +431,17 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// This is different from [canRequestFocus] because it only implies that the /// 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 /// node can't be reached via traversal, not that it can't be focused. It may
/// still be focused explicitly. /// still be focused explicitly.
bool get skipTraversal => _skipTraversal; bool get skipTraversal {
if (_skipTraversal) {
return true;
}
for (final FocusNode ancestor in ancestors) {
if (!ancestor.descendantsAreTraversable) {
return true;
}
}
return false;
}
bool _skipTraversal; bool _skipTraversal;
set skipTraversal(bool value) { set skipTraversal(bool value) {
if (value != _skipTraversal) { if (value != _skipTraversal) {
...@@ -511,13 +523,17 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -511,13 +523,17 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// ///
/// See also: /// See also:
/// ///
/// * [ExcludeFocus], a widget that uses this property to conditionally /// * [ExcludeFocus], a widget that uses this property to conditionally
/// exclude focus for a subtree. /// exclude focus for a subtree.
/// * [Focus], a widget that exposes this setting as a parameter. /// * [descendantsAreTraversable], which makes this widget's descendants
/// * [FocusTraversalGroup], a widget used to group together and configure /// untraversable.
/// the focus traversal policy for a widget subtree that also has an /// * [ExcludeFocusTraversal], a widget that conditionally excludes focus
/// `descendantsAreFocusable` parameter that prevents its children from /// traversal for a subtree.
/// being focused. /// * [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 get descendantsAreFocusable => _descendantsAreFocusable;
bool _descendantsAreFocusable; bool _descendantsAreFocusable;
@mustCallSuper @mustCallSuper
...@@ -534,6 +550,36 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -534,6 +550,36 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
_manager?._markPropertiesChanged(this); _manager?._markPropertiesChanged(this);
} }
/// If false, tells the focus traversal policy to skip over for all of this
/// node's descendants for purposes of the traversal algorithm.
///
/// Defaults to true. Does not affect the focus traversal of this node: for
/// that, use [skipTraversal].
///
/// Does not affect the value of [FocusNode.skipTraversal] on the
/// descendants. Does not affect focusability of the descendants.
///
/// See also:
///
/// * [ExcludeFocusTraversal], a widget that uses this property to conditionally
/// exclude focus traversal for a subtree.
/// * [descendantsAreFocusable], which makes this widget's descendants
/// unfocusable.
/// * [ExcludeFocus], a widget that conditionally excludes focus for a subtree.
/// * [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 descendantsAreTraversable => _descendantsAreTraversable;
bool _descendantsAreTraversable;
@mustCallSuper
set descendantsAreTraversable(bool value) {
if (value != _descendantsAreTraversable) {
_descendantsAreTraversable = 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
...@@ -1105,6 +1151,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -1105,6 +1151,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
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('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true));
properties.add(FlagProperty('descendantsAreTraversable', value: descendantsAreTraversable, ifFalse: 'DESCENDANTS UNTRAVERSABLE', 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));
......
...@@ -124,6 +124,7 @@ class Focus extends StatefulWidget { ...@@ -124,6 +124,7 @@ class Focus extends StatefulWidget {
bool? canRequestFocus, bool? canRequestFocus,
bool? skipTraversal, bool? skipTraversal,
bool? descendantsAreFocusable, bool? descendantsAreFocusable,
bool? descendantsAreTraversable,
this.includeSemantics = true, this.includeSemantics = true,
String? debugLabel, String? debugLabel,
}) : _onKeyEvent = onKeyEvent, }) : _onKeyEvent = onKeyEvent,
...@@ -131,6 +132,7 @@ class Focus extends StatefulWidget { ...@@ -131,6 +132,7 @@ class Focus extends StatefulWidget {
_canRequestFocus = canRequestFocus, _canRequestFocus = canRequestFocus,
_skipTraversal = skipTraversal, _skipTraversal = skipTraversal,
_descendantsAreFocusable = descendantsAreFocusable, _descendantsAreFocusable = descendantsAreFocusable,
_descendantsAreTraversable = descendantsAreTraversable,
_debugLabel = debugLabel, _debugLabel = debugLabel,
assert(child != null), assert(child != null),
assert(autofocus != null), assert(autofocus != null),
...@@ -279,10 +281,17 @@ class Focus extends StatefulWidget { ...@@ -279,10 +281,17 @@ class Focus extends StatefulWidget {
/// Does not affect the value of [FocusNode.canRequestFocus] on the /// Does not affect the value of [FocusNode.canRequestFocus] on the
/// descendants. /// descendants.
/// ///
/// If a descendant node loses focus when this value is changed, the focus
/// will move to the scope enclosing this node.
///
/// See also: /// See also:
/// ///
/// * [ExcludeFocus], a widget that uses this property to conditionally /// * [ExcludeFocus], a widget that uses this property to conditionally
/// exclude focus for a subtree. /// exclude focus for a subtree.
/// * [descendantsAreTraversable], which makes this widget's descendants
/// untraversable.
/// * [ExcludeFocusTraversal], a widget that conditionally excludes focus
/// traversal for a subtree.
/// * [FocusTraversalGroup], a widget used to group together and configure the /// * [FocusTraversalGroup], a widget used to group together and configure the
/// focus traversal policy for a widget subtree that has a /// focus traversal policy for a widget subtree that has a
/// `descendantsAreFocusable` parameter to conditionally block focus for a /// `descendantsAreFocusable` parameter to conditionally block focus for a
...@@ -291,6 +300,30 @@ class Focus extends StatefulWidget { ...@@ -291,6 +300,30 @@ class Focus extends StatefulWidget {
bool get descendantsAreFocusable => _descendantsAreFocusable ?? focusNode?.descendantsAreFocusable ?? true; bool get descendantsAreFocusable => _descendantsAreFocusable ?? focusNode?.descendantsAreFocusable ?? true;
final bool? _descendantsAreFocusable; final bool? _descendantsAreFocusable;
/// {@template flutter.widgets.Focus.descendantsAreTraversable}
/// If false, will make this widget's descendants untraversable.
///
/// Defaults to true. Does not affect traversablility of this node (just its
/// descendants): for that, use [FocusNode.skipTraversal].
///
/// Does not affect the value of [FocusNode.skipTraversal] on the
/// descendants. Does not affect focusability of the descendants.
///
/// See also:
///
/// * [ExcludeFocusTraversal], a widget that uses this property to
/// conditionally exclude focus traversal for a subtree.
/// * [descendantsAreFocusable], which makes this widget's descendants
/// unfocusable.
/// * [ExcludeFocus], a widget that conditionally excludes 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}
bool get descendantsAreTraversable => _descendantsAreTraversable ?? focusNode?.descendantsAreTraversable ?? true;
final bool? _descendantsAreTraversable;
/// {@template flutter.widgets.Focus.includeSemantics} /// {@template flutter.widgets.Focus.includeSemantics}
/// Include semantics information in this widget. /// Include semantics information in this widget.
/// ///
...@@ -420,6 +453,7 @@ class Focus extends StatefulWidget { ...@@ -420,6 +453,7 @@ class Focus extends StatefulWidget {
properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false)); properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false));
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: false)); properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: false));
properties.add(FlagProperty('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true)); properties.add(FlagProperty('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true));
properties.add(FlagProperty('descendantsAreTraversable', value: descendantsAreTraversable, ifFalse: 'DESCENDANTS UNTRAVERSABLE', defaultValue: true));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null)); properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
} }
...@@ -459,6 +493,8 @@ class _FocusWithExternalFocusNode extends Focus { ...@@ -459,6 +493,8 @@ class _FocusWithExternalFocusNode extends Focus {
@override @override
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable; bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
@override @override
bool? get _descendantsAreTraversable => focusNode!.descendantsAreTraversable;
@override
String? get debugLabel => focusNode!.debugLabel; String? get debugLabel => focusNode!.debugLabel;
} }
...@@ -468,6 +504,7 @@ class _FocusState extends State<Focus> { ...@@ -468,6 +504,7 @@ class _FocusState extends State<Focus> {
late bool _hadPrimaryFocus; late bool _hadPrimaryFocus;
late bool _couldRequestFocus; late bool _couldRequestFocus;
late bool _descendantsWereFocusable; late bool _descendantsWereFocusable;
late bool _descendantsWereTraversable;
bool _didAutofocus = false; bool _didAutofocus = false;
FocusAttachment? _focusAttachment; FocusAttachment? _focusAttachment;
...@@ -485,6 +522,7 @@ class _FocusState extends State<Focus> { ...@@ -485,6 +522,7 @@ class _FocusState extends State<Focus> {
_internalNode ??= _createNode(); _internalNode ??= _createNode();
} }
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable; focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
if (widget.skipTraversal != null) { if (widget.skipTraversal != null) {
focusNode.skipTraversal = widget.skipTraversal; focusNode.skipTraversal = widget.skipTraversal;
} }
...@@ -493,6 +531,7 @@ class _FocusState extends State<Focus> { ...@@ -493,6 +531,7 @@ class _FocusState extends State<Focus> {
} }
_couldRequestFocus = focusNode.canRequestFocus; _couldRequestFocus = focusNode.canRequestFocus;
_descendantsWereFocusable = focusNode.descendantsAreFocusable; _descendantsWereFocusable = focusNode.descendantsAreFocusable;
_descendantsWereTraversable = focusNode.descendantsAreTraversable;
_hadPrimaryFocus = focusNode.hasPrimaryFocus; _hadPrimaryFocus = focusNode.hasPrimaryFocus;
_focusAttachment = focusNode.attach(context, onKeyEvent: widget.onKeyEvent, onKey: widget.onKey); _focusAttachment = focusNode.attach(context, onKeyEvent: widget.onKeyEvent, onKey: widget.onKey);
...@@ -507,6 +546,7 @@ class _FocusState extends State<Focus> { ...@@ -507,6 +546,7 @@ class _FocusState extends State<Focus> {
debugLabel: widget.debugLabel, debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus, canRequestFocus: widget.canRequestFocus,
descendantsAreFocusable: widget.descendantsAreFocusable, descendantsAreFocusable: widget.descendantsAreFocusable,
descendantsAreTraversable: widget.descendantsAreTraversable,
skipTraversal: widget.skipTraversal, skipTraversal: widget.skipTraversal,
); );
} }
...@@ -579,6 +619,7 @@ class _FocusState extends State<Focus> { ...@@ -579,6 +619,7 @@ class _FocusState extends State<Focus> {
focusNode.canRequestFocus = widget._canRequestFocus!; focusNode.canRequestFocus = widget._canRequestFocus!;
} }
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable; focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
} }
} else { } else {
_focusAttachment!.detach(); _focusAttachment!.detach();
...@@ -595,6 +636,7 @@ class _FocusState extends State<Focus> { ...@@ -595,6 +636,7 @@ class _FocusState extends State<Focus> {
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; final bool descendantsAreFocusable = focusNode.descendantsAreFocusable;
final bool descendantsAreTraversable = focusNode.descendantsAreTraversable;
widget.onFocusChange?.call(focusNode.hasFocus); widget.onFocusChange?.call(focusNode.hasFocus);
// Check the cached states that matter here, and call setState if they have // Check the cached states that matter here, and call setState if they have
// changed. // changed.
...@@ -613,6 +655,11 @@ class _FocusState extends State<Focus> { ...@@ -613,6 +655,11 @@ class _FocusState extends State<Focus> {
_descendantsWereFocusable = descendantsAreFocusable; _descendantsWereFocusable = descendantsAreFocusable;
}); });
} }
if (_descendantsWereTraversable != descendantsAreTraversable) {
setState(() {
_descendantsWereTraversable = descendantsAreTraversable;
});
}
} }
@override @override
...@@ -784,6 +831,8 @@ class _FocusScopeWithExternalFocusNode extends FocusScope { ...@@ -784,6 +831,8 @@ class _FocusScopeWithExternalFocusNode extends FocusScope {
@override @override
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable; bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
@override @override
bool get descendantsAreTraversable => focusNode!.descendantsAreTraversable;
@override
String? get debugLabel => focusNode!.debugLabel; String? get debugLabel => focusNode!.debugLabel;
} }
......
...@@ -388,6 +388,11 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -388,6 +388,11 @@ abstract class FocusTraversalPolicy with Diagnosticable {
} }
} }
final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, currentNode); final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, currentNode);
if (sortedNodes.isEmpty) {
// If there are no nodes to traverse to, like when descendantsAreTraversable
// is false or skipTraversal for all the nodes is true.
return false;
}
if (forward && focusedChild == sortedNodes.last) { if (forward && focusedChild == sortedNodes.last) {
_focusAndEnsureVisible(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd); _focusAndEnsureVisible(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
return true; return true;
...@@ -1446,10 +1451,12 @@ class FocusTraversalGroup extends StatefulWidget { ...@@ -1446,10 +1451,12 @@ class FocusTraversalGroup extends StatefulWidget {
Key? key, Key? key,
FocusTraversalPolicy? policy, FocusTraversalPolicy? policy,
this.descendantsAreFocusable = true, this.descendantsAreFocusable = true,
this.descendantsAreTraversable = true,
required this.child, required this.child,
}) : assert(descendantsAreFocusable != null), }) : assert(descendantsAreFocusable != null),
policy = policy ?? ReadingOrderTraversalPolicy(), assert(descendantsAreTraversable != null),
super(key: key); policy = policy ?? ReadingOrderTraversalPolicy(),
super(key: key);
/// 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.
...@@ -1471,6 +1478,9 @@ class FocusTraversalGroup extends StatefulWidget { ...@@ -1471,6 +1478,9 @@ class FocusTraversalGroup extends StatefulWidget {
/// {@macro flutter.widgets.Focus.descendantsAreFocusable} /// {@macro flutter.widgets.Focus.descendantsAreFocusable}
final bool descendantsAreFocusable; final bool descendantsAreFocusable;
/// {@macro flutter.widgets.Focus.descendantsAreTraversable}
final bool descendantsAreTraversable;
/// The child widget of this [FocusTraversalGroup]. /// The child widget of this [FocusTraversalGroup].
/// ///
/// {@macro flutter.widgets.ProxyWidget.child} /// {@macro flutter.widgets.ProxyWidget.child}
...@@ -1573,6 +1583,7 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> { ...@@ -1573,6 +1583,7 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
skipTraversal: true, skipTraversal: true,
includeSemantics: false, includeSemantics: false,
descendantsAreFocusable: widget.descendantsAreFocusable, descendantsAreFocusable: widget.descendantsAreFocusable,
descendantsAreTraversable: widget.descendantsAreTraversable,
child: widget.child, child: widget.child,
), ),
); );
...@@ -1737,3 +1748,62 @@ class DirectionalFocusAction extends Action<DirectionalFocusIntent> { ...@@ -1737,3 +1748,62 @@ class DirectionalFocusAction extends Action<DirectionalFocusIntent> {
} }
} }
} }
/// A widget that controls whether or not the descendants of this widget are
/// traversable.
///
/// Does not affect the value of [FocusNode.skipTraversal] of the descendants.
///
/// See also:
///
/// * [Focus], a widget for adding and managing a [FocusNode] in the widget tree.
/// * [ExcludeFocus], a widget that excludes its descendants from focusability.
/// * [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 ExcludeFocusTraversal extends StatelessWidget {
/// Const constructor for [ExcludeFocusTraversal] widget.
///
/// The [excluding] argument must not be null.
///
/// The [child] argument is required, and must not be null.
const ExcludeFocusTraversal({
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 untraversable.
///
/// Defaults to true.
///
/// Does not affect the value of [FocusNode.skipTraversal] on the descendants.
///
/// See also:
///
/// * [Focus.descendantsAreTraversable], 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 [ExcludeFocusTraversal].
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
@override
Widget build(BuildContext context) {
return Focus(
canRequestFocus: false,
skipTraversal: true,
includeSemantics: false,
descendantsAreTraversable: !excluding,
child: child,
);
}
}
...@@ -962,6 +962,75 @@ void main() { ...@@ -962,6 +962,75 @@ void main() {
expect(buttonNode.hasFocus, isFalse); expect(buttonNode.hasFocus, isFalse);
}, },
); );
testWidgets(
'FocusableActionDetector can prevent its descendants from being traversable',
(WidgetTester tester) async {
final FocusNode buttonNode1 = FocusNode(debugLabel: 'Button Node 1');
final FocusNode buttonNode2 = FocusNode(debugLabel: 'Button Node 2');
await tester.pumpWidget(
MaterialApp(
home: FocusableActionDetector(
child: Column(
children: <Widget>[
MaterialButton(
focusNode: buttonNode1,
child: const Text('Node 1'),
onPressed: () {},
),
MaterialButton(
focusNode: buttonNode2,
child: const Text('Node 2'),
onPressed: () {},
),
],
),
),
),
);
buttonNode1.requestFocus();
await tester.pump();
expect(buttonNode1.hasFocus, isTrue);
expect(buttonNode2.hasFocus, isFalse);
primaryFocus!.nextFocus();
await tester.pump();
expect(buttonNode1.hasFocus, isFalse);
expect(buttonNode2.hasFocus, isTrue);
await tester.pumpWidget(
MaterialApp(
home: FocusableActionDetector(
descendantsAreTraversable: false,
child: Column(
children: <Widget>[
MaterialButton(
focusNode: buttonNode1,
child: const Text('Node 1'),
onPressed: () {},
),
MaterialButton(
focusNode: buttonNode2,
child: const Text('Node 2'),
onPressed: () {},
),
],
),
),
),
);
buttonNode1.requestFocus();
await tester.pump();
expect(buttonNode1.hasFocus, isTrue);
expect(buttonNode2.hasFocus, isFalse);
primaryFocus!.nextFocus();
await tester.pump();
expect(buttonNode1.hasFocus, isTrue);
expect(buttonNode2.hasFocus, isFalse);
},
);
}); });
group('Diagnostics', () { group('Diagnostics', () {
......
...@@ -151,6 +151,39 @@ void main() { ...@@ -151,6 +151,39 @@ void main() {
expect(scope.traversalDescendants.contains(child2), isFalse); expect(scope.traversalDescendants.contains(child2), isFalse);
}); });
testWidgets('descendantsAreTraversable disables traversal 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);
expect(scope.traversalDescendants, equals(<FocusNode>[child1, parent1, child2, parent2]));
parent2.descendantsAreTraversable = false;
expect(scope.traversalDescendants, equals(<FocusNode>[child1, parent1, parent2]));
parent1.descendantsAreTraversable = false;
expect(scope.traversalDescendants, equals(<FocusNode>[parent1, parent2]));
parent1.descendantsAreTraversable = true;
parent2.descendantsAreTraversable = true;
scope.descendantsAreTraversable = false;
expect(scope.traversalDescendants, equals(<FocusNode>[]));
});
testWidgets("canRequestFocus doesn't affect traversalChildren", (WidgetTester tester) async { testWidgets("canRequestFocus doesn't affect traversalChildren", (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
...@@ -191,6 +224,7 @@ void main() { ...@@ -191,6 +224,7 @@ void main() {
expect(description, <String>[ expect(description, <String>[
'context: null', 'context: null',
'descendantsAreFocusable: true', 'descendantsAreFocusable: true',
'descendantsAreTraversable: true',
'canRequestFocus: true', 'canRequestFocus: true',
'hasFocus: false', 'hasFocus: false',
'hasPrimaryFocus: false', 'hasPrimaryFocus: false',
...@@ -1156,6 +1190,7 @@ void main() { ...@@ -1156,6 +1190,7 @@ void main() {
expect(description, <String>[ expect(description, <String>[
'context: null', 'context: null',
'descendantsAreFocusable: true', 'descendantsAreFocusable: true',
'descendantsAreTraversable: true',
'canRequestFocus: true', 'canRequestFocus: true',
'hasFocus: false', 'hasFocus: false',
'hasPrimaryFocus: false', 'hasPrimaryFocus: false',
......
...@@ -1085,6 +1085,7 @@ void main() { ...@@ -1085,6 +1085,7 @@ void main() {
focusScopeNode.onKey = ignoreCallback; focusScopeNode.onKey = ignoreCallback;
focusScopeNode.onKeyEvent = ignoreEventCallback; focusScopeNode.onKeyEvent = ignoreEventCallback;
focusScopeNode.descendantsAreFocusable = false; focusScopeNode.descendantsAreFocusable = false;
focusScopeNode.descendantsAreTraversable = false;
focusScopeNode.skipTraversal = false; focusScopeNode.skipTraversal = false;
focusScopeNode.canRequestFocus = true; focusScopeNode.canRequestFocus = true;
FocusScope focusScopeWidget = FocusScope.withExternalFocusNode( FocusScope focusScopeWidget = FocusScope.withExternalFocusNode(
...@@ -1095,11 +1096,13 @@ void main() { ...@@ -1095,11 +1096,13 @@ void main() {
expect(focusScopeNode.onKey, equals(ignoreCallback)); expect(focusScopeNode.onKey, equals(ignoreCallback));
expect(focusScopeNode.onKeyEvent, equals(ignoreEventCallback)); expect(focusScopeNode.onKeyEvent, equals(ignoreEventCallback));
expect(focusScopeNode.descendantsAreFocusable, isFalse); expect(focusScopeNode.descendantsAreFocusable, isFalse);
expect(focusScopeNode.descendantsAreTraversable, isFalse);
expect(focusScopeNode.skipTraversal, isFalse); expect(focusScopeNode.skipTraversal, isFalse);
expect(focusScopeNode.canRequestFocus, isTrue); expect(focusScopeNode.canRequestFocus, isTrue);
expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey)); expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey));
expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent)); expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent));
expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable)); expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable));
expect(focusScopeWidget.descendantsAreTraversable, equals(focusScopeNode.descendantsAreTraversable));
expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal)); expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal));
expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus)); expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus));
...@@ -1111,6 +1114,7 @@ void main() { ...@@ -1111,6 +1114,7 @@ void main() {
focusScopeNode.onKey = handleCallback; focusScopeNode.onKey = handleCallback;
focusScopeNode.onKeyEvent = handleEventCallback; focusScopeNode.onKeyEvent = handleEventCallback;
focusScopeNode.descendantsAreFocusable = true; focusScopeNode.descendantsAreFocusable = true;
focusScopeNode.descendantsAreTraversable = true;
focusScopeWidget = FocusScope.withExternalFocusNode( focusScopeWidget = FocusScope.withExternalFocusNode(
focusScopeNode: focusScopeNode, focusScopeNode: focusScopeNode,
child: Container(key: key1), child: Container(key: key1),
...@@ -1119,11 +1123,13 @@ void main() { ...@@ -1119,11 +1123,13 @@ void main() {
expect(focusScopeNode.onKey, equals(handleCallback)); expect(focusScopeNode.onKey, equals(handleCallback));
expect(focusScopeNode.onKeyEvent, equals(handleEventCallback)); expect(focusScopeNode.onKeyEvent, equals(handleEventCallback));
expect(focusScopeNode.descendantsAreFocusable, isTrue); expect(focusScopeNode.descendantsAreFocusable, isTrue);
expect(focusScopeNode.descendantsAreTraversable, isTrue);
expect(focusScopeNode.skipTraversal, isFalse); expect(focusScopeNode.skipTraversal, isFalse);
expect(focusScopeNode.canRequestFocus, isTrue); expect(focusScopeNode.canRequestFocus, isTrue);
expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey)); expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey));
expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent)); expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent));
expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable)); expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable));
expect(focusScopeWidget.descendantsAreTraversable, equals(focusScopeNode.descendantsAreTraversable));
expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal)); expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal));
expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus)); expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus));
...@@ -1639,12 +1645,47 @@ void main() { ...@@ -1639,12 +1645,47 @@ void main() {
expect(containerNode.hasFocus, isFalse); expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse);
}); });
testWidgets('descendantsAreTraversable works as expected.', (WidgetTester tester) async {
final FocusScopeNode scopeNode = FocusScopeNode(debugLabel: 'scope');
final FocusNode node1 = FocusNode(debugLabel: 'node 1');
final FocusNode node2 = FocusNode(debugLabel: 'node 2');
final FocusNode node3 = FocusNode(debugLabel: 'node 3');
await tester.pumpWidget(
FocusScope(
node: scopeNode,
child: Column(
children: <Widget>[
Focus(
focusNode: node1,
child: Container(),
),
Focus(
focusNode: node2,
descendantsAreTraversable: false,
child: Focus(
focusNode: node3,
child: Container(),
)
),
],
),
),
);
await tester.pump();
expect(scopeNode.traversalDescendants, equals(<FocusNode>[node1, node2]));
expect(node2.traversalDescendants, equals(<FocusNode>[]));
});
testWidgets("Focus doesn't introduce a Semantics node when includeSemantics is false", (WidgetTester tester) async { testWidgets("Focus doesn't introduce a Semantics node when includeSemantics is false", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Focus(includeSemantics: false, child: Container())); await tester.pumpWidget(Focus(includeSemantics: false, child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics));
}); });
testWidgets('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async { testWidgets('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -1695,6 +1736,7 @@ void main() { ...@@ -1695,6 +1736,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue); expect(keyEventHandled, isTrue);
}); });
testWidgets('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async { testWidgets('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -1745,6 +1787,7 @@ void main() { ...@@ -1745,6 +1787,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue); expect(keyEventHandled, isTrue);
}); });
testWidgets("Focus doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async { testWidgets("Focus doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -1766,6 +1809,7 @@ void main() { ...@@ -1766,6 +1809,7 @@ void main() {
focusNode.onKey = ignoreCallback; focusNode.onKey = ignoreCallback;
focusNode.onKeyEvent = ignoreEventCallback; focusNode.onKeyEvent = ignoreEventCallback;
focusNode.descendantsAreFocusable = false; focusNode.descendantsAreFocusable = false;
focusNode.descendantsAreTraversable = false;
focusNode.skipTraversal = false; focusNode.skipTraversal = false;
focusNode.canRequestFocus = true; focusNode.canRequestFocus = true;
Focus focusWidget = Focus.withExternalFocusNode( Focus focusWidget = Focus.withExternalFocusNode(
...@@ -1776,11 +1820,13 @@ void main() { ...@@ -1776,11 +1820,13 @@ void main() {
expect(focusNode.onKey, equals(ignoreCallback)); expect(focusNode.onKey, equals(ignoreCallback));
expect(focusNode.onKeyEvent, equals(ignoreEventCallback)); expect(focusNode.onKeyEvent, equals(ignoreEventCallback));
expect(focusNode.descendantsAreFocusable, isFalse); expect(focusNode.descendantsAreFocusable, isFalse);
expect(focusNode.descendantsAreTraversable, isFalse);
expect(focusNode.skipTraversal, isFalse); expect(focusNode.skipTraversal, isFalse);
expect(focusNode.canRequestFocus, isTrue); expect(focusNode.canRequestFocus, isTrue);
expect(focusWidget.onKey, equals(focusNode.onKey)); expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent)); expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable)); expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
expect(focusWidget.descendantsAreTraversable, equals(focusNode.descendantsAreTraversable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal)); expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus)); expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
...@@ -1792,6 +1838,7 @@ void main() { ...@@ -1792,6 +1838,7 @@ void main() {
focusNode.onKey = handleCallback; focusNode.onKey = handleCallback;
focusNode.onKeyEvent = handleEventCallback; focusNode.onKeyEvent = handleEventCallback;
focusNode.descendantsAreFocusable = true; focusNode.descendantsAreFocusable = true;
focusNode.descendantsAreTraversable = true;
focusWidget = Focus.withExternalFocusNode( focusWidget = Focus.withExternalFocusNode(
focusNode: focusNode, focusNode: focusNode,
child: Container(key: key1), child: Container(key: key1),
...@@ -1800,18 +1847,29 @@ void main() { ...@@ -1800,18 +1847,29 @@ void main() {
expect(focusNode.onKey, equals(handleCallback)); expect(focusNode.onKey, equals(handleCallback));
expect(focusNode.onKeyEvent, equals(handleEventCallback)); expect(focusNode.onKeyEvent, equals(handleEventCallback));
expect(focusNode.descendantsAreFocusable, isTrue); expect(focusNode.descendantsAreFocusable, isTrue);
expect(focusNode.descendantsAreTraversable, isTrue);
expect(focusNode.skipTraversal, isFalse); expect(focusNode.skipTraversal, isFalse);
expect(focusNode.canRequestFocus, isTrue); expect(focusNode.canRequestFocus, isTrue);
expect(focusWidget.onKey, equals(focusNode.onKey)); expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent)); expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable)); expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
expect(focusWidget.descendantsAreTraversable, equals(focusNode.descendantsAreTraversable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal)); expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus)); expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue); expect(keyEventHandled, isTrue);
}); });
testWidgets('Focus passes changes in attribute values to its focus node', (WidgetTester tester) async {
await tester.pumpWidget(
Focus(
child: Container(),
),
);
});
}); });
group('ExcludeFocus', () { group('ExcludeFocus', () {
testWidgets("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async { testWidgets("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
...@@ -1919,6 +1977,7 @@ void main() { ...@@ -1919,6 +1977,7 @@ void main() {
expect(parentFocusNode.hasFocus, isFalse); expect(parentFocusNode.hasFocus, isFalse);
expect(parentFocusNode.enclosingScope!.hasPrimaryFocus, isTrue); expect(parentFocusNode.enclosingScope!.hasPrimaryFocus, isTrue);
}); });
testWidgets("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async { testWidgets("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(ExcludeFocus(child: Container())); await tester.pumpWidget(ExcludeFocus(child: Container()));
......
...@@ -2054,6 +2054,7 @@ void main() { ...@@ -2054,6 +2054,7 @@ 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 { testWidgets("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
...@@ -2092,6 +2093,78 @@ void main() { ...@@ -2092,6 +2093,78 @@ void main() {
expect(containerNode.hasFocus, isFalse); expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse);
}); });
testWidgets("Descendants of FocusTraversalGroup aren't traversable if descendantsAreTraversable is false.", (WidgetTester tester) async {
final FocusNode node1 = FocusNode();
final FocusNode node2 = FocusNode();
await tester.pumpWidget(
FocusTraversalGroup(
descendantsAreTraversable: false,
child: Column(
children: <Widget>[
Focus(
focusNode: node1,
child: Container(),
),
Focus(
focusNode: node2,
child: Container(),
),
],
),
),
);
node1.requestFocus();
await tester.pump();
expect(node1.hasPrimaryFocus, isTrue);
expect(node2.hasPrimaryFocus, isFalse);
expect(primaryFocus!.nextFocus(), isFalse);
await tester.pump();
expect(node1.hasPrimaryFocus, isTrue);
expect(node2.hasPrimaryFocus, isFalse);
});
testWidgets("FocusTraversalGroup with skipTraversal for all descendents set to true doesn't cause an exception.", (WidgetTester tester) async {
final FocusNode node1 = FocusNode();
final FocusNode node2 = FocusNode();
await tester.pumpWidget(
FocusTraversalGroup(
child: Column(
children: <Widget>[
Focus(
skipTraversal: true,
focusNode: node1,
child: Container(),
),
Focus(
skipTraversal: true,
focusNode: node2,
child: Container(),
),
],
),
),
);
node1.requestFocus();
await tester.pump();
expect(node1.hasPrimaryFocus, isTrue);
expect(node2.hasPrimaryFocus, isFalse);
expect(primaryFocus!.nextFocus(), isFalse);
await tester.pump();
expect(node1.hasPrimaryFocus, isTrue);
expect(node2.hasPrimaryFocus, isFalse);
});
testWidgets("Nested FocusTraversalGroup with unfocusable children doesn't assert.", (WidgetTester tester) async { testWidgets("Nested FocusTraversalGroup with unfocusable children doesn't assert.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
...@@ -2140,6 +2213,7 @@ void main() { ...@@ -2140,6 +2213,7 @@ void main() {
expect(containerNode.hasFocus, isFalse); expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse);
}); });
testWidgets("Empty FocusTraversalGroup doesn't cause an exception.", (WidgetTester tester) async { testWidgets("Empty FocusTraversalGroup doesn't cause an exception.", (WidgetTester tester) async {
final GlobalKey key = GlobalKey(debugLabel: 'Test Key'); final GlobalKey key = GlobalKey(debugLabel: 'Test Key');
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
...@@ -2169,6 +2243,7 @@ void main() { ...@@ -2169,6 +2243,7 @@ void main() {
expect(primaryFocus, equals(focusNode)); expect(primaryFocus, equals(focusNode));
}); });
}); });
group(RawKeyboardListener, () { group(RawKeyboardListener, () {
testWidgets('Raw keyboard listener introduces a Semantics node by default', (WidgetTester tester) async { testWidgets('Raw keyboard listener introduces a Semantics node by default', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
...@@ -2195,6 +2270,7 @@ void main() { ...@@ -2195,6 +2270,7 @@ void main() {
ignoreTransform: true, ignoreTransform: true,
)); ));
}); });
testWidgets("Raw keyboard listener doesn't introduce a Semantics node when specified", (WidgetTester tester) async { testWidgets("Raw keyboard listener doesn't introduce a Semantics node when specified", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -2209,6 +2285,63 @@ void main() { ...@@ -2209,6 +2285,63 @@ void main() {
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics));
}); });
}); });
group(ExcludeFocusTraversal, () {
testWidgets("Descendants aren't traversable", (WidgetTester tester) async {
final FocusNode node1 = FocusNode(debugLabel: 'node 1');
final FocusNode node2 = FocusNode(debugLabel: 'node 2');
final FocusNode node3 = FocusNode(debugLabel: 'node 3');
final FocusNode node4 = FocusNode(debugLabel: 'node 4');
await tester.pumpWidget(
FocusTraversalGroup(
child: Column(
children: <Widget>[
Focus(
autofocus: true,
focusNode: node1,
child: Container(),
),
ExcludeFocusTraversal(
child: Focus(
focusNode: node2,
child: Focus(
focusNode: node3,
child: Container(),
),
),
),
Focus(
focusNode: node4,
child: Container(),
),
],
),
),
);
await tester.pump();
expect(node1.hasPrimaryFocus, isTrue);
expect(node2.hasPrimaryFocus, isFalse);
expect(node3.hasPrimaryFocus, isFalse);
expect(node4.hasPrimaryFocus, isFalse);
node1.nextFocus();
await tester.pump();
expect(node1.hasPrimaryFocus, isFalse);
expect(node2.hasPrimaryFocus, isFalse);
expect(node3.hasPrimaryFocus, isFalse);
expect(node4.hasPrimaryFocus, isTrue);
});
testWidgets("Doesn't introduce a Semantics node", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(ExcludeFocusTraversal(child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics));
});
});
} }
class TestRoute extends PageRouteBuilder<void> { class TestRoute extends PageRouteBuilder<void> {
......
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