From e159ca1420e77b520ef1c5d8342e7bd2baacf8a0 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:49:44 +0100
Subject: [PATCH] fix: name the rail's Mail entry Mail for screen readers
instead of its unread count
---
.../navigation-rail-badge-name.test.tsx | 40 +++++++++++++++++++
components/layout/navigation-rail.tsx | 5 ++-
2 files changed, 43 insertions(+), 2 deletions(-)
create mode 100644 components/layout/__tests__/navigation-rail-badge-name.test.tsx
diff --git a/components/layout/__tests__/navigation-rail-badge-name.test.tsx b/components/layout/__tests__/navigation-rail-badge-name.test.tsx
new file mode 100644
index 000000000..4a3a18fd5
--- /dev/null
+++ b/components/layout/__tests__/navigation-rail-badge-name.test.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+// The Mail entry shows the Inbox's unread count as a badge. On the collapsed
+// rail the entry has no visible text, so the badge digits used to become the
+// link's whole accessible name ("2" instead of "Mail").
+
+vi.mock('../account-switcher', () => ({ AccountSwitcher: () => null }));
+vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
+vi.mock('@/components/keyboard-shortcuts-modal', () => ({ KeyboardShortcutsModal: () => null }));
+vi.mock('@/components/search/search-box', () => ({ SearchBox: () => null }));
+vi.mock('@/hooks/use-config', () => ({ useConfig: () => ({ appName: 'Mail' }) }));
+vi.mock('@/hooks/use-stalwart-admin', () => ({ useStalwartAdmin: () => false }));
+vi.mock('@/i18n/navigation', () => ({
+ useRouter: () => ({ push: vi.fn(), back: vi.fn(), replace: vi.fn() }),
+ usePathname: () => '/files',
+ Link: ({ children, href, ...rest }: { children: React.ReactNode; href: string }) => {children},
+}));
+
+const { NavigationRail } = await import('../navigation-rail');
+const { useUpdateStore } = await import('@/stores/update-store');
+const { useEmailStore } = await import('@/stores/email-store');
+
+beforeEach(() => {
+ useUpdateStore.setState({ startPolling: vi.fn() } as never);
+ useEmailStore.setState({ mailboxes: [{ id: 'a', role: 'inbox', unreadEmails: 2 }] } as never);
+});
+
+describe.each([
+ ['collapsed rail', { orientation: 'vertical', collapsed: true }],
+ ['expanded rail', { orientation: 'vertical', collapsed: false }],
+ ['bottom bar', { orientation: 'horizontal', collapsed: false }],
+] as const)('%s', (_name, props) => {
+ it('names the Mail entry by its label, not by its unread count', () => {
+ render();
+ expect(screen.getAllByRole('link', { name: 'mail' }).length).toBeGreaterThan(0);
+ expect(screen.queryByRole('link', { name: '2' })).toBeNull();
+ });
+});
diff --git a/components/layout/navigation-rail.tsx b/components/layout/navigation-rail.tsx
index a661d5c5e..e31d65ec5 100644
--- a/components/layout/navigation-rail.tsx
+++ b/components/layout/navigation-rail.tsx
@@ -382,7 +382,7 @@ export function NavigationRail({
)}>
{item.badge != null && item.badge > 0 && (
-
+
{item.badge > 99 ? "99+" : item.badge}
)}
@@ -588,13 +588,14 @@ export function NavigationRail({
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
aria-current={isActive ? "page" : undefined}
+ aria-label={t(item.labelKey)}
title={collapsed ? t(item.labelKey) : undefined}
style={collapsed ? undefined : { paddingBlock: 'var(--density-sidebar-py)' }}
>
{!collapsed && {t(item.labelKey)}}
{item.badge != null && item.badge > 0 && (
-