Skip to content

Commit 4603bd7

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 4603bd7

File tree

1 file changed

+53
-27
lines changed

1 file changed

+53
-27
lines changed

lib/widgets/home.dart

Lines changed: 53 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,20 @@ 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+
])),
108116
bottomNavigationBar: _BottomNavBar(tabNotifier: _tab));
109117
}
110118
}
@@ -156,7 +164,7 @@ class _BottomNavBar extends StatelessWidget {
156164
onPressed: () => _showMainMenu(context, tabNotifier: tabNotifier)),
157165
];
158166

159-
return DecoratedBox(
167+
Widget result = DecoratedBox(
160168
decoration: BoxDecoration(
161169
border: Border(top: BorderSide(color: designVariables.borderBar)),
162170
color: designVariables.bgBotBar),
@@ -172,6 +180,14 @@ class _BottomNavBar extends StatelessWidget {
172180
for (final navigationBarButton in navigationBarButtons)
173181
Expanded(child: navigationBarButton),
174182
])))));
183+
184+
result = Semantics(
185+
container: true,
186+
explicitChildNodes: true,
187+
role: SemanticsRole.tabBar,
188+
child: result);
189+
190+
return result;
175191
}
176192
}
177193

@@ -271,7 +287,7 @@ class _NavigationBarButton extends StatelessWidget {
271287
final designVariables = DesignVariables.of(context);
272288
final color = selected ? designVariables.iconSelected : designVariables.icon;
273289

274-
return AnimatedScaleOnTap(
290+
Widget result = AnimatedScaleOnTap(
275291
scaleEnd: 0.875,
276292
duration: const Duration(milliseconds: 100),
277293
child: Material(
@@ -287,21 +303,31 @@ class _NavigationBarButton extends StatelessWidget {
287303
// text wrap before getting too close to the button's edge, which is
288304
// visible on tap-down.)
289305
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-
]))))));
306+
child: Column(
307+
spacing: 3,
308+
mainAxisSize: MainAxisSize.min,
309+
children: [
310+
Icon(icon, size: 24, color: color),
311+
Flexible(
312+
child: Text(
313+
label,
314+
style: TextStyle(fontSize: 12, color: color, height: 12 / 12),
315+
textAlign: TextAlign.center,
316+
textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.5))),
317+
])))));
318+
319+
result = MergeSemantics(
320+
child: Semantics(
321+
role: SemanticsRole.tab,
322+
controlsNodes: {
323+
HomePage.contentSemanticsIdentifier,
324+
HomePage.titleSemanticsIdentifier,
325+
},
326+
selected: selected,
327+
onTap: onPressed,
328+
child: result));
329+
330+
return result;
305331
}
306332
}
307333

0 commit comments

Comments
 (0)