Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 56a7c97

Browse files
authored
Adds ability to mark a subtree as not traversable (#94626)
1 parent 23e7449 commit 56a7c97

File tree

8 files changed

+479
-12
lines changed

8 files changed

+479
-12
lines changed

packages/flutter/lib/src/widgets/actions.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,7 @@ class FocusableActionDetector extends StatefulWidget {
10641064
this.focusNode,
10651065
this.autofocus = false,
10661066
this.descendantsAreFocusable = true,
1067+
this.descendantsAreTraversable = true,
10671068
this.shortcuts,
10681069
this.actions,
10691070
this.onShowFocusHighlight,
@@ -1095,6 +1096,9 @@ class FocusableActionDetector extends StatefulWidget {
10951096
/// {@macro flutter.widgets.Focus.descendantsAreFocusable}
10961097
final bool descendantsAreFocusable;
10971098

1099+
/// {@macro flutter.widgets.Focus.descendantsAreTraversable}
1100+
final bool descendantsAreTraversable;
1101+
10981102
/// {@macro flutter.widgets.actions.actions}
10991103
final Map<Type, Action<Intent>>? actions;
11001104

@@ -1281,6 +1285,7 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
12811285
focusNode: widget.focusNode,
12821286
autofocus: widget.autofocus,
12831287
descendantsAreFocusable: widget.descendantsAreFocusable,
1288+
descendantsAreTraversable: widget.descendantsAreTraversable,
12841289
canRequestFocus: _canRequestFocus,
12851290
onFocusChange: _handleFocusChange,
12861291
child: widget.child,

packages/flutter/lib/src/widgets/focus_manager.dart

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -409,12 +409,14 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
409409
bool skipTraversal = false,
410410
bool canRequestFocus = true,
411411
bool descendantsAreFocusable = true,
412+
bool descendantsAreTraversable = true,
412413
}) : assert(skipTraversal != null),
413414
assert(canRequestFocus != null),
414415
assert(descendantsAreFocusable != null),
415416
_skipTraversal = skipTraversal,
416417
_canRequestFocus = canRequestFocus,
417-
_descendantsAreFocusable = descendantsAreFocusable {
418+
_descendantsAreFocusable = descendantsAreFocusable,
419+
_descendantsAreTraversable = descendantsAreTraversable {
418420
// Set it via the setter so that it does nothing on release builds.
419421
this.debugLabel = debugLabel;
420422
}
@@ -429,7 +431,17 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
429431
/// This is different from [canRequestFocus] because it only implies that the
430432
/// node can't be reached via traversal, not that it can't be focused. It may
431433
/// still be focused explicitly.
432-
bool get skipTraversal => _skipTraversal;
434+
bool get skipTraversal {
435+
if (_skipTraversal) {
436+
return true;
437+
}
438+
for (final FocusNode ancestor in ancestors) {
439+
if (!ancestor.descendantsAreTraversable) {
440+
return true;
441+
}
442+
}
443+
return false;
444+
}
433445
bool _skipTraversal;
434446
set skipTraversal(bool value) {
435447
if (value != _skipTraversal) {
@@ -511,13 +523,17 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
511523
///
512524
/// See also:
513525
///
514-
/// * [ExcludeFocus], a widget that uses this property to conditionally
515-
/// exclude focus for a subtree.
516-
/// * [Focus], a widget that exposes this setting as a parameter.
517-
/// * [FocusTraversalGroup], a widget used to group together and configure
518-
/// the focus traversal policy for a widget subtree that also has an
519-
/// `descendantsAreFocusable` parameter that prevents its children from
520-
/// being focused.
526+
/// * [ExcludeFocus], a widget that uses this property to conditionally
527+
/// exclude focus for a subtree.
528+
/// * [descendantsAreTraversable], which makes this widget's descendants
529+
/// untraversable.
530+
/// * [ExcludeFocusTraversal], a widget that conditionally excludes focus
531+
/// traversal for a subtree.
532+
/// * [Focus], a widget that exposes this setting as a parameter.
533+
/// * [FocusTraversalGroup], a widget used to group together and configure
534+
/// the focus traversal policy for a widget subtree that also has an
535+
/// `descendantsAreFocusable` parameter that prevents its children from
536+
/// being focused.
521537
bool get descendantsAreFocusable => _descendantsAreFocusable;
522538
bool _descendantsAreFocusable;
523539
@mustCallSuper
@@ -534,6 +550,36 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
534550
_manager?._markPropertiesChanged(this);
535551
}
536552

553+
/// If false, tells the focus traversal policy to skip over for all of this
554+
/// node's descendants for purposes of the traversal algorithm.
555+
///
556+
/// Defaults to true. Does not affect the focus traversal of this node: for
557+
/// that, use [skipTraversal].
558+
///
559+
/// Does not affect the value of [FocusNode.skipTraversal] on the
560+
/// descendants. Does not affect focusability of the descendants.
561+
///
562+
/// See also:
563+
///
564+
/// * [ExcludeFocusTraversal], a widget that uses this property to conditionally
565+
/// exclude focus traversal for a subtree.
566+
/// * [descendantsAreFocusable], which makes this widget's descendants
567+
/// unfocusable.
568+
/// * [ExcludeFocus], a widget that conditionally excludes focus for a subtree.
569+
/// * [FocusTraversalGroup], a widget used to group together and configure
570+
/// the focus traversal policy for a widget subtree that also has an
571+
/// `descendantsAreFocusable` parameter that prevents its children from
572+
/// being focused.
573+
bool get descendantsAreTraversable => _descendantsAreTraversable;
574+
bool _descendantsAreTraversable;
575+
@mustCallSuper
576+
set descendantsAreTraversable(bool value) {
577+
if (value != _descendantsAreTraversable) {
578+
_descendantsAreTraversable = value;
579+
_manager?._markPropertiesChanged(this);
580+
}
581+
}
582+
537583
/// The context that was supplied to [attach].
538584
///
539585
/// This is typically the context for the widget that is being focused, as it
@@ -1105,6 +1151,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
11051151
super.debugFillProperties(properties);
11061152
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
11071153
properties.add(FlagProperty('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true));
1154+
properties.add(FlagProperty('descendantsAreTraversable', value: descendantsAreTraversable, ifFalse: 'DESCENDANTS UNTRAVERSABLE', defaultValue: true));
11081155
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: true));
11091156
properties.add(FlagProperty('hasFocus', value: hasFocus && !hasPrimaryFocus, ifTrue: 'IN FOCUS PATH', defaultValue: false));
11101157
properties.add(FlagProperty('hasPrimaryFocus', value: hasPrimaryFocus, ifTrue: 'PRIMARY FOCUS', defaultValue: false));

packages/flutter/lib/src/widgets/focus_scope.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,15 @@ class Focus extends StatefulWidget {
124124
bool? canRequestFocus,
125125
bool? skipTraversal,
126126
bool? descendantsAreFocusable,
127+
bool? descendantsAreTraversable,
127128
this.includeSemantics = true,
128129
String? debugLabel,
129130
}) : _onKeyEvent = onKeyEvent,
130131
_onKey = onKey,
131132
_canRequestFocus = canRequestFocus,
132133
_skipTraversal = skipTraversal,
133134
_descendantsAreFocusable = descendantsAreFocusable,
135+
_descendantsAreTraversable = descendantsAreTraversable,
134136
_debugLabel = debugLabel,
135137
assert(child != null),
136138
assert(autofocus != null),
@@ -279,10 +281,17 @@ class Focus extends StatefulWidget {
279281
/// Does not affect the value of [FocusNode.canRequestFocus] on the
280282
/// descendants.
281283
///
284+
/// If a descendant node loses focus when this value is changed, the focus
285+
/// will move to the scope enclosing this node.
286+
///
282287
/// See also:
283288
///
284289
/// * [ExcludeFocus], a widget that uses this property to conditionally
285290
/// exclude focus for a subtree.
291+
/// * [descendantsAreTraversable], which makes this widget's descendants
292+
/// untraversable.
293+
/// * [ExcludeFocusTraversal], a widget that conditionally excludes focus
294+
/// traversal for a subtree.
286295
/// * [FocusTraversalGroup], a widget used to group together and configure the
287296
/// focus traversal policy for a widget subtree that has a
288297
/// `descendantsAreFocusable` parameter to conditionally block focus for a
@@ -291,6 +300,30 @@ class Focus extends StatefulWidget {
291300
bool get descendantsAreFocusable => _descendantsAreFocusable ?? focusNode?.descendantsAreFocusable ?? true;
292301
final bool? _descendantsAreFocusable;
293302

303+
/// {@template flutter.widgets.Focus.descendantsAreTraversable}
304+
/// If false, will make this widget's descendants untraversable.
305+
///
306+
/// Defaults to true. Does not affect traversablility of this node (just its
307+
/// descendants): for that, use [FocusNode.skipTraversal].
308+
///
309+
/// Does not affect the value of [FocusNode.skipTraversal] on the
310+
/// descendants. Does not affect focusability of the descendants.
311+
///
312+
/// See also:
313+
///
314+
/// * [ExcludeFocusTraversal], a widget that uses this property to
315+
/// conditionally exclude focus traversal for a subtree.
316+
/// * [descendantsAreFocusable], which makes this widget's descendants
317+
/// unfocusable.
318+
/// * [ExcludeFocus], a widget that conditionally excludes focus for a subtree.
319+
/// * [FocusTraversalGroup], a widget used to group together and configure the
320+
/// focus traversal policy for a widget subtree that has a
321+
/// `descendantsAreFocusable` parameter to conditionally block focus for a
322+
/// subtree.
323+
/// {@endtemplate}
324+
bool get descendantsAreTraversable => _descendantsAreTraversable ?? focusNode?.descendantsAreTraversable ?? true;
325+
final bool? _descendantsAreTraversable;
326+
294327
/// {@template flutter.widgets.Focus.includeSemantics}
295328
/// Include semantics information in this widget.
296329
///
@@ -420,6 +453,7 @@ class Focus extends StatefulWidget {
420453
properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false));
421454
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: false));
422455
properties.add(FlagProperty('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true));
456+
properties.add(FlagProperty('descendantsAreTraversable', value: descendantsAreTraversable, ifFalse: 'DESCENDANTS UNTRAVERSABLE', defaultValue: true));
423457
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
424458
}
425459

@@ -459,6 +493,8 @@ class _FocusWithExternalFocusNode extends Focus {
459493
@override
460494
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
461495
@override
496+
bool? get _descendantsAreTraversable => focusNode!.descendantsAreTraversable;
497+
@override
462498
String? get debugLabel => focusNode!.debugLabel;
463499
}
464500

@@ -468,6 +504,7 @@ class _FocusState extends State<Focus> {
468504
late bool _hadPrimaryFocus;
469505
late bool _couldRequestFocus;
470506
late bool _descendantsWereFocusable;
507+
late bool _descendantsWereTraversable;
471508
bool _didAutofocus = false;
472509
FocusAttachment? _focusAttachment;
473510

@@ -485,6 +522,7 @@ class _FocusState extends State<Focus> {
485522
_internalNode ??= _createNode();
486523
}
487524
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
525+
focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
488526
if (widget.skipTraversal != null) {
489527
focusNode.skipTraversal = widget.skipTraversal;
490528
}
@@ -493,6 +531,7 @@ class _FocusState extends State<Focus> {
493531
}
494532
_couldRequestFocus = focusNode.canRequestFocus;
495533
_descendantsWereFocusable = focusNode.descendantsAreFocusable;
534+
_descendantsWereTraversable = focusNode.descendantsAreTraversable;
496535
_hadPrimaryFocus = focusNode.hasPrimaryFocus;
497536
_focusAttachment = focusNode.attach(context, onKeyEvent: widget.onKeyEvent, onKey: widget.onKey);
498537

@@ -507,6 +546,7 @@ class _FocusState extends State<Focus> {
507546
debugLabel: widget.debugLabel,
508547
canRequestFocus: widget.canRequestFocus,
509548
descendantsAreFocusable: widget.descendantsAreFocusable,
549+
descendantsAreTraversable: widget.descendantsAreTraversable,
510550
skipTraversal: widget.skipTraversal,
511551
);
512552
}
@@ -579,6 +619,7 @@ class _FocusState extends State<Focus> {
579619
focusNode.canRequestFocus = widget._canRequestFocus!;
580620
}
581621
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
622+
focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
582623
}
583624
} else {
584625
_focusAttachment!.detach();
@@ -595,6 +636,7 @@ class _FocusState extends State<Focus> {
595636
final bool hasPrimaryFocus = focusNode.hasPrimaryFocus;
596637
final bool canRequestFocus = focusNode.canRequestFocus;
597638
final bool descendantsAreFocusable = focusNode.descendantsAreFocusable;
639+
final bool descendantsAreTraversable = focusNode.descendantsAreTraversable;
598640
widget.onFocusChange?.call(focusNode.hasFocus);
599641
// Check the cached states that matter here, and call setState if they have
600642
// changed.
@@ -613,6 +655,11 @@ class _FocusState extends State<Focus> {
613655
_descendantsWereFocusable = descendantsAreFocusable;
614656
});
615657
}
658+
if (_descendantsWereTraversable != descendantsAreTraversable) {
659+
setState(() {
660+
_descendantsWereTraversable = descendantsAreTraversable;
661+
});
662+
}
616663
}
617664

618665
@override
@@ -784,6 +831,8 @@ class _FocusScopeWithExternalFocusNode extends FocusScope {
784831
@override
785832
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
786833
@override
834+
bool get descendantsAreTraversable => focusNode!.descendantsAreTraversable;
835+
@override
787836
String? get debugLabel => focusNode!.debugLabel;
788837
}
789838

packages/flutter/lib/src/widgets/focus_traversal.dart

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,11 @@ abstract class FocusTraversalPolicy with Diagnosticable {
388388
}
389389
}
390390
final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, currentNode);
391+
if (sortedNodes.isEmpty) {
392+
// If there are no nodes to traverse to, like when descendantsAreTraversable
393+
// is false or skipTraversal for all the nodes is true.
394+
return false;
395+
}
391396
if (forward && focusedChild == sortedNodes.last) {
392397
_focusAndEnsureVisible(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
393398
return true;
@@ -1446,10 +1451,12 @@ class FocusTraversalGroup extends StatefulWidget {
14461451
Key? key,
14471452
FocusTraversalPolicy? policy,
14481453
this.descendantsAreFocusable = true,
1454+
this.descendantsAreTraversable = true,
14491455
required this.child,
1450-
}) : assert(descendantsAreFocusable != null),
1451-
policy = policy ?? ReadingOrderTraversalPolicy(),
1452-
super(key: key);
1456+
}) : assert(descendantsAreFocusable != null),
1457+
assert(descendantsAreTraversable != null),
1458+
policy = policy ?? ReadingOrderTraversalPolicy(),
1459+
super(key: key);
14531460

14541461
/// The policy used to move the focus from one focus node to another when
14551462
/// traversing them using a keyboard.
@@ -1471,6 +1478,9 @@ class FocusTraversalGroup extends StatefulWidget {
14711478
/// {@macro flutter.widgets.Focus.descendantsAreFocusable}
14721479
final bool descendantsAreFocusable;
14731480

1481+
/// {@macro flutter.widgets.Focus.descendantsAreTraversable}
1482+
final bool descendantsAreTraversable;
1483+
14741484
/// The child widget of this [FocusTraversalGroup].
14751485
///
14761486
/// {@macro flutter.widgets.ProxyWidget.child}
@@ -1573,6 +1583,7 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
15731583
skipTraversal: true,
15741584
includeSemantics: false,
15751585
descendantsAreFocusable: widget.descendantsAreFocusable,
1586+
descendantsAreTraversable: widget.descendantsAreTraversable,
15761587
child: widget.child,
15771588
),
15781589
);
@@ -1737,3 +1748,62 @@ class DirectionalFocusAction extends Action<DirectionalFocusIntent> {
17371748
}
17381749
}
17391750
}
1751+
1752+
/// A widget that controls whether or not the descendants of this widget are
1753+
/// traversable.
1754+
///
1755+
/// Does not affect the value of [FocusNode.skipTraversal] of the descendants.
1756+
///
1757+
/// See also:
1758+
///
1759+
/// * [Focus], a widget for adding and managing a [FocusNode] in the widget tree.
1760+
/// * [ExcludeFocus], a widget that excludes its descendants from focusability.
1761+
/// * [FocusTraversalGroup], a widget that groups widgets for focus traversal,
1762+
/// and can also be used in the same way as this widget by setting its
1763+
/// `descendantsAreFocusable` attribute.
1764+
class ExcludeFocusTraversal extends StatelessWidget {
1765+
/// Const constructor for [ExcludeFocusTraversal] widget.
1766+
///
1767+
/// The [excluding] argument must not be null.
1768+
///
1769+
/// The [child] argument is required, and must not be null.
1770+
const ExcludeFocusTraversal({
1771+
Key? key,
1772+
this.excluding = true,
1773+
required this.child,
1774+
}) : assert(excluding != null),
1775+
assert(child != null),
1776+
super(key: key);
1777+
1778+
/// If true, will make this widget's descendants untraversable.
1779+
///
1780+
/// Defaults to true.
1781+
///
1782+
/// Does not affect the value of [FocusNode.skipTraversal] on the descendants.
1783+
///
1784+
/// See also:
1785+
///
1786+
/// * [Focus.descendantsAreTraversable], the attribute of a [Focus] widget that
1787+
/// controls this same property for focus widgets.
1788+
/// * [FocusTraversalGroup], a widget used to group together and configure the
1789+
/// focus traversal policy for a widget subtree that has a
1790+
/// `descendantsAreFocusable` parameter to conditionally block focus for a
1791+
/// subtree.
1792+
final bool excluding;
1793+
1794+
/// The child widget of this [ExcludeFocusTraversal].
1795+
///
1796+
/// {@macro flutter.widgets.ProxyWidget.child}
1797+
final Widget child;
1798+
1799+
@override
1800+
Widget build(BuildContext context) {
1801+
return Focus(
1802+
canRequestFocus: false,
1803+
skipTraversal: true,
1804+
includeSemantics: false,
1805+
descendantsAreTraversable: !excluding,
1806+
child: child,
1807+
);
1808+
}
1809+
}

0 commit comments

Comments
 (0)