Skip to content

Conversation

@yiiim
Copy link
Member

@yiiim yiiim commented Jan 8, 2025

Fixes: #160843

This PR adds edge behavior feature for traversing focus using arrow keys. This allows cycling through the focus within the FocusScope in a closed loop using the arrow keys. Which may be needed by TV application developers.

Additionally, as in case #160843, TraversalEdgeBehavior.parentScope can be used to allow nested Navigators to traverse focus beyond the Navigator using arrow keys.

MaterialApp(
  home: Column(
    children: <Widget>[
      makeFocus(0),
      Navigator(
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<void>(
            traversalDirectionedEdgeBehavior: TraversalEdgeBehavior.parentScope,
            builder: (BuildContext context) {
              return Column(
                children: <Widget>[
                  makeFocus(1),
                  makeFocus(2),
                ],
              );
            },
          );
        },
      ),
      makeFocus(3),
    ],
  ),
);

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@yiiim yiiim marked this pull request as draft January 8, 2025 04:44
@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. f: routes Navigator, Router, and related APIs. f: focus Focus traversal, gaining or losing focus labels Jan 8, 2025
@yiiim yiiim marked this pull request as ready for review January 8, 2025 15:52
@yiiim yiiim requested a review from gspencergoog January 8, 2025 15:54
@yiiim yiiim added the c: new feature Nothing broken; request for a new capability label Jan 8, 2025
/// [closedLoop] behavior.
parentScope,

/// Stops the focus traversal at the edge of the focus scope.
Copy link
Contributor

@gspencergoog gspencergoog Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really the edge of the focus scope? Isn't it actually the edge of the window? Directional traversal isn't supposed to be constrained by focus scopes, at least according to the description of DirectionalFocusTraversalPolicyMixin, where it describes an infinite band that it searches for candidates (which is what happens when it's looking for the first focus, at least, although I think we do start with traversalDescendants when we've found the first focus).

It might be good to think about what the actual bounds are on the search, and at least update the documentation to match the real behavior. From what I understand of the existing code and your change, it seems like the behavior is to search globally for the first one, and after that it's bound to the scope that the first focus resides in. Is that correct? And is it desirable? It should mirror what the native platforms do in the case of directional traversal. I think at least Android has a default traversal that I don't think is constrained by the view.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should directional traversal be constrained by FocusScope? This is a debatable question. I believe it should be constrained, as it provides more options, and we can achieve an unconstrained effect through TraversalEdgeBehavior. Additionally, directional traversal could not go beyond FocusScope before, so making it unconstrained would be a breaking change. When reaching the first focus, the traversal behavior will adopt the FocusScope where the focus is located. This is how it is currently handled, and from my observation, it seems to be the case in _moveFocus as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that makes sense, and we don't need to decide the debate here, given the existing behavior seems to be working ok already. However, can you at least update your documentation to explain what "outermost" means in practice (is it the ones closest to the edge of the bounding box containing all the nodes in the scope? Something else?)?

And also please update the documentation for DirectionalFocusTraversalPolicyMixin so that it correctly describes what the algorithm actually does.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some documents.

@yiiim yiiim requested a review from gspencergoog January 9, 2025 05:42
Copy link
Contributor

@gspencergoog gspencergoog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

32384589-a60f0e74-c078-11e7-9bc1-e5b5287aea9d

Thanks for all the work here, I know this is complex code to understand and modify.

@yiiim yiiim added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 14, 2025
@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jan 14, 2025
@auto-submit
Copy link
Contributor

auto-submit bot commented Jan 14, 2025

auto label is removed for flutter/flutter/161285, due to - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

@jmagman
Copy link
Member

jmagman commented Jan 17, 2025

@gspencergoog could you help with the Google testing failure (in TAP local_focus_test.dart), it actually looks legit.

@gspencergoog
Copy link
Contributor

Yeah, it is legit. It's going to be tough to debug, though. The Google test is testing focus changes initiated with keyboard directional input, but it's using a custom focus scope widget and a custom focus traversal policy, both of which are quite complex, so it's hard to know if that code is suspect.

It's clear that this PR is changing the default behavior in some way, though. So maybe that's where to start, @yiiim: check the code and see where it might be changing the default behavior, or the default returned values from the policy functions.

@yiiim
Copy link
Member Author

yiiim commented Jan 17, 2025

This is really exciting 🙈! Let me give it a try.

@yiiim yiiim force-pushed the directional_focus_traversal_edge_behavior branch from 5130e4b to df3f6f6 Compare January 17, 2025 04:31
@yiiim
Copy link
Member Author

yiiim commented Jan 17, 2025

I did it!

@gspencergoog Do you want to take another look?I modified it to not use _requestTraversalFocusInDirection to request focus after finding firstFocus. This keeps the behavior consistent with before.

final FocusNode firstFocus = findFirstFocusInDirection(currentNode, direction) ?? currentNode;

@gspencergoog
Copy link
Contributor

I did it!

Excellent work! The test indeed appears to pass now.

@gspencergoog Do you want to take another look?I modified it to not use _requestTraversalFocusInDirection to request focus after finding firstFocus. This keeps the behavior consistent with before.

Could you please add a test that verifies that? If we'd had one before, it would have alerted you while you were working on it, so we're clearly missing that case.

focus traversal
@yiiim yiiim force-pushed the directional_focus_traversal_edge_behavior branch from df3f6f6 to b992143 Compare January 18, 2025 04:45
@yiiim
Copy link
Member Author

yiiim commented Jan 18, 2025

The new test has been added:

testWidgets('When there is no focused node, the focus can be set to the FocusScopeNode.', (
 WidgetTester tester,
) async {
 final FocusScopeNode scope = FocusScopeNode();
 final FocusScopeNode childScope = FocusScopeNode();
 final FocusNode nodeA = FocusNode();
 await tester.pumpWidget(
   Directionality(
     textDirection: TextDirection.ltr,
     child: FocusScope(
       node: scope,
       child: FocusScope(
         node: childScope,
         child: Padding(
           padding: const EdgeInsets.only(top: 10),
           child: Focus(focusNode: nodeA, child: const Text('A')),
         ),
       ),
     ),
   ),
 );
 expect(scope.focusInDirection(TraversalDirection.down), isTrue);
 await tester.pump();
 expect(childScope.hasFocus, isTrue);
 expect(nodeA.hasFocus, isFalse);
});

But should we do this? Does it make sense for the focus to be on the FocusScopeNode?

@Piinks Piinks requested a review from gspencergoog February 5, 2025 19:15
Copy link
Contributor

@gspencergoog gspencergoog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

32384589-a60f0e74-c078-11e7-9bc1-e5b5287aea9d

@gspencergoog
Copy link
Contributor

But should we do this? Does it make sense for the focus to be on the FocusScopeNode?

Yes, I think that makes sense. The scope node should focus the appropriate thing next when it starts with the focus.

@yiiim yiiim added the autosubmit Merge PR when tree becomes green via auto submit App label Feb 7, 2025
@auto-submit auto-submit bot added this pull request to the merge queue Feb 7, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 7, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 7, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 8, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 8, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 9, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 9, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 20, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 20, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: new feature Nothing broken; request for a new capability f: focus Focus traversal, gaining or losing focus f: material design flutter/packages/flutter/material repository. f: routes Navigator, Router, and related APIs. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants