diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 45c7e6ca94..38b49c2edd 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -1261,6 +1261,10 @@ "@wildcardMentionTopicDescription": { "description": "Description for \"@topic\" wildcard-mention autocomplete options when writing a channel message." }, + "navBarMenuLabel": "Menu", + "@navBarMenuLabel": { + "description": "Label for the Menu button on the bottom navigation bar." + }, "messageIsEditedLabel": "EDITED", "@messageIsEditedLabel": { "description": "Label for an edited message. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)" diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index ce46ae6e7d..fe79921c07 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -1833,6 +1833,12 @@ abstract class ZulipLocalizations { /// **'Notify topic'** String get wildcardMentionTopicDescription; + /// Label for the Menu button on the bottom navigation bar. + /// + /// In en, this message translates to: + /// **'Menu'** + String get navBarMenuLabel; + /// Label for an edited message. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.) /// /// In en, this message translates to: diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart index d4c35968bf..5e69a181da 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -1054,6 +1054,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'إخطار الموضوع'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'EDITED'; diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index 0a43ac50b5..241e886e55 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -1073,6 +1073,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Thema benachrichtigen'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'BEARBEITET'; diff --git a/lib/generated/l10n/zulip_localizations_el.dart b/lib/generated/l10n/zulip_localizations_el.dart index c7725a64e2..ce72dc77bf 100644 --- a/lib/generated/l10n/zulip_localizations_el.dart +++ b/lib/generated/l10n/zulip_localizations_el.dart @@ -1054,6 +1054,9 @@ class ZulipLocalizationsEl extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notify topic'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'EDITED'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 334df16239..6d10362555 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -1054,6 +1054,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notify topic'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'EDITED'; diff --git a/lib/generated/l10n/zulip_localizations_es.dart b/lib/generated/l10n/zulip_localizations_es.dart index f45d3db383..80098d557f 100644 --- a/lib/generated/l10n/zulip_localizations_es.dart +++ b/lib/generated/l10n/zulip_localizations_es.dart @@ -1054,6 +1054,9 @@ class ZulipLocalizationsEs extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notify topic'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'EDITED'; diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index 0d64c3b679..adb41177b2 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -1070,6 +1070,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notify topic'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'EDITED'; diff --git a/lib/generated/l10n/zulip_localizations_he.dart b/lib/generated/l10n/zulip_localizations_he.dart index 5e1609ccba..e59309be3e 100644 --- a/lib/generated/l10n/zulip_localizations_he.dart +++ b/lib/generated/l10n/zulip_localizations_he.dart @@ -1054,6 +1054,9 @@ class ZulipLocalizationsHe extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notify topic'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'EDITED'; diff --git a/lib/generated/l10n/zulip_localizations_hu.dart b/lib/generated/l10n/zulip_localizations_hu.dart index aacb5d9d8d..6a3c11465c 100644 --- a/lib/generated/l10n/zulip_localizations_hu.dart +++ b/lib/generated/l10n/zulip_localizations_hu.dart @@ -1054,6 +1054,9 @@ class ZulipLocalizationsHu extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notify topic'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'EDITED'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index 4fc1ba6e42..cd27a1daa7 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -1067,6 +1067,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notifica argomento'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'MODIFICATO'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index e779208dcf..d11aedf19f 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -1029,6 +1029,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'トピック参加者に通知'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => '編集済み'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index 3d9b7990e7..162ca9e677 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -1054,6 +1054,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notify topic'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'EDITED'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index ea8d3b4fd8..40682171b2 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -1070,6 +1070,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Powiadom w wątku'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'ZMIENIONO'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 17f82a0d26..04f8903b91 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -1083,6 +1083,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Оповестить тему'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'ИЗМЕНЕНО'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 092070d0f3..e2dc542fc5 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -1056,6 +1056,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notify topic'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'UPRAVENÉ'; diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index f18f440748..6f9d8343ca 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -1091,6 +1091,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Obvesti udeležence teme'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'UREJENO'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index 3810960b63..84df26a42e 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -1071,6 +1071,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Повідомити канал'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'РЕДАГОВАНО'; diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index 5db806ac0e..b08c25004e 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -1054,6 +1054,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get wildcardMentionTopicDescription => 'Notify topic'; + @override + String get navBarMenuLabel => 'Menu'; + @override String get messageIsEditedLabel => 'EDITED'; diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 62c09c0857..ecb156eb06 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import '../generated/l10n/zulip_localizations.dart'; import '../model/narrow.dart'; @@ -47,6 +48,9 @@ class HomePage extends StatefulWidget { HomePage.buildRoute(accountId: accountId))); } + static String contentSemanticsIdentifier = 'home-page-content'; + static String titleSemanticsIdentifier = 'home-page-title'; + @override State createState() => _HomePageState(); } @@ -93,58 +97,98 @@ class _HomePageState extends State { (_HomePageTab.directMessages, RecentDmConversationsPageBody()), ]; - _NavigationBarButton button(_HomePageTab tab, IconData icon) { - return _NavigationBarButton(icon: icon, - selected: _tab.value == tab, - onPressed: () { - _tab.value = tab; - }); - } + return Scaffold( + appBar: ZulipAppBar(titleSpacing: 16, + title: Semantics( + identifier: HomePage.titleSemanticsIdentifier, + namesRoute: true, + child: Text(_currentTabTitle))), + body: Semantics( + role: SemanticsRole.tabPanel, + identifier: HomePage.contentSemanticsIdentifier, + container: true, + explicitChildNodes: true, + child: Stack( + children: [ + for (final (tab, body) in pageBodies) + Offstage(offstage: tab != _tab.value, child: body), + ]), + ), + bottomNavigationBar: _BottomNavBar(tabNotifier: _tab)); + } +} + +class _BottomNavBar extends StatelessWidget { + const _BottomNavBar({required this.tabNotifier}); + + final ValueNotifier<_HomePageTab> tabNotifier; + + _NavigationBarButton _button({ + required _HomePageTab tab, + required IconData icon, + required String label}) + { + return _NavigationBarButton(icon: icon, + label: label, + selected: tabNotifier.value == tab, + onPressed: () { + tabNotifier.value = tab; + }); + } + + @override + Widget build(BuildContext context) { + final designVariables = DesignVariables.of(context); + final zulipLocalizations = ZulipLocalizations.of(context); // TODO(a11y): add tooltips for these buttons final navigationBarButtons = [ - button(_HomePageTab.inbox, ZulipIcons.inbox), - _NavigationBarButton( icon: ZulipIcons.message_feed, + _button(tab: _HomePageTab.inbox, + icon: ZulipIcons.inbox, + label: zulipLocalizations.inboxPageTitle), + _NavigationBarButton(icon: ZulipIcons.message_feed, + label: zulipLocalizations.combinedFeedPageTitle, selected: false, onPressed: () => Navigator.push(context, MessageListPage.buildRoute(context: context, narrow: const CombinedFeedNarrow()))), - button(_HomePageTab.channels, ZulipIcons.hash_italic), + _button(tab: _HomePageTab.channels, + icon: ZulipIcons.hash_italic, + label: zulipLocalizations.channelsPageTitle), // TODO(#1094): Users - button(_HomePageTab.directMessages, ZulipIcons.two_person), - _NavigationBarButton( icon: ZulipIcons.menu, + _button(tab: _HomePageTab.directMessages, + icon: ZulipIcons.two_person, + label: zulipLocalizations.recentDmConversationsPageTitle), + _NavigationBarButton(icon: ZulipIcons.menu, + label: zulipLocalizations.navBarMenuLabel, selected: false, - onPressed: () => _showMainMenu(context, tabNotifier: _tab)), + onPressed: () => _showMainMenu(context, tabNotifier: tabNotifier)), ]; - final designVariables = DesignVariables.of(context); - return Scaffold( - appBar: ZulipAppBar(titleSpacing: 16, - title: Text(_currentTabTitle)), - body: Stack( - children: [ - for (final (tab, body) in pageBodies) - // TODO(#535): Decide if we find it helpful to use something like - // [SemanticsProperties.namesRoute] to structure this UI better - // for screen-reader software. - Offstage(offstage: tab != _tab.value, child: body), - ]), - bottomNavigationBar: DecoratedBox( - decoration: BoxDecoration( - border: Border(top: BorderSide(color: designVariables.borderBar)), - color: designVariables.bgBotBar), - child: SafeArea( - child: SizedBox(height: 48, - child: Center( - child: ConstrainedBox( - // TODO(design): determine a suitable max width for bottom nav bar - constraints: const BoxConstraints(maxWidth: 600), - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - for (final navigationBarButton in navigationBarButtons) - Expanded(child: navigationBarButton), - ]))))))); + Widget result = DecoratedBox( + decoration: BoxDecoration( + border: Border(top: BorderSide(color: designVariables.borderBar)), + color: designVariables.bgBotBar), + child: SafeArea( + child: Center( + heightFactor: 1, + child: ConstrainedBox( + // TODO(design): determine a suitable max width for bottom nav bar + constraints: const BoxConstraints(maxWidth: 600, minHeight: 48), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final navigationBarButton in navigationBarButtons) + Expanded(child: navigationBarButton), + ]))))); + + result = Semantics( + container: true, + explicitChildNodes: true, + role: SemanticsRole.tabBar, + child: result); + + return result; } } @@ -231,35 +275,60 @@ class _NavigationBarButton extends StatelessWidget { required this.icon, required this.selected, required this.onPressed, + required this.label, }); final IconData icon; final bool selected; final void Function() onPressed; + final String label; @override Widget build(BuildContext context) { final designVariables = DesignVariables.of(context); + final color = selected ? designVariables.iconSelected : designVariables.icon; - final iconColor = WidgetStateColor.fromMap({ - WidgetState.pressed: designVariables.iconSelected, - ~WidgetState.pressed: selected ? designVariables.iconSelected - : designVariables.icon, - }); - - return AnimatedScaleOnTap( + Widget result = AnimatedScaleOnTap( scaleEnd: 0.875, duration: const Duration(milliseconds: 100), - child: IconButton( - icon: Icon(icon, size: 24), - onPressed: onPressed, - style: IconButton.styleFrom( + child: Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: BorderRadius.all(Radius.circular(4)), // TODO(#417): Disable splash effects for all buttons globally. splashFactory: NoSplash.splashFactory, highlightColor: designVariables.navigationButtonBg, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4))), - ).copyWith(foregroundColor: iconColor))); + onTap: onPressed, + child: Padding( + // (Added 3px horizontal padding not present in Figma, to make the + // text wrap before getting too close to the button's edge, which is + // visible on tap-down.) + padding: const EdgeInsets.fromLTRB(3, 6, 3, 3), + child: Column( + spacing: 3, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 24, color: color), + Flexible( + child: Text( + label, + style: TextStyle(fontSize: 12, color: color, height: 12 / 12), + textAlign: TextAlign.center, + textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.5))), + ]))))); + + result = MergeSemantics( + child: Semantics( + role: SemanticsRole.tab, + controlsNodes: { + HomePage.contentSemanticsIdentifier, + HomePage.titleSemanticsIdentifier, + }, + selected: selected, + onTap: onPressed, + child: result)); + + return result; } } diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 1ee0a0ae8e..239449c990 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:checks/checks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_checks/flutter_checks.dart'; @@ -82,6 +84,15 @@ void main () { } group('bottom nav navigation', () { + final findBottomNavSemantics = find.byWidgetPredicate((widget) { + if (widget is! Semantics) return false; + return widget.properties.role == SemanticsRole.tab; + }); + + // Finds a widget within the bottom navbar's semantics box subtree. + Finder findInBottomNav(Finder finder) => + find.descendant(of: findBottomNavSemantics, matching: finder); + testWidgets('preserve states when switching between views', (tester) async { await prepare(tester); await store.addUser(eg.otherUser); @@ -131,6 +142,26 @@ void main () { matching: find.text('Direct messages'))).findsOne(); }); + testWidgets("view switches when labels are tapped", (tester) async { + await prepare(tester); + + check(find.descendant( + of: find.byType(ZulipAppBar), + matching: find.text('Inbox'))).findsOne(); + + await tester.tap(findInBottomNav(find.text('Channels'))); + await tester.pump(); + check(find.descendant( + of: find.byType(ZulipAppBar), + matching: find.text('Channels'))).findsOne(); + + await tester.tap(findInBottomNav(find.text('Direct messages'))); + await tester.pump(); + check(find.descendant( + of: find.byType(ZulipAppBar), + matching: find.text('Direct messages'))).findsOne(); + }); + testWidgets('combined feed', (tester) async { await prepare(tester); pushedRoutes.clear(); diff --git a/test/widgets/recent_dm_conversations_test.dart b/test/widgets/recent_dm_conversations_test.dart index a558734119..b5fdd10217 100644 --- a/test/widgets/recent_dm_conversations_test.dart +++ b/test/widgets/recent_dm_conversations_test.dart @@ -59,7 +59,7 @@ Future setupPage(WidgetTester tester, { // Switch to direct messages tab. await tester.tap(find.descendant( - of: find.byType(Center), + of: find.byType(DecoratedBox), matching: find.byIcon(ZulipIcons.two_person))); await tester.pump(); }