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
@@ -262,9 +262,9 @@ export default function CommandPalette() {
title: c.email,
subtitle: `${trafficUsed} / ${trafficTotal}${c.comment ? ` · ${c.comment}` : ''}`,
icon: isOnline ? (
<CheckCircleFilled style={{ color: '#52c41a' }} />
<CheckCircleFilled aria-hidden style={{ color: '#52c41a' }} />
) : (
<CloseCircleFilled style={{ color: '#ff4d4f' }} />
<CloseCircleFilled aria-hidden style={{ color: '#ff4d4f' }} />
),
action: () => {
close();
@@ -274,7 +274,7 @@ export default function CommandPalette() {
c.subId && allSetting.subURI
? {
label: t('commandPalette.copySubscription'),
icon: <CopyOutlined />,
icon: <CopyOutlined aria-hidden />,
execute: (e) => {
e.stopPropagation();
copySubscription(c);
@@ -331,7 +331,7 @@ export default function CommandPalette() {
category: 'inbounds',
title: ib.remark || ib.tag || `Inbound #${ib.id}`,
subtitle: `Port ${ib.port || ''}`,
icon: <ImportOutlined style={{ color: '#1677ff' }} />,
icon: <ImportOutlined aria-hidden style={{ color: '#1677ff' }} />,
tag:
tags.length > 0 ? (
<div style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap' }}>{tags}</div>
@@ -348,7 +348,7 @@ export default function CommandPalette() {
path: '/',
title: t('menu.dashboard'),
keywords: ['overview', 'dashboard', 'cpu', 'ram', 'memory', 'traffic', 'speed'],
icon: <DashboardOutlined />,
icon: <DashboardOutlined aria-hidden />,
},
{
path: '/inbounds',
@@ -365,67 +365,67 @@ export default function CommandPalette() {
'hysteria',
'masque',
],
icon: <ImportOutlined />,
icon: <ImportOutlined aria-hidden />,
},
{
path: '/clients',
title: t('menu.clients'),
keywords: ['clients', 'users', 'sub', 'traffic', 'quota'],
icon: <TeamOutlined />,
icon: <TeamOutlined aria-hidden />,
},
{
path: '/groups',
title: t('menu.groups'),
keywords: ['groups', 'tags', 'batch'],
icon: <TagsOutlined />,
icon: <TagsOutlined aria-hidden />,
},
{
path: '/nodes',
title: t('menu.nodes'),
keywords: ['nodes', 'servers', 'cluster', 'remote nodes'],
icon: <ClusterOutlined />,
icon: <ClusterOutlined aria-hidden />,
},
{
path: '/hosts',
title: t('menu.hosts'),
keywords: ['hosts', 'sni', 'domains'],
icon: <GlobalOutlined />,
icon: <GlobalOutlined aria-hidden />,
},
{
path: '/outbound',
title: t('menu.outbounds'),
keywords: ['outbounds', 'freedom', 'blackhole', 'socks', 'http', 'warp', 'nord', 'pia'],
icon: <ExportOutlined />,
icon: <ExportOutlined aria-hidden />,
},
{
path: '/routing',
title: t('menu.routing'),
keywords: ['routing', 'rules', 'geoip', 'geosite', 'direct', 'block'],
icon: <SwapOutlined />,
icon: <SwapOutlined aria-hidden />,
},
{
path: '/settings',
title: t('menu.settings'),
keywords: ['settings', 'config', 'port', 'password', 'ssl', 'telegram'],
icon: <SettingOutlined />,
icon: <SettingOutlined aria-hidden />,
},
{
path: '/xray',
title: t('menu.xray'),
keywords: ['xray', 'templates', 'balancer', 'dns'],
icon: <ToolOutlined />,
icon: <ToolOutlined aria-hidden />,
},
{
path: '/api-docs',
title: t('menu.apiDocs'),
keywords: ['api', 'api docs', 'swagger', 'rest api', 'endpoints'],
icon: <ApiOutlined />,
icon: <ApiOutlined aria-hidden />,
},
{
path: '/sponsors',
title: t('menu.sponsors'),
keywords: ['sponsors', 'sponsor', 'partners'],
icon: <CrownOutlined />,
icon: <CrownOutlined aria-hidden />,
},
];
@@ -451,63 +451,63 @@ export default function CommandPalette() {
title: `${t('menu.settings')} · ${t('pages.settings.panelSettings')}`,
subtitle: t('pages.settings.panelSettings'),
keywords: ['general', 'webPort', 'webBasePath', 'listenIP', 'ssl', 'certificate'],
icon: <SettingOutlined />,
icon: <SettingOutlined aria-hidden />,
},
{
path: '/settings#security',
title: `${t('menu.settings')} · ${t('pages.settings.securitySettings')}`,
subtitle: t('pages.settings.securitySettings'),
keywords: ['security', 'password', 'username', '2fa', 'two factor', 'login limit'],
icon: <SafetyOutlined />,
icon: <SafetyOutlined aria-hidden />,
},
{
path: '/settings#telegram',
title: `${t('menu.settings')} · ${t('pages.settings.TGBotSettings')}`,
subtitle: t('pages.settings.TGBotSettings'),
keywords: ['telegram', 'tgbot', 'bot token', 'chat id', 'notifications', 'alerts'],
icon: <MessageOutlined />,
icon: <MessageOutlined aria-hidden />,
},
{
path: '/settings#email',
title: `${t('menu.settings')} · ${t('pages.settings.emailSettings')}`,
subtitle: t('pages.settings.emailSettings'),
keywords: ['email', 'smtp', 'mail', 'crash alerts'],
icon: <MailOutlined />,
icon: <MailOutlined aria-hidden />,
},
{
path: '/settings#discord',
title: `${t('menu.settings')} · ${t('pages.settings.discordSettings')}`,
subtitle: t('pages.settings.discordSettings'),
keywords: ['discord', 'bot', 'channel', 'notifications', 'alerts'],
icon: <DiscordOutlined />,
icon: <DiscordOutlined aria-hidden />,
},
{
path: '/settings#subscription',
title: `${t('menu.settings')} · ${t('pages.settings.subSettings')}`,
subtitle: t('pages.settings.subSettings'),
keywords: ['subscription', 'subPort', 'subURI', 'subDomain', 'reverse proxy'],
icon: <CloudServerOutlined />,
icon: <CloudServerOutlined aria-hidden />,
},
{
path: '/settings#subscription-formats',
title: `${t('menu.settings')} · ${t('menu.subFormats')}`,
subtitle: t('menu.subFormats'),
keywords: ['formats', 'clash', 'sing-box', 'v2ray', 'json', 'sub formats'],
icon: <CodeOutlined />,
icon: <CodeOutlined aria-hidden />,
},
{
path: '/settings#subscription-balancers',
title: `${t('menu.settings')} · ${t('pages.settings.subBalancers.menu')}`,
subtitle: t('pages.settings.subBalancers.menu'),
keywords: ['balancers', 'sub balancers', 'balancer nodes'],
icon: <ApartmentOutlined />,
icon: <ApartmentOutlined aria-hidden />,
},
{
path: '/xray#basic',
title: `${t('menu.xray')} · ${t('pages.xray.basicTemplate')}`,
subtitle: t('pages.xray.basicTemplate'),
keywords: ['basics', 'freedom strategy', 'happy eyeballs', 'torrent', 'connection'],
icon: <ToolOutlined />,
icon: <ToolOutlined aria-hidden />,
},
{
path: '/xray#basic',
@@ -522,56 +522,56 @@ export default function CommandPalette() {
'statsOutbound',
'metrics_out',
],
icon: <DashboardOutlined />,
icon: <DashboardOutlined aria-hidden />,
},
{
path: '/xray#basic',
title: `${t('menu.xray')} · ${t('pages.xray.connectionLimits')}`,
subtitle: t('pages.xray.connectionLimits'),
keywords: ['limits', 'idle timeout', 'bufferSize', 'connIdle', 'timeout'],
icon: <ClockCircleOutlined />,
icon: <ClockCircleOutlined aria-hidden />,
},
{
path: '/xray#basic',
title: `${t('menu.xray')} · ${t('pages.xray.logConfigs')}`,
subtitle: t('pages.xray.logConfigs'),
keywords: ['logs', 'access log', 'error log', 'dns log', 'mask address', 'loglevel'],
icon: <FileTextOutlined />,
icon: <FileTextOutlined aria-hidden />,
},
{
path: '/xray#balancer',
title: `${t('menu.xray')} · ${t('pages.xray.Balancers')}`,
subtitle: t('pages.xray.Balancers'),
keywords: ['balancers', 'leastPing', 'roundRobin', 'fallback', 'strategy'],
icon: <ClusterOutlined />,
icon: <ClusterOutlined aria-hidden />,
},
{
path: '/xray#dns',
title: `${t('menu.xray')} · DNS`,
subtitle: 'DNS',
keywords: ['dns', 'dns servers', 'hosts', 'doh', 'dot', 'cloudflare dns'],
icon: <DatabaseOutlined />,
icon: <DatabaseOutlined aria-hidden />,
},
{
path: '/xray#outbound',
title: `${t('menu.xray')} · ${t('pages.xray.Outbounds')}`,
subtitle: t('pages.xray.Outbounds'),
keywords: ['outbound', 'freedom', 'direct', 'proxy outbounds'],
icon: <ExportOutlined />,
icon: <ExportOutlined aria-hidden />,
},
{
path: '/xray#routing',
title: `${t('menu.xray')} · ${t('pages.xray.basicRouting')}`,
subtitle: t('pages.xray.basicRouting'),
keywords: ['routing', 'routing rules', 'geoip', 'geosite', 'block', 'direct'],
icon: <SwapOutlined />,
icon: <SwapOutlined aria-hidden />,
},
{
path: '/xray#advanced',
title: `${t('menu.xray')} · ${t('pages.xray.advancedTemplate')}`,
subtitle: t('pages.xray.advancedTemplate'),
keywords: ['advanced', 'json template', 'advanced config', 'custom json'],
icon: <CodeOutlined />,
icon: <CodeOutlined aria-hidden />,
},
];
@@ -599,7 +599,7 @@ export default function CommandPalette() {
title: t('commandPalette.restartXray'),
subtitle: t('pages.index.restartXray'),
keywords: ['restart', 'xray restart', 'reboot xray'],
icon: <ReloadOutlined style={{ color: '#faad14' }} />,
icon: <ReloadOutlined aria-hidden style={{ color: '#faad14' }} />,
action: restartXray,
},
{
@@ -608,7 +608,7 @@ export default function CommandPalette() {
title: t('menu.theme'),
subtitle: isUltra ? 'Ultra Dark' : isDark ? 'Dark' : 'Light',
keywords: ['theme', 'light', 'dark', 'ultra'],
icon: isDark ? <SunOutlined /> : <MoonOutlined />,
icon: isDark ? <SunOutlined aria-hidden /> : <MoonOutlined aria-hidden />,
action: cycleTheme,
},
{
@@ -617,7 +617,7 @@ export default function CommandPalette() {
title: t('pages.inbounds.addInbound'),
subtitle: t('menu.inbounds'),
keywords: ['add inbound', 'create inbound', 'new port', 'new inbound'],
icon: <PlusOutlined style={{ color: '#52c41a' }} />,
icon: <PlusOutlined aria-hidden style={{ color: '#52c41a' }} />,
action: () => {
close();
navigate('/inbounds');
@@ -629,7 +629,7 @@ export default function CommandPalette() {
title: t('pages.clients.addClient'),
subtitle: t('menu.clients'),
keywords: ['add client', 'create user', 'new client', 'new user'],
icon: <PlusOutlined style={{ color: '#52c41a' }} />,
icon: <PlusOutlined aria-hidden style={{ color: '#52c41a' }} />,
action: () => {
close();
navigate('/clients');
@@ -705,9 +705,9 @@ export default function CommandPalette() {
>
<div className="command-palette-header">
{isClientSearching ? (
<LoadingOutlined className="command-palette-search-icon spinning" />
<LoadingOutlined aria-hidden className="command-palette-search-icon spinning" />
) : (
<SearchOutlined className="command-palette-search-icon" />
<SearchOutlined aria-hidden className="command-palette-search-icon" />
)}
<input
ref={inputRef}