Skip to content

Commit 28ad215

Browse files
committed
home: Adjust semantics of bottom nav bar
Some of this has an effect I observes in my testing on iOS with VoiceOver: - MergeSemantics on each tab causes the tab to be represented by a single node instead of multiple, so it isn't double-visited when swiping right to traverse the tree. The rest is just a best effort at following the docs, which doesn't make the experience obviously worse, but I didn't notice an effect in my testing.
1 parent ca48d88 commit 28ad215

File tree

1 file changed

+54
-27
lines changed

1 file changed

+54
-27
lines changed

lib/widgets/home.dart

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'dart:async';
2-
import 'dart:ui';
32

43
import 'package:flutter/material.dart';
4+
import 'package:flutter/rendering.dart';
55

66
import '../generated/l10n/zulip_localizations.dart';
77
import '../model/narrow.dart';
@@ -48,6 +48,9 @@ class HomePage extends StatefulWidget {
4848
HomePage.buildRoute(accountId: accountId)));
4949
}
5050

51+
static String contentSemanticsIdentifier = 'home-page-content';
52+
static String titleSemanticsIdentifier = 'home-page-title';
53+
5154
@override
5255
State<HomePage> createState() => _HomePageState();
5356
}
@@ -96,15 +99,21 @@ class _HomePageState extends State<HomePage> {
9699

97100
return Scaffold(
98101
appBar: ZulipAppBar(titleSpacing: 16,
99-
title: Text(_currentTabTitle)),
100-
body: Stack(
101-
children: [
102-
for (final (tab, body) in pageBodies)
103-
// TODO(#535): Decide if we find it helpful to use something like
104-
// [SemanticsProperties.namesRoute] to structure this UI better
105-
// for screen-reader software.
106-
Offstage(offstage: tab != _tab.value, child: body),
107-
]),
102+
title: Semantics(
103+
identifier: HomePage.titleSemanticsIdentifier,
104+
namesRoute: true,
105+
child: Text(_currentTabTitle))),
106+
body: Semantics(
107+
role: SemanticsRole.tabPanel,
108+
identifier: HomePage.contentSemanticsIdentifier,
109+
container: true,
110+
explicitChildNodes: true,
111+
child: Stack(
112+
children: [
113+
for (final (tab, body) in pageBodies)
114+
Offstage(offstage: tab != _tab.value, child: body),
115+
]),
116+
),
108117
bottomNavigationBar: _BottomNavBar(tabNotifier: _tab));
109118
}
110119
}
@@ -149,7 +158,7 @@ class _BottomNavBar extends StatelessWidget {
149158
label: zulipLocalizations.navBarMenuLabel),
150159
];
151160

152-
return DecoratedBox(
161+
Widget result = DecoratedBox(
153162
decoration: BoxDecoration(
154163
border: Border(top: BorderSide(color: designVariables.borderBar)),
155164
color: designVariables.bgBotBar),
@@ -163,6 +172,14 @@ class _BottomNavBar extends StatelessWidget {
163172
for (final navigationBarButton in navigationBarButtons)
164173
Expanded(child: navigationBarButton),
165174
]))));
175+
176+
result = Semantics(
177+
container: true,
178+
explicitChildNodes: true,
179+
role: SemanticsRole.tabBar,
180+
child: result);
181+
182+
return result;
166183
}
167184
}
168185

@@ -262,7 +279,7 @@ class _NavigationBarButton extends StatelessWidget {
262279
final designVariables = DesignVariables.of(context);
263280
final color = selected ? designVariables.iconSelected : designVariables.icon;
264281

265-
return AnimatedScaleOnTap(
282+
Widget result = AnimatedScaleOnTap(
266283
scaleEnd: 0.875,
267284
duration: const Duration(milliseconds: 100),
268285
child: Material(
@@ -278,21 +295,31 @@ class _NavigationBarButton extends StatelessWidget {
278295
// text wrap before getting too close to the button's edge, which is
279296
// visible on tap-down.)
280297
padding: const EdgeInsets.fromLTRB(3, 6, 3, 3),
281-
child: Semantics(
282-
role: SemanticsRole.tab,
283-
selected: selected,
284-
child: Column(
285-
spacing: 3,
286-
mainAxisSize: MainAxisSize.min,
287-
children: [
288-
Icon(icon, size: 24, color: color),
289-
Flexible(
290-
child: Text(
291-
label,
292-
style: TextStyle(fontSize: 12, color: color, height: 12 / 12),
293-
textAlign: TextAlign.center,
294-
textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.5))),
295-
]))))));
298+
child: Column(
299+
spacing: 3,
300+
mainAxisSize: MainAxisSize.min,
301+
children: [
302+
Icon(icon, size: 24, color: color),
303+
Flexible(
304+
child: Text(
305+
label,
306+
style: TextStyle(fontSize: 12, color: color, height: 12 / 12),
307+
textAlign: TextAlign.center,
308+
textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.5))),
309+
])))));
310+
311+
result = MergeSemantics(
312+
child: Semantics(
313+
role: SemanticsRole.tab,
314+
controlsNodes: {
315+
HomePage.contentSemanticsIdentifier,
316+
HomePage.titleSemanticsIdentifier,
317+
},
318+
selected: selected,
319+
onTap: onPressed,
320+
child: result));
321+
322+
return result;
296323
}
297324
}
298325

0 commit comments

Comments
 (0)