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
+25 -20
View File
@@ -1,4 +1,4 @@
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
import { lazy, useCallback, useEffect, useId, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
@@ -89,6 +89,7 @@ async function fetchEmailsForGroup(name: string): Promise<string[]> {
export default function GroupsPage() {
usePageTitle();
const { t } = useTranslation();
const uid = useId();
const { isDark, isUltra, antdThemeConfig } = useTheme();
const { isMobile } = useMediaQuery();
const [modal, modalContextHolder] = Modal.useModal();
@@ -353,41 +354,41 @@ export default function GroupsPage() {
return [
{
key: 'subLinks',
icon: <LinkOutlined />,
icon: <LinkOutlined aria-hidden />,
label: t('pages.clients.subLinksSelected', { count: row.clientCount || 0 }),
disabled: !row.clientCount,
onClick: () => openSubLinksFor(row),
},
{
key: 'adjust',
icon: <ClockCircleOutlined />,
icon: <ClockCircleOutlined aria-hidden />,
label: t('pages.clients.adjustSelected', { count: row.clientCount || 0 }),
disabled: !row.clientCount,
onClick: () => openAdjustFor(row),
},
{
key: 'reset',
icon: <RetweetOutlined />,
icon: <RetweetOutlined aria-hidden />,
label: t('pages.groups.resetTraffic'),
disabled: !row.clientCount,
onClick: () => onResetTraffic(row),
},
{
key: 'addClients',
icon: <UsergroupAddOutlined />,
icon: <UsergroupAddOutlined aria-hidden />,
label: t('pages.groups.addToGroup'),
onClick: () => openAddClientsFor(row),
},
{
key: 'rename',
icon: <EditOutlined />,
icon: <EditOutlined aria-hidden />,
label: t('pages.groups.rename'),
onClick: () => openRename(row),
},
{ type: 'divider' },
{
key: 'removeClients',
icon: <UsergroupDeleteOutlined />,
icon: <UsergroupDeleteOutlined aria-hidden />,
label: t('pages.groups.removeFromGroup'),
danger: true,
disabled: !row.clientCount,
@@ -395,7 +396,7 @@ export default function GroupsPage() {
},
{
key: 'deleteClients',
icon: <DeleteOutlined />,
icon: <DeleteOutlined aria-hidden />,
label: t('pages.groups.deleteClients'),
danger: true,
disabled: !row.clientCount,
@@ -403,7 +404,7 @@ export default function GroupsPage() {
},
{
key: 'delete',
icon: <DeleteOutlined />,
icon: <DeleteOutlined aria-hidden />,
label: t('pages.groups.deleteGroupOnly'),
danger: true,
onClick: () => onDelete(row),
@@ -424,7 +425,7 @@ export default function GroupsPage() {
size="small"
type="text"
style={{ fontSize: 16 }}
icon={<MoreOutlined />}
icon={<MoreOutlined aria-hidden />}
/>
</Dropdown>
<Tooltip title={t('pages.groups.rename')}>
@@ -433,7 +434,7 @@ export default function GroupsPage() {
size="small"
type="text"
style={{ fontSize: 16 }}
icon={<EditOutlined />}
icon={<EditOutlined aria-hidden />}
onClick={() => openRename(row)}
/>
</Tooltip>
@@ -518,14 +519,14 @@ export default function GroupsPage() {
<Statistic
title={t('pages.groups.totalGroups')}
value={String(totalGroups)}
prefix={<TagsOutlined />}
prefix={<TagsOutlined aria-hidden />}
/>
</Col>
<Col xs={12} sm={12} md={6}>
<Statistic
title={t('pages.groups.totalGroupedClients')}
value={String(totalClients)}
prefix={<TeamOutlined />}
prefix={<TeamOutlined aria-hidden />}
/>
</Col>
<Col xs={12} sm={12} md={6}>
@@ -534,9 +535,11 @@ export default function GroupsPage() {
value={0}
formatter={() => (
<span>
<ArrowUpOutlined /> {SizeFormatter.sizeFormat(totalUpload)}
<ArrowUpOutlined aria-hidden />{' '}
{SizeFormatter.sizeFormat(totalUpload)}
{' / '}
<ArrowDownOutlined /> {SizeFormatter.sizeFormat(totalDownload)}
<ArrowDownOutlined aria-hidden />{' '}
{SizeFormatter.sizeFormat(totalDownload)}
</span>
)}
/>
@@ -545,7 +548,7 @@ export default function GroupsPage() {
<Statistic
title={t('pages.groups.totalTraffic')}
value={SizeFormatter.sizeFormat(totalTraffic)}
prefix={<PieChartOutlined />}
prefix={<PieChartOutlined aria-hidden />}
/>
</Col>
</Row>
@@ -561,7 +564,7 @@ export default function GroupsPage() {
<Button
aria-label={t('pages.groups.addGroup')}
type="primary"
icon={<PlusOutlined />}
icon={<PlusOutlined aria-hidden />}
onClick={openCreate}
>
{!isMobile && t('pages.groups.addGroup')}
@@ -579,7 +582,7 @@ export default function GroupsPage() {
locale={{
emptyText: (
<div className="card-empty">
<TagsOutlined style={{ fontSize: 32, marginBottom: 8 }} />
<TagsOutlined aria-hidden style={{ fontSize: 32, marginBottom: 8 }} />
<div>{t('noData')}</div>
</div>
),
@@ -604,8 +607,9 @@ export default function GroupsPage() {
destroyOnHidden
>
<Form layout="vertical">
<Form.Item label={t('pages.groups.name')}>
<Form.Item label={t('pages.groups.name')} htmlFor={`${uid}-create-name`}>
<Input
id={`${uid}-create-name`}
value={createName}
onChange={(e) => setCreateName(e.target.value)}
onPressEnter={confirmCreate}
@@ -627,8 +631,9 @@ export default function GroupsPage() {
destroyOnHidden
>
<Form layout="vertical">
<Form.Item label={t('pages.groups.name')}>
<Form.Item label={t('pages.groups.name')} htmlFor={`${uid}-rename-name`}>
<Input
id={`${uid}-rename-name`}
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onPressEnter={confirmRename}