mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-12 16:15:02 +03:00
1bdb517b88
Ant Design's Spin wraps its content in aria-live="polite", so every WebSocket tick on the dashboard, inbounds, clients and nodes pages was announced, and only the changed number was read, with no label. Pass aria-live="off" on every Spin that wraps content; aria-busy still signals loading. Each vital tile is now a group named by its metric, so "47.2 %" is read as CPU. rc-select renders each tag's remove "x" as an aria-hidden span with no tabIndex, so screen-reader users could only Backspace the last value away, never remove one SNI or inbound from the middle. selectTagRender swaps in a real button named "Remove <value>" and moves focus to the next tag after removal. It is applied to every tags/multiple Select. maxTagCount="responsive" is dropped because it hid selected inbounds behind "+N", where no screen reader could reach them. Two oxlint rules (antd-a11y/spin-content-not-live and antd-a11y/select-tag-remove) keep new code from regressing either fix.
125 lines
4.3 KiB
TypeScript
125 lines
4.3 KiB
TypeScript
import { useId, type ReactNode } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
|
|
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
|
|
|
import { selectTagRender } from '@/components/form';
|
|
import { FormField } from '@/components/form/rhf';
|
|
|
|
interface StringListProps {
|
|
name: string[];
|
|
label: ReactNode;
|
|
tooltip?: string;
|
|
placeholder: (index: number) => string;
|
|
}
|
|
|
|
function StringList({ name, label, tooltip, placeholder }: StringListProps) {
|
|
const { t } = useTranslation();
|
|
const labelId = useId();
|
|
const { control } = useFormContext();
|
|
const { fields, append, remove } = useFieldArray({ control, name: name.join('.') });
|
|
return (
|
|
<Form.Item label={<span id={labelId}>{label}</span>} tooltip={tooltip}>
|
|
<Button aria-label={t('add')} size="small" onClick={() => append('')}>
|
|
<PlusOutlined aria-hidden />
|
|
</Button>
|
|
{fields.map((field, j) => (
|
|
<Space.Compact key={field.id} block className="mt-4">
|
|
<FormField name={[...name, j]} noStyle>
|
|
<Input
|
|
aria-labelledby={labelId}
|
|
aria-describedby={tooltip ? `${labelId}-hint` : undefined}
|
|
placeholder={placeholder(j)}
|
|
/>
|
|
</FormField>
|
|
<Button aria-label={t('remove')} size="small" onClick={() => remove(j)}>
|
|
<MinusOutlined aria-hidden />
|
|
</Button>
|
|
</Space.Compact>
|
|
))}
|
|
{tooltip && (
|
|
<span id={`${labelId}-hint`} hidden>
|
|
{tooltip}
|
|
</span>
|
|
)}
|
|
</Form.Item>
|
|
);
|
|
}
|
|
|
|
// Named autoSystemWfpBlockLeak values; xray-core 26.10.31 also takes typed "tcp/<port>" or "udp/<port>".
|
|
const WFP_BLOCK_LEAK_OPTIONS = [
|
|
{ value: 'dns', label: 'dns' },
|
|
{ value: 'windnsclient', label: 'windnsclient' },
|
|
{ value: 'misconfigtun', label: 'misconfigtun' },
|
|
];
|
|
|
|
export default function TunFields() {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<>
|
|
<FormField name={['settings', 'name']} label={t('pages.inbounds.info.interfaceName')}>
|
|
<Input placeholder="xray0" />
|
|
</FormField>
|
|
<FormField
|
|
name={['settings', 'desc']}
|
|
label={t('pages.inbounds.form.tunDesc')}
|
|
tooltip={t('pages.inbounds.form.tunDescTooltip')}
|
|
>
|
|
<Input id="tun-desc" placeholder="Wintun" />
|
|
</FormField>
|
|
<FormField name={['settings', 'mtu']} label="MTU">
|
|
<InputNumber min={0} />
|
|
</FormField>
|
|
<StringList
|
|
name={['settings', 'gateway']}
|
|
label={t('pages.inbounds.info.gateway')}
|
|
placeholder={(j) => (j === 0 ? '10.0.0.1/16' : 'fc00::1/64')}
|
|
/>
|
|
<StringList
|
|
name={['settings', 'dns']}
|
|
label="DNS"
|
|
placeholder={(j) => (j === 0 ? '1.1.1.1' : '8.8.8.8')}
|
|
/>
|
|
<FormField name={['settings', 'userLevel']} label={t('pages.xray.tun.userLevel')}>
|
|
<InputNumber min={0} />
|
|
</FormField>
|
|
<StringList
|
|
name={['settings', 'autoSystemRoutingTable']}
|
|
label={t('pages.inbounds.info.autoSystemRoutes')}
|
|
tooltip={t('pages.inbounds.form.autoSystemRoutesTooltip')}
|
|
placeholder={(j) => (j === 0 ? '0.0.0.0/0' : '::/0')}
|
|
/>
|
|
<FormField
|
|
name={['settings', 'autoOutboundsInterface']}
|
|
label={t('pages.inbounds.form.autoOutboundsInterface')}
|
|
tooltip={t('pages.inbounds.form.autoOutboundsInterfaceTooltip')}
|
|
>
|
|
<Input placeholder="auto" />
|
|
</FormField>
|
|
<FormField
|
|
name={['settings', 'autoSystemDnsToGateway']}
|
|
label={t('pages.inbounds.form.autoSystemDnsToGateway')}
|
|
tooltip={t('pages.inbounds.form.autoSystemDnsToGatewayTooltip')}
|
|
valueProp="checked"
|
|
>
|
|
<Switch id="tun-auto-dns-gateway" />
|
|
</FormField>
|
|
<FormField
|
|
name={['settings', 'autoSystemWfpBlockLeak']}
|
|
label={t('pages.inbounds.form.autoSystemWfpBlockLeak')}
|
|
tooltip={t('pages.inbounds.form.autoSystemWfpBlockLeakTooltip')}
|
|
>
|
|
<Select
|
|
id="tun-wfp-block-leak"
|
|
mode="tags"
|
|
tagRender={selectTagRender}
|
|
tokenSeparators={[',', ' ']}
|
|
allowClear
|
|
options={WFP_BLOCK_LEAK_OPTIONS}
|
|
/>
|
|
</FormField>
|
|
</>
|
|
);
|
|
}
|