@@ -84,8 +84,43 @@ enum TraversalDirection {
8484 left,
8585}
8686
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] .
89124///
90125/// The focus traversal policy is what determines which widget is "next",
91126/// "previous", or in a direction from the widget associated with the currently
@@ -407,12 +442,24 @@ abstract class FocusTraversalPolicy with Diagnosticable {
407442 return false ;
408443 }
409444 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+ }
412453 }
413454 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+ }
416463 }
417464
418465 final Iterable <FocusNode > maybeFlipped = forward ? sortedNodes : sortedNodes.reversed;
@@ -1592,7 +1639,7 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
15921639 // The internal focus node used to collect the children of this node into a
15931640 // group, and to provide a context for the traversal algorithm to sort the
15941641 // group with.
1595- FocusNode ? focusNode;
1642+ late final FocusNode focusNode;
15961643
15971644 @override
15981645 void initState () {
@@ -1606,15 +1653,15 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
16061653
16071654 @override
16081655 void dispose () {
1609- focusNode? .dispose ();
1656+ focusNode.dispose ();
16101657 super .dispose ();
16111658 }
16121659
16131660 @override
16141661 Widget build (BuildContext context) {
16151662 return _FocusTraversalGroupMarker (
16161663 policy: widget.policy,
1617- focusNode: focusNode! ,
1664+ focusNode: focusNode,
16181665 child: Focus (
16191666 focusNode: focusNode,
16201667 canRequestFocus: false ,
@@ -1705,9 +1752,20 @@ class NextFocusIntent extends Intent {
17051752///
17061753/// See [FocusTraversalPolicy] for more information about focus traversal.
17071754class 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+
17081766 @override
1709- void invoke (NextFocusIntent intent) {
1710- primaryFocus ! . nextFocus () ;
1767+ KeyEventResult toKeyEventResult (NextFocusIntent intent, bool invokeResult ) {
1768+ return invokeResult ? KeyEventResult .handled : KeyEventResult .skipRemainingHandlers ;
17111769 }
17121770}
17131771
@@ -1729,9 +1787,20 @@ class PreviousFocusIntent extends Intent {
17291787///
17301788/// See [FocusTraversalPolicy] for more information about focus traversal.
17311789class 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+
17321801 @override
1733- void invoke (PreviousFocusIntent intent) {
1734- primaryFocus ! . previousFocus () ;
1802+ KeyEventResult toKeyEventResult (PreviousFocusIntent intent, bool invokeResult ) {
1803+ return invokeResult ? KeyEventResult .handled : KeyEventResult .skipRemainingHandlers ;
17351804 }
17361805}
17371806
0 commit comments