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
+12 -8
View File
@@ -208,7 +208,11 @@ export default function HostFormModal({
{
key: 'basic',
forceRender: true,
label: catTabLabel(<ProfileOutlined />, t('pages.hosts.sections.basic'), isMobile),
label: catTabLabel(
<ProfileOutlined aria-hidden />,
t('pages.hosts.sections.basic'),
isMobile,
),
children: (
<>
<FormField
@@ -291,7 +295,7 @@ export default function HostFormModal({
key: 'security',
forceRender: true,
label: catTabLabel(
<SafetyCertificateOutlined />,
<SafetyCertificateOutlined aria-hidden />,
t('pages.hosts.sections.security'),
isMobile,
),
@@ -373,7 +377,7 @@ export default function HostFormModal({
key: 'advanced',
forceRender: true,
label: catTabLabel(
<ControlOutlined />,
<ControlOutlined aria-hidden />,
t('pages.hosts.sections.advanced'),
isMobile,
),
@@ -386,7 +390,7 @@ export default function HostFormModal({
key: 'adv-general',
forceRender: true,
label: catTabLabel(
<SettingOutlined />,
<SettingOutlined aria-hidden />,
t('pages.hosts.sections.general'),
isMobile,
),
@@ -425,7 +429,7 @@ export default function HostFormModal({
key: 'adv-mux',
forceRender: true,
label: catTabLabel(
<PartitionOutlined />,
<PartitionOutlined aria-hidden />,
t('pages.hosts.fields.muxParams'),
isMobile,
),
@@ -445,7 +449,7 @@ export default function HostFormModal({
key: 'adv-sockopt',
forceRender: true,
label: catTabLabel(
<DeploymentUnitOutlined />,
<DeploymentUnitOutlined aria-hidden />,
t('pages.hosts.fields.sockoptParams'),
isMobile,
),
@@ -465,7 +469,7 @@ export default function HostFormModal({
key: 'adv-finalmask',
forceRender: true,
label: catTabLabel(
<RocketOutlined />,
<RocketOutlined aria-hidden />,
t('pages.hosts.fields.finalMask'),
isMobile,
),
@@ -489,7 +493,7 @@ export default function HostFormModal({
key: 'clash',
forceRender: true,
label: catTabLabel(
<NodeIndexOutlined />,
<NodeIndexOutlined aria-hidden />,
t('pages.hosts.sections.clash'),
isMobile,
),
+13 -8
View File
@@ -95,7 +95,7 @@ export default function HostList(props: HostListProps) {
<Button
size="small"
type="text"
icon={<ArrowUpOutlined />}
icon={<ArrowUpOutlined aria-hidden />}
aria-label={t('pages.hosts.moveUp')}
disabled={idx === 0}
onClick={() => onMove(h, 'up')}
@@ -105,7 +105,7 @@ export default function HostList(props: HostListProps) {
<Button
size="small"
type="text"
icon={<ArrowDownOutlined />}
icon={<ArrowDownOutlined aria-hidden />}
aria-label={t('pages.hosts.moveDown')}
disabled={idx >= count - 1}
onClick={() => onMove(h, 'down')}
@@ -115,7 +115,7 @@ export default function HostList(props: HostListProps) {
<Button
size="small"
type="text"
icon={<EditOutlined />}
icon={<EditOutlined aria-hidden />}
aria-label={t('edit')}
onClick={() => onEdit(h)}
/>
@@ -125,7 +125,7 @@ export default function HostList(props: HostListProps) {
size="small"
type="text"
danger
icon={<DeleteOutlined />}
icon={<DeleteOutlined aria-hidden />}
aria-label={t('delete')}
onClick={() => onDelete(h)}
/>
@@ -139,7 +139,12 @@ export default function HostList(props: HostListProps) {
key: 'enable',
width: 90,
render: (_, h) => (
<Switch size="small" checked={!h.isDisabled} onChange={(next) => onToggleEnable(h, next)} />
<Switch
size="small"
checked={!h.isDisabled}
aria-label={t('pages.hosts.fields.enable')}
onChange={(next) => onToggleEnable(h, next)}
/>
),
},
{
@@ -276,7 +281,7 @@ export default function HostList(props: HostListProps) {
const toolbar = (
<div className="card-toolbar">
{selectedGroupIds.length === 0 ? (
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={onAdd}>
{!isMobile && t('pages.hosts.addHost')}
</Button>
) : (
@@ -291,7 +296,7 @@ export default function HostList(props: HostListProps) {
</Tag>
<Button onClick={() => onBulkEnable(true)}>{t('pages.hosts.bulkEnable')}</Button>
<Button onClick={() => onBulkEnable(false)}>{t('pages.hosts.bulkDisable')}</Button>
<Button danger icon={<DeleteOutlined />} onClick={onBulkDelete}>
<Button danger icon={<DeleteOutlined aria-hidden />} onClick={onBulkDelete}>
{t('pages.hosts.bulkDelete')}
</Button>
</>
@@ -316,7 +321,7 @@ export default function HostList(props: HostListProps) {
locale={{
emptyText: (
<div className="card-empty">
<GlobalOutlined style={{ fontSize: 32, marginBottom: 8 }} />
<GlobalOutlined aria-hidden style={{ fontSize: 32, marginBottom: 8 }} />
<div>{t('noData')}</div>
</div>
),
+9 -3
View File
@@ -174,7 +174,7 @@ export default function HostsPage() {
<Statistic
title={t('pages.hosts.summary.total')}
value={String(summary.total)}
prefix={<GlobalOutlined />}
prefix={<GlobalOutlined aria-hidden />}
/>
</Col>
<Col xs={8} sm={8} md={8}>
@@ -182,7 +182,10 @@ export default function HostsPage() {
title={t('pages.hosts.summary.enabled')}
value={String(summary.enabled)}
prefix={
<CheckCircleOutlined style={{ color: 'var(--ant-color-success)' }} />
<CheckCircleOutlined
aria-hidden
style={{ color: 'var(--ant-color-success)' }}
/>
}
/>
</Col>
@@ -191,7 +194,10 @@ export default function HostsPage() {
title={t('pages.hosts.summary.disabled')}
value={String(summary.disabled)}
prefix={
<StopOutlined style={{ color: 'var(--ant-color-text-quaternary)' }} />
<StopOutlined
aria-hidden
style={{ color: 'var(--ant-color-text-quaternary)' }}
/>
}
/>
</Col>