mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-12 16:15:02 +03:00
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:
@@ -199,6 +199,7 @@ export default function RealityTargetScannerModal({
|
||||
{t('pages.inbounds.form.scanModalDesc')}
|
||||
</Typography.Paragraph>
|
||||
<Input.Search
|
||||
aria-label={t('pages.inbounds.form.scanDiscoverPlaceholder')}
|
||||
allowClear
|
||||
enterButton={t('pages.inbounds.form.scan')}
|
||||
loading={loading}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useId } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -59,6 +59,7 @@ export default function RealityForm({
|
||||
clearMldsa65,
|
||||
}: RealityFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const { getFieldState, trigger } = useFormContext();
|
||||
const [scannerOpen, setScannerOpen] = useState(false);
|
||||
const mldsa65Seed = useWatch({ name: 'streamSettings.realitySettings.mldsa65Seed' });
|
||||
@@ -113,6 +114,7 @@ export default function RealityForm({
|
||||
<Select options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))} />
|
||||
</FormField>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-target`}
|
||||
label={t('pages.inbounds.form.target')}
|
||||
tooltip={t('pages.inbounds.form.realityTargetHint')}
|
||||
>
|
||||
@@ -127,19 +129,27 @@ export default function RealityForm({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Input style={{ flex: 1 }} placeholder="example.com:443" />
|
||||
<Input
|
||||
id={`${uid}-target`}
|
||||
aria-describedby={`${uid}-target-hint`}
|
||||
style={{ flex: 1 }}
|
||||
placeholder="example.com:443"
|
||||
/>
|
||||
</FormField>
|
||||
<Button
|
||||
icon={<RadarChartOutlined />}
|
||||
icon={<RadarChartOutlined aria-hidden />}
|
||||
loading={scanning}
|
||||
onClick={() => scanRealityTarget()}
|
||||
>
|
||||
{t('pages.inbounds.form.scan')}
|
||||
</Button>
|
||||
<Button icon={<SearchOutlined />} onClick={() => setScannerOpen(true)}>
|
||||
<Button icon={<SearchOutlined aria-hidden />} onClick={() => setScannerOpen(true)}>
|
||||
{t('pages.inbounds.form.findTargets')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<span id={`${uid}-target-hint`} hidden>
|
||||
{t('pages.inbounds.form.realityTargetHint')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
{scanResult && (
|
||||
<Form.Item label=" " colon={false}>
|
||||
@@ -235,32 +245,45 @@ export default function RealityForm({
|
||||
>
|
||||
<Input placeholder="x.y.z" />
|
||||
</FormField>
|
||||
<Form.Item label={t('pages.inbounds.form.shortIds')}>
|
||||
<Form.Item htmlFor={`${uid}-shortIds`} label={t('pages.inbounds.form.shortIds')}>
|
||||
<Space.Compact block style={{ display: 'flex' }}>
|
||||
<FormField name={['streamSettings', 'realitySettings', 'shortIds']} noStyle>
|
||||
<Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
|
||||
<Select
|
||||
id={`${uid}-shortIds`}
|
||||
mode="tags"
|
||||
tokenSeparators={[',']}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</FormField>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={randomizeShortIds}
|
||||
/>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-spiderX`}
|
||||
label={t('pages.inbounds.form.spiderX')}
|
||||
tooltip={t('pages.inbounds.form.spiderXHint')}
|
||||
>
|
||||
<Space.Compact block style={{ display: 'flex' }}>
|
||||
<FormField name={['streamSettings', 'realitySettings', 'settings', 'spiderX']} noStyle>
|
||||
<Input style={{ flex: 1 }} />
|
||||
<Input
|
||||
id={`${uid}-spiderX`}
|
||||
aria-describedby={`${uid}-spiderX-hint`}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</FormField>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={randomizeSpiderX}
|
||||
/>
|
||||
</Space.Compact>
|
||||
<span id={`${uid}-spiderX-hint`} hidden>
|
||||
{t('pages.inbounds.form.spiderXHint')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
<FormField
|
||||
name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, InputNumber, Radio, Select, Space, Switch } from 'antd';
|
||||
import {
|
||||
@@ -77,7 +78,7 @@ function CertRow({
|
||||
{total > 1 && (
|
||||
<Form.Item label=" ">
|
||||
<Button size="small" danger onClick={onRemove}>
|
||||
<MinusOutlined /> {t('remove')}
|
||||
<MinusOutlined aria-hidden /> {t('remove')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
@@ -161,16 +162,20 @@ function CertRow({
|
||||
|
||||
function EchSockoptSection() {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const { control, setValue } = useFormContext();
|
||||
const echSockopt = useWatch({ control, name: 'streamSettings.tlsSettings.echSockopt' });
|
||||
const on = !!echSockopt;
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-echSockopt`}
|
||||
label={t('pages.inbounds.form.echSockopt')}
|
||||
tooltip={t('pages.inbounds.form.echSockoptTip')}
|
||||
>
|
||||
<Switch
|
||||
id={`${uid}-echSockopt`}
|
||||
aria-describedby={`${uid}-echSockopt-hint`}
|
||||
checked={on}
|
||||
onChange={(v) =>
|
||||
setValue(
|
||||
@@ -179,6 +184,9 @@ function EchSockoptSection() {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span id={`${uid}-echSockopt-hint`} hidden>
|
||||
{t('pages.inbounds.form.echSockoptTip')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
{on && (
|
||||
<>
|
||||
@@ -226,6 +234,7 @@ export default function TlsForm({
|
||||
clearEchCert,
|
||||
}: TlsFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const { control } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
@@ -242,16 +251,18 @@ export default function TlsForm({
|
||||
>
|
||||
<CipherSuitesSelect placeholder={t('pages.inbounds.form.autoOption')} />
|
||||
</FormField>
|
||||
<Form.Item label={t('pages.inbounds.form.minMaxVersion')}>
|
||||
<Form.Item htmlFor={`${uid}-minVersion`} label={t('pages.inbounds.form.minMaxVersion')}>
|
||||
<Space.Compact block>
|
||||
<FormField name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
|
||||
<Select
|
||||
id={`${uid}-minVersion`}
|
||||
style={{ width: '50%' }}
|
||||
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
|
||||
<Select
|
||||
aria-label={t('pages.inbounds.form.maxVersion')}
|
||||
style={{ width: '50%' }}
|
||||
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
||||
/>
|
||||
@@ -330,7 +341,7 @@ export default function TlsForm({
|
||||
})
|
||||
}
|
||||
>
|
||||
<PlusOutlined />
|
||||
<PlusOutlined aria-hidden />
|
||||
</Button>
|
||||
</Form.Item>
|
||||
{fields.map((field, idx) => (
|
||||
@@ -375,6 +386,7 @@ export default function TlsForm({
|
||||
</Space>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-pinnedPeerCertSha256`}
|
||||
label={t('pages.inbounds.form.pinnedPeerCertSha256')}
|
||||
tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
|
||||
>
|
||||
@@ -384,6 +396,8 @@ export default function TlsForm({
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
id={`${uid}-pinnedPeerCertSha256`}
|
||||
aria-describedby={`${uid}-pinnedPeerCertSha256-hint`}
|
||||
mode="tags"
|
||||
tokenSeparators={[',', ' ']}
|
||||
placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
|
||||
@@ -391,18 +405,23 @@ export default function TlsForm({
|
||||
/>
|
||||
</FormField>
|
||||
<Button
|
||||
icon={<FileProtectOutlined />}
|
||||
icon={<FileProtectOutlined aria-hidden />}
|
||||
onClick={pinFromCert}
|
||||
loading={saving}
|
||||
aria-label={t('pages.inbounds.form.pinFromCert')}
|
||||
title={t('pages.inbounds.form.pinFromCert')}
|
||||
/>
|
||||
<Button
|
||||
icon={<CloudDownloadOutlined />}
|
||||
icon={<CloudDownloadOutlined aria-hidden />}
|
||||
onClick={() => pinFromRemote()}
|
||||
loading={saving}
|
||||
aria-label={t('pages.inbounds.form.pinFromRemote')}
|
||||
title={t('pages.inbounds.form.pinFromRemote')}
|
||||
/>
|
||||
</Space.Compact>
|
||||
<span id={`${uid}-pinnedPeerCertSha256-hint`} hidden>
|
||||
{t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
<FormField
|
||||
name={['streamSettings', 'tlsSettings', 'settings', 'verifyPeerCertByName']}
|
||||
|
||||
Reference in New Issue
Block a user