From ab36ab6557ea69281c4b52191fda91efdc6046d3 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Wed, 19 Nov 2025 12:19:34 -0800 Subject: [PATCH] home: Tweak main-menu buttons to follow Figma The buttons were 48px instead of 44px tall, and the label's line height was 26px instead of 23px. --- lib/widgets/home.dart | 66 ++++++++++++++++++++++--------------- test/widgets/home_test.dart | 19 +++++++++++ 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 3c0a58d6ee..5abfa9e632 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -486,8 +486,13 @@ class _MainMenuHeaderState extends State<_MainMenuHeader> { } } -abstract class _MenuButton extends StatelessWidget { - const _MenuButton(); +/// A button in the main menu. +/// +/// See Figma: +/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2037-243759&m=dev +@visibleForTesting +abstract class MenuButton extends StatelessWidget { + const MenuButton({super.key}); String label(ZulipLocalizations zulipLocalizations); @@ -524,11 +529,20 @@ abstract class _MenuButton extends StatelessWidget { final designVariables = DesignVariables.of(context); final zulipLocalizations = ZulipLocalizations.of(context); + // Make [TextButton] set 44 instead of 48 for the height. + final visualDensity = VisualDensity(vertical: -1); + // A value that [TextButton] adds to some of its layout parameters; + // we can cancel out those adjustments by subtracting it. + final densityVerticalAdjustment = visualDensity.baseSizeAdjustment.dy; + final borderSideSelected = BorderSide(width: 1, strokeAlign: BorderSide.strokeAlignOutside, color: designVariables.borderMenuButtonSelected); final buttonStyle = TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 8), + // Make the button 44px instead of 48px tall, to match the Figma. + visualDensity: visualDensity, + padding: EdgeInsets.symmetric( + vertical: 10 - densityVerticalAdjustment, horizontal: 8), foregroundColor: designVariables.labelMenuButton, // This has a default behavior of affecting the background color of the // button for states including "hovered", "focused" and "pressed". @@ -554,26 +568,24 @@ abstract class _MenuButton extends StatelessWidget { return AnimatedScaleOnTap( duration: const Duration(milliseconds: 100), scaleEnd: 0.95, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 44), - child: TextButton( - onPressed: () => _handlePress(context), - style: buttonStyle, - child: Row(spacing: 8, children: [ - SizedBox.square(dimension: _iconSize, - child: buildLeading(context)), - Expanded(child: Text(label(zulipLocalizations), - // TODO(design): determine if we prefer to wrap - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 19, height: 26 / 19) - .merge(weightVariableTextStyle(context, wght: selected ? 600 : 400)))), - ?trailing, - ])))); + child: TextButton( + onPressed: () => _handlePress(context), + style: buttonStyle, + child: Row(spacing: 8, children: [ + SizedBox.square(dimension: _iconSize, + child: buildLeading(context)), + Expanded(child: Text(label(zulipLocalizations), + // TODO(design): determine if we prefer to wrap + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 19, height: 23 / 19) + .merge(weightVariableTextStyle(context, wght: selected ? 600 : 400)))), + ?trailing, + ]))); } } /// A menu button controlling the selected [_HomePageTab] on the bottom nav bar. -abstract class _NavigationBarMenuButton extends _MenuButton { +abstract class _NavigationBarMenuButton extends MenuButton { const _NavigationBarMenuButton({required this.tabNotifier}); final ValueNotifier<_HomePageTab> tabNotifier; @@ -589,7 +601,7 @@ abstract class _NavigationBarMenuButton extends _MenuButton { } } -class _SearchButton extends _MenuButton { +class _SearchButton extends MenuButton { const _SearchButton(); @override @@ -634,7 +646,7 @@ class _InboxButton extends _NavigationBarMenuButton { _HomePageTab get navigationTarget => _HomePageTab.inbox; } -class _MentionsButton extends _MenuButton { +class _MentionsButton extends MenuButton { const _MentionsButton(); @override @@ -664,7 +676,7 @@ class _MentionsButton extends _MenuButton { } } -class _StarredMessagesButton extends _MenuButton { +class _StarredMessagesButton extends MenuButton { const _StarredMessagesButton(); @override @@ -682,7 +694,7 @@ class _StarredMessagesButton extends _MenuButton { } } -class _CombinedFeedButton extends _MenuButton { +class _CombinedFeedButton extends MenuButton { const _CombinedFeedButton(); @override @@ -742,7 +754,7 @@ class _DirectMessagesButton extends _NavigationBarMenuButton { _HomePageTab get navigationTarget => _HomePageTab.directMessages; } -class _MyProfileButton extends _MenuButton { +class _MyProfileButton extends MenuButton { const _MyProfileButton(); @override @@ -753,7 +765,7 @@ class _MyProfileButton extends _MenuButton { final store = PerAccountStoreWidget.of(context); return Avatar( userId: store.selfUserId, - size: _MenuButton._iconSize, + size: MenuButton._iconSize, borderRadius: 4, showPresence: false, ); @@ -772,7 +784,7 @@ class _MyProfileButton extends _MenuButton { } } -class _SettingsButton extends _MenuButton { +class _SettingsButton extends MenuButton { const _SettingsButton(); @override @@ -789,7 +801,7 @@ class _SettingsButton extends _MenuButton { } } -class _AboutZulipButton extends _MenuButton { +class _AboutZulipButton extends MenuButton { const _AboutZulipButton(); @override diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 84475108ea..198ed55764 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -256,6 +256,25 @@ void main () { .isSameColorAs(designVariables.icon); } + testWidgets('buttons are 44px tall', (tester) async { + prepareBoringImageHttpClient(); + await prepare(tester); + + await tapOpenMenuAndAwait(tester); + checkIconSelected(tester, inboxMenuIconFinder); + checkIconNotSelected(tester, channelsMenuIconFinder); + + final inboxElement = tester.element( + find.ancestor(of: inboxMenuIconFinder, matching: find.bySubtype())); + check((inboxElement.renderObject as RenderBox)).size.height.equals(44); + + final channelsElement = tester.element( + find.ancestor(of: inboxMenuIconFinder, matching: find.bySubtype())); + check((channelsElement.renderObject as RenderBox)).size.height.equals(44); + + debugNetworkImageHttpClientProvider = null; + }); + testWidgets('navigation states reflect on navigation bar menu buttons', (tester) async { prepareBoringImageHttpClient(); await prepare(tester);