Skip to content

Commit 62added

Browse files
authored
Fix focus switching when changing tabs in CupertinoTabScaffold (flutter#14431)
* Fix focus switching when changing tabs in CupertinoTabScaffold * review * Test focuses are saved within tabs * review
1 parent 681192a commit 62added

File tree

2 files changed

+163
-14
lines changed

2 files changed

+163
-14
lines changed

packages/flutter/lib/src/cupertino/tab_scaffold.dart

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
134134
Widget build(BuildContext context) {
135135
final List<Widget> stacked = <Widget>[];
136136

137-
Widget content = new _TabView(
137+
Widget content = new _TabSwitchingView(
138138
currentTabIndex: _currentPage,
139139
tabNumber: widget.tabBar.items.length,
140140
tabBuilder: widget.tabBuilder,
@@ -197,10 +197,10 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
197197
}
198198
}
199199

200-
/// An widget laying out multiple tabs with only one active tab being built
200+
/// A widget laying out multiple tabs with only one active tab being built
201201
/// at a time and on stage. Off stage tabs' animations are stopped.
202-
class _TabView extends StatefulWidget {
203-
const _TabView({
202+
class _TabSwitchingView extends StatefulWidget {
203+
const _TabSwitchingView({
204204
@required this.currentTabIndex,
205205
@required this.tabNumber,
206206
@required this.tabBuilder,
@@ -213,16 +213,45 @@ class _TabView extends StatefulWidget {
213213
final IndexedWidgetBuilder tabBuilder;
214214

215215
@override
216-
_TabViewState createState() => new _TabViewState();
216+
_TabSwitchingViewState createState() => new _TabSwitchingViewState();
217217
}
218218

219-
class _TabViewState extends State<_TabView> {
219+
class _TabSwitchingViewState extends State<_TabSwitchingView> {
220220
List<Widget> tabs;
221+
List<FocusScopeNode> tabFocusNodes;
221222

222223
@override
223224
void initState() {
224225
super.initState();
225226
tabs = new List<Widget>(widget.tabNumber);
227+
tabFocusNodes = new List<FocusScopeNode>.generate(
228+
widget.tabNumber,
229+
(int index) => new FocusScopeNode(),
230+
);
231+
}
232+
233+
@override
234+
void didChangeDependencies() {
235+
super.didChangeDependencies();
236+
_focusActiveTab();
237+
}
238+
239+
@override
240+
void didUpdateWidget(_TabSwitchingView oldWidget) {
241+
super.didUpdateWidget(oldWidget);
242+
_focusActiveTab();
243+
}
244+
245+
void _focusActiveTab() {
246+
FocusScope.of(context).setFirstFocus(tabFocusNodes[widget.currentTabIndex]);
247+
}
248+
249+
@override
250+
void dispose() {
251+
for (FocusScopeNode focusScopeNode in tabFocusNodes) {
252+
focusScopeNode.detach();
253+
}
254+
super.dispose();
226255
}
227256

228257
@override
@@ -232,14 +261,18 @@ class _TabViewState extends State<_TabView> {
232261
children: new List<Widget>.generate(widget.tabNumber, (int index) {
233262
final bool active = index == widget.currentTabIndex;
234263

235-
if (active || tabs[index] != null)
264+
if (active || tabs[index] != null) {
236265
tabs[index] = widget.tabBuilder(context, index);
266+
}
237267

238268
return new Offstage(
239269
offstage: !active,
240270
child: new TickerMode(
241271
enabled: active,
242-
child: tabs[index] ?? new Container(),
272+
child: new FocusScope(
273+
node: tabFocusNodes[index],
274+
child: tabs[index] ?? new Container(),
275+
),
243276
),
244277
);
245278
}),

packages/flutter/test/cupertino/tab_scaffold_test.dart

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/cupertino.dart';
6+
import 'package:flutter/material.dart';
67
import 'package:flutter_test/flutter_test.dart';
78

89
import '../painting/mocks_for_image_cache.dart';
@@ -34,7 +35,7 @@ void main() {
3435
onPaint: () { tabsPainted.add(index); }
3536
)
3637
);
37-
}
38+
},
3839
);
3940
},
4041
);
@@ -88,11 +89,11 @@ void main() {
8889
settings: settings,
8990
builder: (BuildContext context) {
9091
return new CupertinoTabScaffold(
91-
tabBar: _buildTabBar(),
92-
tabBuilder: (BuildContext context, int index) {
93-
tabsBuilt.add(index);
94-
return new Text('Page ${index + 1}');
95-
}
92+
tabBar: _buildTabBar(),
93+
tabBuilder: (BuildContext context, int index) {
94+
tabsBuilt.add(index);
95+
return new Text('Page ${index + 1}');
96+
},
9697
);
9798
},
9899
);
@@ -119,6 +120,121 @@ void main() {
119120
expect(find.text('Page 1'), findsOneWidget);
120121
expect(find.text('Page 2', skipOffstage: false), isOffstage);
121122
});
123+
124+
testWidgets('Last tab gets focus', (WidgetTester tester) async {
125+
// 2 nodes for 2 tabs
126+
final List<FocusNode> focusNodes = <FocusNode>[new FocusNode(), new FocusNode()];
127+
128+
await tester.pumpWidget(
129+
new WidgetsApp(
130+
color: const Color(0xFFFFFFFF),
131+
onGenerateRoute: (RouteSettings settings) {
132+
return new CupertinoPageRoute<Null>(
133+
settings: settings,
134+
builder: (BuildContext context) {
135+
return new CupertinoTabScaffold(
136+
tabBar: _buildTabBar(),
137+
tabBuilder: (BuildContext context, int index) {
138+
return new TextField(
139+
focusNode: focusNodes[index],
140+
autofocus: true,
141+
);
142+
},
143+
);
144+
},
145+
);
146+
},
147+
),
148+
);
149+
150+
expect(focusNodes[0].hasFocus, isTrue);
151+
152+
await tester.tap(find.text('Tab 2'));
153+
await tester.pump();
154+
155+
expect(focusNodes[0].hasFocus, isFalse);
156+
expect(focusNodes[1].hasFocus, isTrue);
157+
158+
await tester.tap(find.text('Tab 1'));
159+
await tester.pump();
160+
161+
expect(focusNodes[0].hasFocus, isTrue);
162+
expect(focusNodes[1].hasFocus, isFalse);
163+
});
164+
165+
testWidgets('Do not affect focus order in the route', (WidgetTester tester) async {
166+
final List<FocusNode> focusNodes = <FocusNode>[
167+
new FocusNode(), new FocusNode(), new FocusNode(), new FocusNode(),
168+
];
169+
170+
await tester.pumpWidget(
171+
new WidgetsApp(
172+
color: const Color(0xFFFFFFFF),
173+
onGenerateRoute: (RouteSettings settings) {
174+
return new CupertinoPageRoute<Null>(
175+
settings: settings,
176+
builder: (BuildContext context) {
177+
return new Material(
178+
child: new CupertinoTabScaffold(
179+
tabBar: _buildTabBar(),
180+
tabBuilder: (BuildContext context, int index) {
181+
return new Column(
182+
children: <Widget>[
183+
new TextField(
184+
focusNode: focusNodes[index * 2],
185+
decoration: const InputDecoration(
186+
hintText: 'TextField 1',
187+
),
188+
),
189+
new TextField(
190+
focusNode: focusNodes[index * 2 + 1],
191+
decoration: const InputDecoration(
192+
hintText: 'TextField 2',
193+
),
194+
),
195+
],
196+
);
197+
},
198+
),
199+
);
200+
},
201+
);
202+
},
203+
),
204+
);
205+
206+
expect(
207+
focusNodes.any((FocusNode node) => node.hasFocus),
208+
isFalse,
209+
);
210+
211+
await tester.tap(find.widgetWithText(TextField, 'TextField 2'));
212+
213+
expect(
214+
focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)),
215+
1,
216+
);
217+
218+
await tester.tap(find.text('Tab 2'));
219+
await tester.pump();
220+
221+
await tester.tap(find.widgetWithText(TextField, 'TextField 1'));
222+
223+
expect(
224+
focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)),
225+
2,
226+
);
227+
228+
await tester.tap(find.text('Tab 1'));
229+
await tester.pump();
230+
231+
// Upon going back to tab 1, the item it tab 1 that previously had the focus
232+
// (TextField 2) gets it back.
233+
expect(
234+
focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)),
235+
1,
236+
);
237+
});
122238
}
123239

124240
CupertinoTabBar _buildTabBar() {

0 commit comments

Comments
 (0)