@@ -84,8 +84,43 @@ enum TraversalDirection {
84
84
left,
85
85
}
86
86
87
- /// An object used to specify a focus traversal policy used for configuring a
88
- /// [FocusTraversalGroup] widget.
87
+ /// Controls the transfer of focus beyond the first and the last items of a
88
+ /// [FocusScopeNode] .
89
+ ///
90
+ /// This enumeration only controls the traversal behavior performed by
91
+ /// [FocusTraversalPolicy] . Other methods of focus transfer, such as direct
92
+ /// calls to [FocusNode.requestFocus] and [FocusNode.unfocus] , are not affected
93
+ /// by this enumeration.
94
+ ///
95
+ /// See also:
96
+ ///
97
+ /// * [FocusTraversalPolicy] , which implements the logic behind this enum.
98
+ /// * [FocusScopeNode] , which is configured by this enum.
99
+ enum TraversalEdgeBehavior {
100
+ /// Keeps the focus among the items of the focus scope.
101
+ ///
102
+ /// Requesting the next focus after the last focusable item will transfer the
103
+ /// focus to the first item, and requesting focus previous to the first item
104
+ /// will transfer the focus to the last item, thus forming a closed loop of
105
+ /// focusable items.
106
+ closedLoop,
107
+
108
+ /// Allows the focus to leave the [FlutterView] .
109
+ ///
110
+ /// Requesting next focus after the last focusable item or previous to the
111
+ /// first item will unfocus any focused nodes. If the focus traversal action
112
+ /// was initiated by the embedder (e.g. the Flutter Engine) the embedder
113
+ /// receives a result indicating that the focus is no longer within the
114
+ /// current [FlutterView] . For example, [NextFocusAction] invoked via keyboard
115
+ /// (typically the TAB key) would receive [KeyEventResult.skipRemainingHandlers]
116
+ /// allowing the embedder handle the shortcut. On the web, typically the
117
+ /// control is transfered to the browser, allowing the user to reach the
118
+ /// address bar, escape an `iframe` , or focus on HTML elements other than
119
+ /// those managed by Flutter.
120
+ leaveFlutterView,
121
+ }
122
+
123
+ /// Determines how focusable widgets are traversed within a [FocusTraversalGroup] .
89
124
///
90
125
/// The focus traversal policy is what determines which widget is "next",
91
126
/// "previous", or in a direction from the widget associated with the currently
@@ -407,12 +442,24 @@ abstract class FocusTraversalPolicy with Diagnosticable {
407
442
return false ;
408
443
}
409
444
if (forward && focusedChild == sortedNodes.last) {
410
- _focusAndEnsureVisible (sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy .keepVisibleAtEnd);
411
- return true ;
445
+ switch (nearestScope.traversalEdgeBehavior) {
446
+ case TraversalEdgeBehavior .leaveFlutterView:
447
+ focusedChild! .unfocus ();
448
+ return false ;
449
+ case TraversalEdgeBehavior .closedLoop:
450
+ _focusAndEnsureVisible (sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy .keepVisibleAtEnd);
451
+ return true ;
452
+ }
412
453
}
413
454
if (! forward && focusedChild == sortedNodes.first) {
414
- _focusAndEnsureVisible (sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy .keepVisibleAtStart);
415
- return true ;
455
+ switch (nearestScope.traversalEdgeBehavior) {
456
+ case TraversalEdgeBehavior .leaveFlutterView:
457
+ focusedChild! .unfocus ();
458
+ return false ;
459
+ case TraversalEdgeBehavior .closedLoop:
460
+ _focusAndEnsureVisible (sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy .keepVisibleAtStart);
461
+ return true ;
462
+ }
416
463
}
417
464
418
465
final Iterable <FocusNode > maybeFlipped = forward ? sortedNodes : sortedNodes.reversed;
@@ -1592,7 +1639,7 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
1592
1639
// The internal focus node used to collect the children of this node into a
1593
1640
// group, and to provide a context for the traversal algorithm to sort the
1594
1641
// group with.
1595
- FocusNode ? focusNode;
1642
+ late final FocusNode focusNode;
1596
1643
1597
1644
@override
1598
1645
void initState () {
@@ -1606,15 +1653,15 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
1606
1653
1607
1654
@override
1608
1655
void dispose () {
1609
- focusNode? .dispose ();
1656
+ focusNode.dispose ();
1610
1657
super .dispose ();
1611
1658
}
1612
1659
1613
1660
@override
1614
1661
Widget build (BuildContext context) {
1615
1662
return _FocusTraversalGroupMarker (
1616
1663
policy: widget.policy,
1617
- focusNode: focusNode! ,
1664
+ focusNode: focusNode,
1618
1665
child: Focus (
1619
1666
focusNode: focusNode,
1620
1667
canRequestFocus: false ,
@@ -1705,9 +1752,20 @@ class NextFocusIntent extends Intent {
1705
1752
///
1706
1753
/// See [FocusTraversalPolicy] for more information about focus traversal.
1707
1754
class NextFocusAction extends Action <NextFocusIntent > {
1755
+ /// Attempts to pass the focus to the next widget.
1756
+ ///
1757
+ /// Returns true if a widget was focused as a result of invoking this action.
1758
+ ///
1759
+ /// Returns false when the traversal reached the end and the engine must pass
1760
+ /// focus to platform UI.
1761
+ @override
1762
+ bool invoke (NextFocusIntent intent) {
1763
+ return primaryFocus! .nextFocus ();
1764
+ }
1765
+
1708
1766
@override
1709
- void invoke (NextFocusIntent intent) {
1710
- primaryFocus ! . nextFocus () ;
1767
+ KeyEventResult toKeyEventResult (NextFocusIntent intent, bool invokeResult ) {
1768
+ return invokeResult ? KeyEventResult .handled : KeyEventResult .skipRemainingHandlers ;
1711
1769
}
1712
1770
}
1713
1771
@@ -1729,9 +1787,20 @@ class PreviousFocusIntent extends Intent {
1729
1787
///
1730
1788
/// See [FocusTraversalPolicy] for more information about focus traversal.
1731
1789
class PreviousFocusAction extends Action <PreviousFocusIntent > {
1790
+ /// Attempts to pass the focus to the previous widget.
1791
+ ///
1792
+ /// Returns true if a widget was focused as a result of invoking this action.
1793
+ ///
1794
+ /// Returns false when the traversal reached the beginning and the engine must
1795
+ /// pass focus to platform UI.
1796
+ @override
1797
+ bool invoke (PreviousFocusIntent intent) {
1798
+ return primaryFocus! .previousFocus ();
1799
+ }
1800
+
1732
1801
@override
1733
- void invoke (PreviousFocusIntent intent) {
1734
- primaryFocus ! . previousFocus () ;
1802
+ KeyEventResult toKeyEventResult (PreviousFocusIntent intent, bool invokeResult ) {
1803
+ return invokeResult ? KeyEventResult .handled : KeyEventResult .skipRemainingHandlers ;
1735
1804
}
1736
1805
}
1737
1806
0 commit comments