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
+18 -5
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState, useId } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Button, Form, Input, Modal, Select, Space, Spin, Typography, message } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
@@ -33,6 +33,7 @@ function fileNameFromUrl(url: string): string {
export default function GeodataSection({ active, onBusy, onClose }: GeodataSectionProps) {
const { t } = useTranslation();
const uid = useId();
const [modal, modalContextHolder] = Modal.useModal();
const [messageApi, messageContextHolder] = message.useMessage();
const [loading, setLoading] = useState(false);
@@ -180,15 +181,25 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
<Spin spinning={loading}>
<Alert type="info" className="mb-12" title={t('pages.index.geodataHint')} showIcon />
<Form layout="vertical">
<Form.Item label={t('pages.index.geodataCron')} style={{ marginBottom: 8 }}>
<Form.Item
htmlFor={`${uid}-cron`}
label={t('pages.index.geodataCron')}
style={{ marginBottom: 8 }}
>
<Input
id={`${uid}-cron`}
value={cron}
placeholder={DEFAULT_CRON}
onChange={(e) => setCron(e.target.value)}
/>
</Form.Item>
<Form.Item label={t('pages.index.geodataOutbound')} style={{ marginBottom: 8 }}>
<Form.Item
htmlFor={`${uid}-outbound`}
label={t('pages.index.geodataOutbound')}
style={{ marginBottom: 8 }}
>
<Select
id={`${uid}-outbound`}
style={{ width: '100%' }}
allowClear
value={outbound}
@@ -204,6 +215,7 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
{rows.map((row, i) => (
<Space.Compact key={i} style={{ width: '100%' }}>
<Input
aria-label={`URL ${i + 1}`}
style={{ width: '60%' }}
placeholder="https://example.com/geosite_custom.dat"
value={row.url}
@@ -211,6 +223,7 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
onBlur={() => onUrlBlur(i)}
/>
<Input
aria-label={`${t('pages.index.geodataFile')} ${i + 1}`}
style={{ width: '40%' }}
placeholder={t('pages.index.geodataFile')}
value={row.file}
@@ -218,14 +231,14 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
/>
<Button
aria-label={t('delete')}
icon={<DeleteOutlined />}
icon={<DeleteOutlined aria-hidden />}
onClick={() => setRows((p) => p.filter((_, j) => j !== i))}
/>
</Space.Compact>
))}
<div className="actions-row">
<Button
icon={<PlusOutlined />}
icon={<PlusOutlined aria-hidden />}
onClick={() => setRows((p) => [...p, { url: '', file: '' }])}
>
{t('pages.index.geodataAddFile')}