fix(frontend): make the panel usable with screen readers (#6907)

* fix(frontend): make the panel usable with screen readers

* fix(frontend): keep mobile tab names and the inbound expiry hint

Review of #6907 found two gaps in the screen-reader pass:

- On phone-width screens catTabLabel renders only the icon and names the
  tab by putting aria-label on it, but callers now pass that icon with
  aria-hidden for the desktop label, so every Settings, Xray and Host
  category tab lost its name under TalkBack. catTabLabel now clears
  aria-hidden on the icon it uses as the name.
- DateTimePicker forwarded id and aria-label but not aria-describedby,
  so the inbound expiry hint ("leave blank to never expire") was never
  attached. It now forwards it on both the Gregorian and Jalali input.
This commit is contained in:
Alireza Ahmadi
2026-10-23 18:29:47 +02:00
committed by GitHub
parent 775b2b0b26
commit e2184777a0
156 changed files with 2373 additions and 863 deletions
+43 -21
View File
@@ -100,7 +100,7 @@ function DonateButton({ ariaLabel }: { ariaLabel: string }) {
aria-label={ariaLabel}
title={ariaLabel}
>
<HeartOutlined />
<HeartOutlined aria-hidden />
</a>
);
}
@@ -115,7 +115,7 @@ function DocsButton({ ariaLabel }: { ariaLabel: string }) {
aria-label={ariaLabel}
title={ariaLabel}
>
<ReadOutlined />
<ReadOutlined aria-hidden />
</a>
);
}
@@ -132,7 +132,7 @@ function VersionBadge({ version, collapsed }: { version: string; collapsed?: boo
aria-label={`GitHub ${label}`}
title={label}
>
<GithubOutlined />
<GithubOutlined aria-hidden />
{!collapsed && <span className="sider-version-text">{label}</span>}
</a>
);
@@ -151,7 +151,13 @@ function ThemeCycleButton({
onCycle: () => void;
ariaLabel: string;
}) {
const icon = !isDark ? <SunOutlined /> : !isUltra ? <MoonOutlined /> : <MoonFilled />;
const icon = !isDark ? (
<SunOutlined aria-hidden />
) : !isUltra ? (
<MoonOutlined aria-hidden />
) : (
<MoonFilled aria-hidden />
);
return (
<button
id={id}
@@ -248,42 +254,46 @@ export default function AppSidebar() {
const children: NonNullable<MenuProps['items']> = [
{
key: '/settings#general',
icon: <SettingOutlined />,
icon: <SettingOutlined aria-hidden />,
label: t('pages.settings.panelSettings'),
},
{
key: '/settings#security',
icon: <SafetyOutlined />,
icon: <SafetyOutlined aria-hidden />,
label: t('pages.settings.securitySettings'),
},
{
key: '/settings#telegram',
icon: <MessageOutlined />,
icon: <MessageOutlined aria-hidden />,
label: t('pages.settings.TGBotSettings'),
},
{ key: '/settings#email', icon: <MailOutlined />, label: t('pages.settings.emailSettings') },
{
key: '/settings#email',
icon: <MailOutlined aria-hidden />,
label: t('pages.settings.emailSettings'),
},
{
key: '/settings#discord',
icon: <DiscordOutlined />,
icon: <DiscordOutlined aria-hidden />,
label: t('pages.settings.discordSettings'),
},
{
key: '/settings#subscription',
icon: <CloudServerOutlined />,
icon: <CloudServerOutlined aria-hidden />,
label: t('pages.settings.subSettings'),
},
];
if (showSubFormats) {
children.push({
key: '/settings#subscription-formats',
icon: <CodeOutlined />,
icon: <CodeOutlined aria-hidden />,
label: t('menu.subFormats'),
});
}
if (showSubBalancers) {
children.push({
key: '/settings#subscription-balancers',
icon: <ApartmentOutlined />,
icon: <ApartmentOutlined aria-hidden />,
label: t('pages.settings.subBalancers.menu'),
});
}
@@ -292,10 +302,22 @@ export default function AppSidebar() {
const xrayChildren = useMemo<NonNullable<MenuProps['items']>>(
() => [
{ key: '/xray#basic', icon: <SettingOutlined />, label: t('pages.xray.basicTemplate') },
{ key: '/xray#balancer', icon: <ClusterOutlined />, label: t('pages.xray.Balancers') },
{ key: '/xray#dns', icon: <DatabaseOutlined />, label: 'DNS' },
{ key: '/xray#advanced', icon: <CodeOutlined />, label: t('pages.xray.advancedTemplate') },
{
key: '/xray#basic',
icon: <SettingOutlined aria-hidden />,
label: t('pages.xray.basicTemplate'),
},
{
key: '/xray#balancer',
icon: <ClusterOutlined aria-hidden />,
label: t('pages.xray.Balancers'),
},
{ key: '/xray#dns', icon: <DatabaseOutlined aria-hidden />, label: 'DNS' },
{
key: '/xray#advanced',
icon: <CodeOutlined aria-hidden />,
label: t('pages.xray.advancedTemplate'),
},
],
[t],
);
@@ -394,7 +416,7 @@ export default function AppSidebar() {
title={t(pinned ? 'menu.unpinSidebar' : 'menu.pinSidebar')}
onClick={togglePinned}
>
{pinned ? <PushpinFilled /> : <PushpinOutlined />}
{pinned ? <PushpinFilled aria-hidden /> : <PushpinOutlined aria-hidden />}
</button>
<DocsButton ariaLabel={t('menu.docs') || 'Documentation'} />
<DonateButton ariaLabel={t('menu.donate') || 'Donate'} />
@@ -421,7 +443,7 @@ export default function AppSidebar() {
aria-label={t('commandPalette.title') || 'Command Palette (Ctrl + K)'}
>
<span className="sidebar-command-left">
<SearchOutlined className="sidebar-command-icon" />
<SearchOutlined aria-hidden className="sidebar-command-icon" />
<span className="sidebar-command-text">
{t('commandPalette.search') || 'Search...'}
</span>
@@ -488,7 +510,7 @@ export default function AppSidebar() {
aria-label={t('close')}
onClick={() => setDrawerOpen(false)}
>
<CloseOutlined />
<CloseOutlined aria-hidden />
</button>
</div>
</div>
@@ -503,7 +525,7 @@ export default function AppSidebar() {
style={{ margin: '8px 12px 4px', width: 'calc(100% - 24px)' }}
>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<SearchOutlined className="sidebar-command-icon" />
<SearchOutlined aria-hidden className="sidebar-command-icon" />
<span>{t('commandPalette.search') || 'Search...'}</span>
</span>
<span className="sidebar-command-kbd">
@@ -547,7 +569,7 @@ export default function AppSidebar() {
aria-label={t('menu.openMenu')}
onClick={() => setDrawerOpen(true)}
>
<MenuOutlined />
<MenuOutlined aria-hidden />
</button>
)}
</div>