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 -22
View File
@@ -252,7 +252,7 @@ export default function NodeList({
parent: record.parentGuid ? nameByGuid.get(record.parentGuid) || '-' : '-',
})}
>
<Tag icon={<ApartmentOutlined />} style={{ margin: 0 }}>
<Tag icon={<ApartmentOutlined aria-hidden />} style={{ margin: 0 }}>
{t('pages.nodes.subNode')}
</Tag>
</Tooltip>
@@ -263,7 +263,7 @@ export default function NodeList({
type="text"
size="small"
style={{ fontSize: 16 }}
icon={<ThunderboltOutlined />}
icon={<ThunderboltOutlined aria-hidden />}
aria-label={t('pages.nodes.probe')}
onClick={() => onProbe(record)}
/>
@@ -274,7 +274,7 @@ export default function NodeList({
type="text"
size="small"
style={{ fontSize: 16 }}
icon={<CloudDownloadOutlined />}
icon={<CloudDownloadOutlined aria-hidden />}
aria-label={t('pages.nodes.updatePanel')}
onClick={() => onUpdateNode(record)}
/>
@@ -285,7 +285,7 @@ export default function NodeList({
type="text"
size="small"
style={{ fontSize: 16 }}
icon={<EditOutlined />}
icon={<EditOutlined aria-hidden />}
aria-label={t('edit')}
onClick={() => onEdit(record)}
/>
@@ -296,7 +296,7 @@ export default function NodeList({
size="small"
danger
style={{ fontSize: 16 }}
icon={<DeleteOutlined />}
icon={<DeleteOutlined aria-hidden />}
aria-label={t('delete')}
onClick={() => onDelete(record)}
/>
@@ -316,6 +316,7 @@ export default function NodeList({
<Switch
checked={!!record.enable}
size="small"
aria-label={t('pages.nodes.enable')}
onChange={(v) => onToggleEnable(record, v)}
/>
),
@@ -331,7 +332,7 @@ export default function NodeList({
>
<span className="name">
{record.transitive && (
<ApartmentOutlined style={{ marginInlineEnd: 6, opacity: 0.6 }} />
<ApartmentOutlined aria-hidden style={{ marginInlineEnd: 6, opacity: 0.6 }} />
)}
{record.name}
</span>
@@ -391,7 +392,7 @@ export default function NodeList({
<StatusLabel status={record.status} xrayState={record.xrayState} />
{tip && (
<Tooltip title={tip}>
<ExclamationCircleOutlined style={{ color: iconColor }} />
<ExclamationCircleOutlined style={{ color: iconColor }} aria-label={tip} />
</Tooltip>
)}
</Space>
@@ -460,7 +461,7 @@ export default function NodeList({
render: (_value, record) => (
<Space size={2}>
<Tag className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>
<TeamOutlined /> {record.clientCount || 0}
<TeamOutlined aria-hidden /> {record.clientCount || 0}
</Tag>
{record.activeCount ? (
<Tooltip title={t('subscription.active')}>
@@ -562,7 +563,7 @@ export default function NodeList({
locale={{
emptyText: (
<div className="card-empty">
<ClusterOutlined style={{ fontSize: 32, marginBottom: 8 }} />
<ClusterOutlined aria-hidden style={{ fontSize: 32, marginBottom: 8 }} />
<div>{t('noData')}</div>
</div>
),
@@ -579,14 +580,14 @@ export default function NodeList({
return (
<Card size="small" hoverable>
<div className="toolbar">
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={onAdd}>
{t('pages.nodes.addNode')}
</Button>
<Button icon={<SafetyCertificateOutlined />} onClick={onMtls}>
<Button icon={<SafetyCertificateOutlined aria-hidden />} onClick={onMtls}>
{t('pages.nodes.mtls.title')}
</Button>
{selectedIds.length > 0 && (
<Button icon={<CloudDownloadOutlined />} onClick={onUpdateSelected}>
<Button icon={<CloudDownloadOutlined aria-hidden />} onClick={onUpdateSelected}>
{t('pages.nodes.updateSelected', { count: selectedIds.length })}
</Button>
)}
@@ -597,7 +598,7 @@ export default function NodeList({
<div className="node-cards">
{dataSource.length === 0 ? (
<div className="card-empty">
<ClusterOutlined style={{ fontSize: 28, opacity: 0.5 }} />
<ClusterOutlined aria-hidden style={{ fontSize: 28, opacity: 0.5 }} />
<div>{t('noData')}</div>
</div>
) : (
@@ -609,11 +610,11 @@ export default function NodeList({
style={{ paddingInlineStart: 16, opacity: 0.85 }}
>
<div className="card-head">
<ApartmentOutlined style={{ opacity: 0.6 }} />
<ApartmentOutlined aria-hidden style={{ opacity: 0.6 }} />
<StatusDot status={record.status} xrayState={record.xrayState} />
<span className="node-name">{record.name}</span>
<div className="card-actions">
<Tag icon={<ApartmentOutlined />} style={{ margin: 0 }}>
<Tag icon={<ApartmentOutlined aria-hidden />} style={{ margin: 0 }}>
{t('pages.nodes.subNode')}
</Tag>
</div>
@@ -653,6 +654,7 @@ export default function NodeList({
<Switch
checked={!!record.enable}
size="small"
aria-label={t('pages.nodes.enable')}
onChange={(v) => onToggleEnable(record, v)}
/>
<Dropdown
@@ -664,7 +666,7 @@ export default function NodeList({
key: 'probe',
label: (
<>
<ThunderboltOutlined /> {t('pages.nodes.probe')}
<ThunderboltOutlined aria-hidden /> {t('pages.nodes.probe')}
</>
),
onClick: () => onProbe(record),
@@ -675,7 +677,8 @@ export default function NodeList({
key: 'update',
label: (
<>
<CloudDownloadOutlined /> {t('pages.nodes.updatePanel')}
<CloudDownloadOutlined aria-hidden />{' '}
{t('pages.nodes.updatePanel')}
</>
),
onClick: () => onUpdateNode(record),
@@ -686,7 +689,7 @@ export default function NodeList({
key: 'edit',
label: (
<>
<EditOutlined /> {t('edit')}
<EditOutlined aria-hidden /> {t('edit')}
</>
),
onClick: () => onEdit(record),
@@ -696,7 +699,7 @@ export default function NodeList({
danger: true,
label: (
<>
<DeleteOutlined /> {t('delete')}
<DeleteOutlined aria-hidden /> {t('delete')}
</>
),
onClick: () => onDelete(record),
@@ -708,7 +711,7 @@ export default function NodeList({
type="text"
size="small"
className="row-action-trigger"
icon={<MoreOutlined />}
icon={<MoreOutlined aria-hidden />}
aria-label={t('more')}
/>
</Dropdown>
@@ -782,7 +785,7 @@ export default function NodeList({
const { tip, iconColor } = statusIssue(statsNode);
return tip ? (
<Tooltip title={tip}>
<ExclamationCircleOutlined style={{ color: iconColor }} />
<ExclamationCircleOutlined style={{ color: iconColor }} aria-label={tip} />
</Tooltip>
) : null;
})()}
@@ -820,7 +823,7 @@ export default function NodeList({
<div className="stat-row">
<span className="stat-label">{t('clients')}</span>
<Tag>
<TeamOutlined /> {statsNode.clientCount || 0}
<TeamOutlined aria-hidden /> {statsNode.clientCount || 0}
</Tag>
{statsNode.activeCount ? (
<Tag color="green">