fix: name the rail's Mail entry Mail for screen readers instead of its unread count

This commit is contained in:
Linus Rath
2026-10-26 16:49:44 +01:00
parent bec17e6a7b
commit e159ca1420
2 changed files with 43 additions and 2 deletions
@@ -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 }) => <a href={href} {...rest}>{children}</a>,
}));
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(<NavigationRail {...props} />);
expect(screen.getAllByRole('link', { name: 'mail' }).length).toBeGreaterThan(0);
expect(screen.queryByRole('link', { name: '2' })).toBeNull();
});
});
+3 -2
View File
@@ -382,7 +382,7 @@ export function NavigationRail({
)}>
<Icon className="w-5 h-5" />
{item.badge != null && item.badge > 0 && (
<span className="absolute -top-1.5 -right-2.5 flex items-center justify-center min-w-[16px] h-4 text-[10px] font-bold rounded-full bg-red-500 text-white px-1">
<span aria-hidden="true" className="absolute -top-1.5 -right-2.5 flex items-center justify-center min-w-[16px] h-4 text-[10px] font-bold rounded-full bg-red-500 text-white px-1">
{item.badge > 99 ? "99+" : item.badge}
</span>
)}
@@ -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)' }}
>
<Icon className={cn("w-[18px] h-[18px] flex-shrink-0", isActive && "text-primary")} />
{!collapsed && <span className="truncate">{t(item.labelKey)}</span>}
{item.badge != null && item.badge > 0 && (
<span className={cn(
<span aria-hidden="true" className={cn(
"absolute flex items-center justify-center min-w-[16px] h-4 text-[10px] font-bold rounded-full bg-red-500 text-white px-1",
collapsed ? "-top-0.5 -right-0.5" : "right-1.5"
)}>