Skip to content

Commit 15ed7bd

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: MritunjayTiwari14 <shreyashtiwari14.11@gmail.com>
1 parent 612a782 commit 15ed7bd

File tree

3 files changed

+1561
-62
lines changed

3 files changed

+1561
-62
lines changed

lib/widgets/home.dart

Lines changed: 89 additions & 52 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
}
@@ -114,13 +122,17 @@ class _BottomNavBar extends StatelessWidget {
114122

115123
final ValueNotifier<_HomePageTab> tabNotifier;
116124

117-
_NavigationBarButton _button(_HomePageTab tab, IconData icon, String label) {
125+
_NavigationBarButton _button({
126+
required _HomePageTab tab,
127+
required IconData icon,
128+
required String label
129+
}) {
118130
return _NavigationBarButton(icon: icon,
131+
label: label,
119132
selected: tabNotifier.value == tab,
120133
onPressed: () {
121134
tabNotifier.value = tab;
122-
},
123-
label: label);
135+
});
124136
}
125137

126138
@override
@@ -130,39 +142,54 @@ class _BottomNavBar extends StatelessWidget {
130142

131143
// TODO(a11y): add tooltips for these buttons
132144
final navigationBarButtons = [
133-
_button(_HomePageTab.inbox, ZulipIcons.inbox,
134-
zulipLocalizations.inboxPageTitle),
135-
_NavigationBarButton( icon: ZulipIcons.message_feed,
145+
_button(tab: _HomePageTab.inbox,
146+
icon: ZulipIcons.inbox,
147+
label: zulipLocalizations.inboxPageTitle),
148+
_NavigationBarButton(
149+
icon: ZulipIcons.message_feed,
150+
label: zulipLocalizations.combinedFeedPageTitle,
136151
selected: false,
137152
onPressed: () => Navigator.push(context,
138153
MessageListPage.buildRoute(context: context,
139-
narrow: const CombinedFeedNarrow())),
140-
label: zulipLocalizations.combinedFeedPageTitle),
141-
_button(_HomePageTab.channels, ZulipIcons.hash_italic,
142-
zulipLocalizations.channelsPageTitle),
154+
narrow: const CombinedFeedNarrow()))),
155+
_button(tab: _HomePageTab.channels,
156+
icon: ZulipIcons.hash_italic,
157+
label: zulipLocalizations.channelsPageTitle),
143158
// TODO(#1094): Users
144-
_button(_HomePageTab.directMessages, ZulipIcons.two_person,
145-
zulipLocalizations.recentDmConversationsPageTitle),
146-
_NavigationBarButton( icon: ZulipIcons.menu,
159+
_button(tab: _HomePageTab.directMessages,
160+
icon: ZulipIcons.two_person,
161+
label: zulipLocalizations.recentDmConversationsPageTitle),
162+
_NavigationBarButton(
163+
icon: ZulipIcons.menu,
164+
label: zulipLocalizations.navBarMenuLabel,
147165
selected: false,
148-
onPressed: () => _showMainMenu(context, tabNotifier: tabNotifier),
149-
label: zulipLocalizations.navBarMenuLabel),
166+
onPressed: () => _showMainMenu(context, tabNotifier: tabNotifier)),
150167
];
151168

152-
return DecoratedBox(
169+
Widget result = DecoratedBox(
153170
decoration: BoxDecoration(
154171
border: Border(top: BorderSide(color: designVariables.borderBar)),
155172
color: designVariables.bgBotBar),
156-
child: SafeArea(
157-
child: ConstrainedBox(
158-
// TODO(design): determine a suitable max width for bottom nav bar
159-
constraints: const BoxConstraints(maxWidth: 600, minHeight: 48),
160-
child: Row(
161-
crossAxisAlignment: CrossAxisAlignment.start,
162-
children: [
163-
for (final navigationBarButton in navigationBarButtons)
164-
Expanded(child: navigationBarButton),
165-
]))));
173+
child: IntrinsicHeight(
174+
child: SafeArea(
175+
child: Center(
176+
child: ConstrainedBox(
177+
// TODO(design): determine a suitable max width for bottom nav bar
178+
constraints: const BoxConstraints(maxWidth: 600, minHeight: 48),
179+
child: Row(
180+
crossAxisAlignment: CrossAxisAlignment.start,
181+
children: [
182+
for (final navigationBarButton in navigationBarButtons)
183+
Expanded(child: navigationBarButton),
184+
]))))));
185+
186+
result = Semantics(
187+
container: true,
188+
explicitChildNodes: true,
189+
role: SemanticsRole.tabBar,
190+
child: result);
191+
192+
return result;
166193
}
167194
}
168195

@@ -262,7 +289,7 @@ class _NavigationBarButton extends StatelessWidget {
262289
final designVariables = DesignVariables.of(context);
263290
final color = selected ? designVariables.iconSelected : designVariables.icon;
264291

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

0 commit comments

Comments
 (0)