When traversing from one focus group to the next (with tab on keyboard) I expect the focus to move to the first field in the next group, but it seems to focus on nothing - and then another tab moves into that group.
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (var i = 0; i < 2; i++)
FocusableActionDetector(
onFocusChange: (focused) {
if (!focused) {
print('Have left focus group');
}
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < 3; i++)
SizedBox(
width: 150,
child: TextField(),
),
],
),
),
],
),
),
),
);
}
}
I expect that after the last field in the first group a press of Tab
should move focus immediately to the first item in the next focus group.
I have tried all manner of FocusNode
, FocusScope
, FocusScope.of(context).___
, however I am finding the focus management in Flutter a bit confusing.
Preface: I'm not an expert on Focus
and find it quite convoluted myself.
But the below works for what you're after, I believe, traversing from one column to the next, without focusing "invisible" items. At least on mobile devices. Web platform... that's a whole different ballgame (and I doubt it works the same).
I got rid of the FocusableActionDetector
(which acts as a FocusNode
itself) and wrapped each Column in a FocusTraversalGroup
. I believe Flutter tries to go from TraversalGroup to TraversalGroup when it can.
The FocusScope
wrapping the TraversalGroups prevents the Back button and any other clickable items from getting focus (once the FocusScope
has gained focus).
class MyHomePage2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
width: double.infinity,
child: FocusScope( // LIMIT FOCUS TO DESCENDANTS
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (var i = 0; i < 2; i++)
FocusTraversalGroup( // CREATE GROUPS HERE
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < 3; i++)
SizedBox(
width: 150,
child: TextField(),
),
],
),
),
],
),
),
),
),
);
}
}
(to the best of my knowledge, which is fuzzy)
FocusScope
limits node traversal to its direct descendant nodes
FocusScope
to another FocusScope
FocusScope
the user will need to either manually/click-focus the other FocusScope
, or its descendant nodes must requestFocus
FocusScope
instead of FocusTraversalGroup
above, would limit node traversal to one Column
, whichever got focus. It would not jump from one Column
to the next when reaching last TextField
FocusTraversalGroup
collects descendants into a group for traversal
FocusTraversalGroup
is availableDumping the focus tree using debugDumpFocusTree
(a static function available everywhere) can be helpful in debugging.
I sometimes add it to the AppBar for easy, on-demand access:
return Scaffold(
appBar: AppBar(
title: Text('Focus Tab Page'),
actions: [
IconButton(icon: Icon(Icons.info_outline), onPressed: debugDumpFocusTree)
],
),
I've copied to relevant part below.
└─Child 2: FocusScopeNode#c83f0(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])
│ context: FocusScope
│ IN FOCUS PATH
│ focusedChildren: FocusScopeNode#7d536([IN FOCUS PATH])
│
├─Child 1: FocusScopeNode#7d536([IN FOCUS PATH])
│ │ context: FocusScope
│ │ IN FOCUS PATH
│ │ focusedChildren: FocusNode#1b886([PRIMARY FOCUS]),
│ │ FocusNode#72c3b, FocusNode#34b25, FocusNode#3b410,
│ │ FocusNode#02fac, FocusNode#61cd5
│ │
│ ├─Child 1: FocusNode#1d51e(FocusTraversalGroup)
│ │ │ context: Focus
│ │ │ NOT FOCUSABLE
│ │ │
│ │ ├─Child 1: FocusNode#3b410
│ │ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#70699]
│ │ │
│ │ ├─Child 2: FocusNode#34b25
│ │ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#7c822]
│ │ │
│ │ └─Child 3: FocusNode#72c3b
│ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#f00bc]
│ │
│ └─Child 2: FocusNode#bdb17(FocusTraversalGroup [IN FOCUS PATH])
│ │ context: Focus
│ │ NOT FOCUSABLE
│ │ IN FOCUS PATH
│ │
│ ├─Child 1: FocusNode#1b886([PRIMARY FOCUS])
│ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#c5a56]
│ │ PRIMARY FOCUS
│ │
│ ├─Child 2: FocusNode#61cd5
│ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#a4dd8]
│ │
│ └─Child 3: FocusNode#02fac
│ context: EditableText-[LabeledGlobalKey<EditableTextState>#fe12d]
│
├─Child 2: FocusNode#4886e
│ context: Focus
│
└─Child 3: FocusNode#9241b
context: Focus