Files
3x-ui/frontend/src/pages/inbounds/form/protocols/vless.tsx
T
Alireza Ahmadi e2184777a0 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.
2026-10-23 18:29:47 +02:00

102 lines
3.4 KiB
TypeScript

import { useState, useId } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input, InputNumber, Select, Space, Typography } from 'antd';
import { Controller, useFormContext } from 'react-hook-form';
import { FormField } from '@/components/form/rhf';
import { VLESS_AUTH_LABEL_KEYS, type VlessAuthKind } from '@/lib/xray/vless-encryption';
interface VlessFieldsProps {
saving: boolean;
selectedVlessAuth: string;
vlessAuthKind: VlessAuthKind | null;
network: string;
security: string;
getNewVlessEnc: (kind: VlessAuthKind) => void;
clearVlessEnc: () => void;
}
export default function VlessFields({
saving,
selectedVlessAuth,
vlessAuthKind,
network,
security,
getNewVlessEnc,
clearVlessEnc,
}: VlessFieldsProps) {
const { t } = useTranslation();
const uid = useId();
const { control } = useFormContext();
const [authKind, setAuthKind] = useState<VlessAuthKind>(vlessAuthKind ?? 'x25519');
const [syncedAuthKind, setSyncedAuthKind] = useState(vlessAuthKind);
if (vlessAuthKind !== syncedAuthKind) {
setSyncedAuthKind(vlessAuthKind);
setAuthKind(vlessAuthKind ?? 'x25519');
}
const authOptions = (Object.entries(VLESS_AUTH_LABEL_KEYS) as [VlessAuthKind, string][]).map(
([value, labelKey]) => ({ value, label: t(labelKey) }),
);
return (
<>
<FormField name={['settings', 'decryption']} label={t('pages.inbounds.decryption')}>
<Input />
</FormField>
<FormField name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
<Input />
</FormField>
<Form.Item htmlFor={`${uid}-authKind`} label={t('pages.inbounds.vlessAuthGenerate')}>
<Space size={8} wrap>
<Select
id={`${uid}-authKind`}
value={authKind}
onChange={(v) => setAuthKind(v)}
options={authOptions}
style={{ width: 240 }}
/>
<Button type="primary" loading={saving} onClick={() => getNewVlessEnc(authKind)}>
{t('pages.inbounds.vlessAuthGenerateButton')}
</Button>
<Button danger onClick={clearVlessEnc}>
{t('clear')}
</Button>
</Space>
<Typography.Text type="secondary" className="vless-auth-state">
{t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
</Typography.Text>
</Form.Item>
{network === 'tcp' && (security === 'tls' || security === 'reality') && (
<Form.Item
label={t('pages.inbounds.form.visionTestseed')}
extra="Applies only to clients using the xtls-rprx-vision flow; ignored otherwise."
>
<Space.Compact block>
{[900, 500, 900, 256].map((def, i) => (
<Controller
key={i}
control={control}
name={`settings.testseed.${i}`}
defaultValue={def}
render={({ field }) => (
<InputNumber
aria-label={`${t('pages.inbounds.form.visionTestseed')} ${i + 1}`}
min={1}
style={{ width: '25%' }}
value={field.value as number}
onChange={field.onChange}
onBlur={field.onBlur}
ref={field.ref}
/>
)}
/>
))}
</Space.Compact>
</Form.Item>
)}
</>
);
}