Skip to content

Commit 7db3cd4

Browse files
home: Adjust semantics of bottom nav bar
Fixes #1857. Fixes #1960. 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. Co-authored-by: chrisbobbe <[email protected]>
1 parent 0c1fe29 commit 7db3cd4

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
}
@@ -156,7 +165,7 @@ class _BottomNavBar extends StatelessWidget {
156165
onPressed: () => _showMainMenu(context, tabNotifier: tabNotifier)),
157166
];
158167

159-
return DecoratedBox(
168+
Widget result = DecoratedBox(
160169
decoration: BoxDecoration(
161170
border: Border(top: BorderSide(color: designVariables.borderBar)),
162171
color: designVariables.bgBotBar),
@@ -172,6 +181,14 @@ class _BottomNavBar extends StatelessWidget {
172181
for (final navigationBarButton in navigationBarButtons)
173182
Expanded(child: navigationBarButton),
174183
])))));
184+
185+
result = Semantics(
186+
container: true,
187+
explicitChildNodes: true,
188+
role: SemanticsRole.tabBar,
189+
child: result);
190+
191+
return result;
175192
}
176193
}
177194

@@ -271,7 +288,7 @@ class _NavigationBarButton extends StatelessWidget {
271288
final designVariables = DesignVariables.of(context);
272289
final color = selected ? designVariables.iconSelected : designVariables.icon;
273290

274-
return AnimatedScaleOnTap(
291+
Widget result = AnimatedScaleOnTap(
275292
scaleEnd: 0.875,
276293
duration: const Duration(milliseconds: 100),
277294
child: Material(
@@ -287,21 +304,31 @@ class _NavigationBarButton extends StatelessWidget {
287304
// text wrap before getting too close to the button's edge, which is
288305
// visible on tap-down.)
289306
padding: const EdgeInsets.fromLTRB(3, 6, 3, 3),
290-
child: Semantics(
291-
role: SemanticsRole.tab,
292-
selected: selected,
293-
child: Column(
294-
spacing: 3,
295-
mainAxisSize: MainAxisSize.min,
296-
children: [
297-
Icon(icon, size: 24, color: color),
298-
Flexible(
299-
child: Text(
300-
label,
301-
style: TextStyle(fontSize: 12, color: color, height: 12 / 12),
302-
textAlign: TextAlign.center,
303-
textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.5))),
304-
]))))));
307+
child: Column(
308+
spacing: 3,
309+
mainAxisSize: MainAxisSize.min,
310+
children: [
311+
Icon(icon, size: 24, color: color),
312+
Flexible(
313+
child: Text(
314+
label,
315+
style: TextStyle(fontSize: 12, color: color, height: 12 / 12),
316+
textAlign: TextAlign.center,
317+
textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.5))),
318+
])))));
319+
320+
result = MergeSemantics(
321+
child: Semantics(
322+
role: SemanticsRole.tab,
323+
controlsNodes: {
324+
HomePage.contentSemanticsIdentifier,
325+
HomePage.titleSemanticsIdentifier,
326+
},
327+
selected: selected,
328+
onTap: onPressed,
329+
child: result));
330+
331+
return result;
305332
}
306333
}
307334

0 commit comments

Comments
 (0)