Unverified Commit 4763be74 authored by chunhtai's avatar chunhtai Committed by GitHub

Can traverse if current focused node skips traversal (#130812)

Currently if the focus is on a focusnode that `skipTraversal = true`, the tab won't be able to traverse focus out of the node.

this pr fixes it
parent 9c8ee4fa
......@@ -352,7 +352,7 @@ abstract class FocusTraversalPolicy with Diagnosticable {
@protected
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode);
Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode) {
Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) {
final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy();
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{};
for (final FocusNode node in scope.descendants) {
......@@ -374,7 +374,10 @@ abstract class FocusTraversalPolicy with Diagnosticable {
}
// Skip non-focusable and non-traversable nodes in the same way that
// FocusScopeNode.traversalDescendants would.
if (node.canRequestFocus && !node.skipTraversal) {
//
// Current focused node needs to be in the group so that the caller can
// find the next traversable node from the current focused node.
if (node == currentNode || (node.canRequestFocus && !node.skipTraversal)) {
groups[groupNode] ??= _FocusTraversalGroupInfo(groupNode, members: <FocusNode>[], defaultPolicy: defaultPolicy);
assert(!groups[groupNode]!.members.contains(node));
groups[groupNode]!.members.add(node);
......@@ -388,7 +391,7 @@ abstract class FocusTraversalPolicy with Diagnosticable {
List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) {
final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope);
// Build the sorting data structure, separating descendants into groups.
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode);
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode, currentNode);
// Sort the member lists using the individual policy sorts.
for (final FocusNode? key in groups.keys) {
......@@ -397,6 +400,7 @@ abstract class FocusTraversalPolicy with Diagnosticable {
groups[key]!.members.addAll(sortedMembers);
}
// Traverse the group tree, adding the children of members in the order they
// appear in the member lists.
final List<FocusNode> sortedDescendants = <FocusNode>[];
......@@ -421,17 +425,29 @@ abstract class FocusTraversalPolicy with Diagnosticable {
// They were left in above because they were needed to find their members
// during sorting.
sortedDescendants.removeWhere((FocusNode node) {
return !node.canRequestFocus || node.skipTraversal;
return node != currentNode && (!node.canRequestFocus || node.skipTraversal);
});
// Sanity check to make sure that the algorithm above doesn't diverge from
// the one in FocusScopeNode.traversalDescendants in terms of which nodes it
// finds.
assert(
sortedDescendants.length <= scope.traversalDescendants.length && sortedDescendants.toSet().difference(scope.traversalDescendants.toSet()).isEmpty,
'Sorted descendants contains different nodes than FocusScopeNode.traversalDescendants would. '
'These are the different nodes: ${sortedDescendants.toSet().difference(scope.traversalDescendants.toSet())}',
);
assert((){
final Set<FocusNode> difference = sortedDescendants.toSet().difference(scope.traversalDescendants.toSet());
if (currentNode.skipTraversal) {
assert(
difference.length == 1 && difference.contains(currentNode),
'Sorted descendants contains different nodes than FocusScopeNode.traversalDescendants would. '
'These are the different nodes: ${difference.where((FocusNode node) => node != currentNode)}',
);
return true;
}
assert(
difference.isEmpty,
'Sorted descendants contains different nodes than FocusScopeNode.traversalDescendants would. '
'These are the different nodes: $difference',
);
return true;
}());
return sortedDescendants;
}
......@@ -453,7 +469,7 @@ abstract class FocusTraversalPolicy with Diagnosticable {
bool _moveFocus(FocusNode currentNode, {required bool forward}) {
final FocusScopeNode nearestScope = currentNode.nearestScope!;
invalidateScopeData(nearestScope);
final FocusNode? focusedChild = nearestScope.focusedChild;
FocusNode? focusedChild = nearestScope.focusedChild;
if (focusedChild == null) {
final FocusNode? firstFocus = forward ? findFirstFocus(currentNode) : findLastFocus(currentNode);
if (firstFocus != null) {
......@@ -464,8 +480,11 @@ abstract class FocusTraversalPolicy with Diagnosticable {
return true;
}
}
final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, currentNode);
if (sortedNodes.isEmpty) {
focusedChild ??= nearestScope;
final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, focusedChild);
assert(sortedNodes.contains(focusedChild));
if (sortedNodes.length < 2) {
// If there are no nodes to traverse to, like when descendantsAreTraversable
// is false or skipTraversal for all the nodes is true.
return false;
......@@ -473,7 +492,7 @@ abstract class FocusTraversalPolicy with Diagnosticable {
if (forward && focusedChild == sortedNodes.last) {
switch (nearestScope.traversalEdgeBehavior) {
case TraversalEdgeBehavior.leaveFlutterView:
focusedChild!.unfocus();
focusedChild.unfocus();
return false;
case TraversalEdgeBehavior.closedLoop:
requestFocusCallback(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
......@@ -483,7 +502,7 @@ abstract class FocusTraversalPolicy with Diagnosticable {
if (!forward && focusedChild == sortedNodes.first) {
switch (nearestScope.traversalEdgeBehavior) {
case TraversalEdgeBehavior.leaveFlutterView:
focusedChild!.unfocus();
focusedChild.unfocus();
return false;
case TraversalEdgeBehavior.closedLoop:
requestFocusCallback(sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart);
......
......@@ -812,7 +812,7 @@ void main() {
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isTrue);
expect(focusNode1.nextFocus(), isFalse);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
......
......@@ -271,7 +271,7 @@ void main() {
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isTrue);
expect(focusNode1.nextFocus(), isFalse);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
......
......@@ -414,7 +414,7 @@ void main() {
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isTrue);
expect(focusNode1.nextFocus(), isFalse);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
......
......@@ -6690,7 +6690,7 @@ void main() {
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isTrue);
expect(focusNode1.nextFocus(), isFalse);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
......
......@@ -908,6 +908,7 @@ void main() {
await tester.pumpWidget(
MaterialApp(
home: FocusableActionDetector(
focusNode: FocusNode(skipTraversal: true),
child: Column(
children: <Widget>[
ElevatedButton(
......@@ -938,6 +939,7 @@ void main() {
await tester.pumpWidget(
MaterialApp(
home: FocusableActionDetector(
focusNode: FocusNode(skipTraversal: true),
descendantsAreTraversable: false,
child: Column(
children: <Widget>[
......
......@@ -2090,6 +2090,61 @@ void main() {
expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue);
}, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
testWidgets('Focus traversal actions works when current focus skip traversal', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: 'key1');
final GlobalKey key2 = GlobalKey(debugLabel: 'key2');
final GlobalKey key3 = GlobalKey(debugLabel: 'key3');
await tester.pumpWidget(
WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return TestRoute(
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
debugLabel: 'scope',
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Focus(
autofocus: true,
skipTraversal: true,
debugLabel: '1',
child: SizedBox(width: 100, height: 100, key: key1),
),
Focus(
debugLabel: '2',
child: SizedBox(width: 100, height: 100, key: key2),
),
Focus(
debugLabel: '3',
child: SizedBox(width: 100, height: 100, key: key3),
),
],
),
],
),
),
),
);
},
),
);
expect(Focus.of(key1.currentContext!).hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
expect(Focus.of(key2.currentContext!).hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
expect(Focus.of(key3.currentContext!).hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
// Skips key 1 because it skips traversal.
expect(Focus.of(key2.currentContext!).hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
expect(Focus.of(key3.currentContext!).hasPrimaryFocus, isTrue);
}, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
testWidgets('Focus traversal inside a vertical scrollable scrolls to stay visible.', (WidgetTester tester) async {
final List<int> items = List<int>.generate(11, (int index) => index).toList();
final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList();
......
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