From 07695e41be54ddf887420a60fd90d938a2c12476 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Fri, 24 Oct 2025 16:38:55 +0530 Subject: [PATCH] home: Add label to bottom navbar icons Fixes #1857. Fixes #1960. --- assets/l10n/app_en.arb | 4 + lib/generated/l10n/zulip_localizations.dart | 6 ++ .../l10n/zulip_localizations_ar.dart | 3 + .../l10n/zulip_localizations_de.dart | 3 + .../l10n/zulip_localizations_el.dart | 3 + .../l10n/zulip_localizations_en.dart | 3 + .../l10n/zulip_localizations_es.dart | 3 + .../l10n/zulip_localizations_fr.dart | 3 + .../l10n/zulip_localizations_he.dart | 3 + .../l10n/zulip_localizations_hu.dart | 3 + .../l10n/zulip_localizations_it.dart | 3 + .../l10n/zulip_localizations_ja.dart | 3 + .../l10n/zulip_localizations_nb.dart | 3 + .../l10n/zulip_localizations_pl.dart | 3 + .../l10n/zulip_localizations_ru.dart | 3 + .../l10n/zulip_localizations_sk.dart | 3 + .../l10n/zulip_localizations_sl.dart | 3 + .../l10n/zulip_localizations_uk.dart | 3 + .../l10n/zulip_localizations_zh.dart | 3 + lib/widgets/home.dart | 80 ++++++++++++------- test/widgets/home_test.dart | 34 ++++++++ .../widgets/recent_dm_conversations_test.dart | 2 +- 22 files changed, 146 insertions(+), 31 deletions(-) diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index b0b3523746..2ce121833f 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -1255,6 +1255,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 e76f70ca2b..6a72121963 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 8bbaefedb2..4326c3d34d 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -1047,6 +1047,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 2e5d76a19d..03a2040aaa 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -1072,6 +1072,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 546d67c58a..1f4be1a0a9 100644 --- a/lib/generated/l10n/zulip_localizations_el.dart +++ b/lib/generated/l10n/zulip_localizations_el.dart @@ -1047,6 +1047,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 f7797e261b..376affd764 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -1047,6 +1047,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 c9bb85e3f4..4601715713 100644 --- a/lib/generated/l10n/zulip_localizations_es.dart +++ b/lib/generated/l10n/zulip_localizations_es.dart @@ -1047,6 +1047,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 a56381bc5f..db676c768e 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -1063,6 +1063,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 6d58801b0e..4515718fcf 100644 --- a/lib/generated/l10n/zulip_localizations_he.dart +++ b/lib/generated/l10n/zulip_localizations_he.dart @@ -1047,6 +1047,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 942d73b6de..de960ee201 100644 --- a/lib/generated/l10n/zulip_localizations_hu.dart +++ b/lib/generated/l10n/zulip_localizations_hu.dart @@ -1047,6 +1047,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 eb0bea54f2..b5e50c1de4 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -1066,6 +1066,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 db0e7a0780..9f4d4891f3 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -1027,6 +1027,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 60b9739d88..79a28dd135 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -1047,6 +1047,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 bf31ba7d86..e9bf90298c 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -1063,6 +1063,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 3673e97924..2750a90372 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -1074,6 +1074,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 a927a4d27d..9c38955d48 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -1049,6 +1049,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 d58235e92e..b166daa47e 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -1085,6 +1085,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 e83f72029d..7ba1cc56ff 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -1064,6 +1064,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 267df9335e..98b675602a 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -1047,6 +1047,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..de9982b388 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -86,6 +86,8 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { + final zulipLocalizations = ZulipLocalizations.of(context); + const pageBodies = [ (_HomePageTab.inbox, InboxPageBody()), (_HomePageTab.channels, SubscriptionListPageBody()), @@ -93,28 +95,34 @@ class _HomePageState extends State { (_HomePageTab.directMessages, RecentDmConversationsPageBody()), ]; - _NavigationBarButton button(_HomePageTab tab, IconData icon) { + _NavigationBarButton button(_HomePageTab tab, IconData icon, String label) { return _NavigationBarButton(icon: icon, selected: _tab.value == tab, onPressed: () { _tab.value = tab; - }); + }, + label: label); } // TODO(a11y): add tooltips for these buttons final navigationBarButtons = [ - button(_HomePageTab.inbox, ZulipIcons.inbox), + button(_HomePageTab.inbox, ZulipIcons.inbox, + zulipLocalizations.inboxPageTitle), _NavigationBarButton( icon: ZulipIcons.message_feed, selected: false, onPressed: () => Navigator.push(context, MessageListPage.buildRoute(context: context, - narrow: const CombinedFeedNarrow()))), - button(_HomePageTab.channels, ZulipIcons.hash_italic), + narrow: const CombinedFeedNarrow())), + label: zulipLocalizations.combinedFeedPageTitle), + button(_HomePageTab.channels, ZulipIcons.hash_italic, + zulipLocalizations.channelsPageTitle), // TODO(#1094): Users - button(_HomePageTab.directMessages, ZulipIcons.two_person), + button(_HomePageTab.directMessages, ZulipIcons.two_person, + zulipLocalizations.recentDmConversationsPageTitle), _NavigationBarButton( icon: ZulipIcons.menu, selected: false, - onPressed: () => _showMainMenu(context, tabNotifier: _tab)), + onPressed: () => _showMainMenu(context, tabNotifier: _tab), + label: zulipLocalizations.navBarMenuLabel), ]; final designVariables = DesignVariables.of(context); @@ -134,17 +142,15 @@ class _HomePageState extends State { 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), - ]))))))); + 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), + ]))))); } } @@ -231,35 +237,49 @@ 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 iconColor = WidgetStateColor.fromMap({ - WidgetState.pressed: designVariables.iconSelected, - ~WidgetState.pressed: selected ? designVariables.iconSelected - : designVariables.icon, - }); + final color = selected ? designVariables.iconSelected : designVariables.icon; return 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( + semanticsLabel: '$label tab', + label, + style: TextStyle(fontSize: 12, color: color, height: 12 / 12), + textAlign: TextAlign.center, + textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.5))), + ]))))); } } diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 1ee0a0ae8e..88a964a8ee 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -82,6 +82,20 @@ void main () { } group('bottom nav navigation', () { + final findBottomNavDecoratedBox = find.byWidgetPredicate((widget) { + if (widget is! DecoratedBox) return false; + + final decoration = widget.decoration; + if (decoration is! BoxDecoration) return false; + + final expectedBorderTop = BorderSide(color: Colors.black.withValues(alpha: 0.2)); + return decoration.border == Border(top: expectedBorderTop); + }); + + // Finds a widget within the bottom navbar's decorated box subtree. + Finder findInBottomNav(Finder finder) => + find.descendant(of: findBottomNavDecoratedBox, matching: finder); + testWidgets('preserve states when switching between views', (tester) async { await prepare(tester); await store.addUser(eg.otherUser); @@ -131,6 +145,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(); }