mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +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:
@@ -38,7 +38,7 @@ export default function CustomSockoptList({
|
||||
<Button
|
||||
type="dashed"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
|
||||
>
|
||||
{t('pages.inbounds.form.addCustomOption')}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
@@ -271,6 +271,7 @@ export default function FinalMaskForm({
|
||||
showAll = false,
|
||||
side = 'client',
|
||||
}: FinalMaskFormProps) {
|
||||
const uid = useId();
|
||||
const base = asPath(name);
|
||||
|
||||
// Migrate legacy mask shapes once on mount so configs saved before #6334 (fragment
|
||||
@@ -346,8 +347,9 @@ export default function FinalMaskForm({
|
||||
)}
|
||||
{showQuic && (
|
||||
<>
|
||||
<Form.Item label="QUIC Params">
|
||||
<Form.Item label="QUIC Params" htmlFor={`${uid}-quic-params`}>
|
||||
<Switch
|
||||
id={`${uid}-quic-params`}
|
||||
checked={hasQuicParams}
|
||||
onChange={(v) => {
|
||||
form.setFieldValue([...base, 'quicParams'], v ? defaultQuicParams() : undefined);
|
||||
@@ -385,7 +387,7 @@ function TcpMasksList({
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
onClick={() =>
|
||||
add({ type: 'fragment', settings: defaultTcpMaskSettings('fragment') })
|
||||
@@ -531,12 +533,13 @@ function TcpMaskItem({
|
||||
rules={[{ required: true, message: 'Password is required' }]}
|
||||
>
|
||||
<Input
|
||||
aria-label="Password"
|
||||
placeholder="Obfuscation password"
|
||||
style={{ width: 'calc(100% - 32px)' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
aria-label={t('regenerate')}
|
||||
onClick={() =>
|
||||
form.setFieldValue(
|
||||
@@ -558,36 +561,47 @@ function TcpMaskItem({
|
||||
}
|
||||
|
||||
const FILTER_MASK_OPTION = { value: 'filter', label: 'Filter (source IPs)' };
|
||||
const FILTER_ALLOWED_HINT = 'Only these sources may connect; empty allows every source';
|
||||
const FILTER_BLOCKED_HINT = 'These sources are dropped before any other mask runs';
|
||||
|
||||
// xray-core 26.10.21 (#7206) screens a listener's source IPs; a blocked match wins over an allowed
|
||||
// one, and an empty list leaves that side open. Links and JSON subscriptions drop it.
|
||||
function FilterMaskSettings({ fieldName }: { fieldName: number }) {
|
||||
const uid = useId();
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Allowed Source IPs"
|
||||
name={[fieldName, 'settings', 'allowedSourceIPs']}
|
||||
tooltip="Only these sources may connect; empty allows every source"
|
||||
tooltip={FILTER_ALLOWED_HINT}
|
||||
>
|
||||
<Select
|
||||
aria-describedby={`${uid}-allowed-hint`}
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
tokenSeparators={[',', ' ']}
|
||||
placeholder="10.0.0.0/8, geoip:ir"
|
||||
/>
|
||||
</Form.Item>
|
||||
<span id={`${uid}-allowed-hint`} hidden>
|
||||
{FILTER_ALLOWED_HINT}
|
||||
</span>
|
||||
<Form.Item
|
||||
label="Blocked Source IPs"
|
||||
name={[fieldName, 'settings', 'blockedSourceIPs']}
|
||||
tooltip="These sources are dropped before any other mask runs"
|
||||
tooltip={FILTER_BLOCKED_HINT}
|
||||
>
|
||||
<Select
|
||||
aria-describedby={`${uid}-blocked-hint`}
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
tokenSeparators={[',', ' ']}
|
||||
placeholder="203.0.113.7, geoip:cn"
|
||||
/>
|
||||
</Form.Item>
|
||||
<span id={`${uid}-blocked-hint`} hidden>
|
||||
{FILTER_BLOCKED_HINT}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -683,7 +697,7 @@ function FragmentRangeList({
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => add('')}
|
||||
/>
|
||||
@@ -776,7 +790,7 @@ function XmcProfilesList({ tcpFieldName }: { tcpFieldName: number }) {
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => add(defaultXmcProfile())}
|
||||
/>
|
||||
@@ -846,6 +860,21 @@ const HEADER_CUSTOM_TCP_GROUPS = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
const UDP_MODE_HINT =
|
||||
'Prefix puts the header on every packet; Standalone exchanges it as separate handshake packets first';
|
||||
const NOISE_RESET_HINT = 'Seconds before the noise is sent to a destination again; 0 sends it once';
|
||||
const NOISE_SCRIPT_HINT =
|
||||
'A Lua file xray-core runs to build the noise, found beside the xray binary; replaces the noise list';
|
||||
const DOH_TLS_HINT =
|
||||
'tlsSettings for the https:// addresses, e.g. a serverName to front the domain';
|
||||
const DOH_QUIC_HINT = 'QUIC parameters for an https:// address reached over h3';
|
||||
const REUSE_HINT = 'Writes the bytes an earlier item captured under this name';
|
||||
const TRANSFORM_HINT =
|
||||
'An expression tree, e.g. {"op": "concat", "args": [{"type": "str", "bytes": "OK "}, {"reuse": "nonce"}]}';
|
||||
const CAPTURE_HINT = 'Saves the bytes this item writes or reads under a name later items can reuse';
|
||||
const UDP_HOP_HINT =
|
||||
'Advertised to clients as a hop range; the server listens on its own port only';
|
||||
|
||||
function HeaderCustomGroups({
|
||||
tcpFieldName,
|
||||
form,
|
||||
@@ -856,6 +885,7 @@ function HeaderCustomGroups({
|
||||
absoluteSettingsPath: (string | number)[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
return (
|
||||
<>
|
||||
{HEADER_CUSTOM_TCP_GROUPS.map(({ key: groupKey, label, tooltip }) => (
|
||||
@@ -866,10 +896,14 @@ function HeaderCustomGroups({
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
aria-describedby={`${uid}-${groupKey}-hint`}
|
||||
onClick={() => addGroup([defaultClientServerItem()])}
|
||||
/>
|
||||
<span id={`${uid}-${groupKey}-hint`} hidden>
|
||||
{tooltip}
|
||||
</span>
|
||||
</Form.Item>
|
||||
{groups.map((group, gi) => (
|
||||
<div key={group.key}>
|
||||
@@ -890,7 +924,7 @@ function HeaderCustomGroups({
|
||||
<Form.Item label="Items">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => addItem(defaultClientServerItem())}
|
||||
/>
|
||||
@@ -950,7 +984,7 @@ function UdpMasksList({
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => {
|
||||
const def = isHysteria || isWireguard ? 'salamander' : 'mkcp-legacy';
|
||||
@@ -1228,6 +1262,7 @@ function SalamanderUdpMaskSettings({
|
||||
absolutePath: (string | number)[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const packetSizePath = [...absolutePath, 'settings', 'packetSize'];
|
||||
const packetSize = Form.useWatch(packetSizePath, { form, preserve: true });
|
||||
const mode = typeof packetSize === 'string' && packetSize.trim() !== '' ? 'gecko' : 'salamander';
|
||||
@@ -1235,6 +1270,7 @@ function SalamanderUdpMaskSettings({
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-salamander-mode`}
|
||||
label="Mode"
|
||||
extra={
|
||||
mode === 'gecko'
|
||||
@@ -1243,6 +1279,7 @@ function SalamanderUdpMaskSettings({
|
||||
}
|
||||
>
|
||||
<Select
|
||||
id={`${uid}-salamander-mode`}
|
||||
value={mode}
|
||||
onChange={(next) => {
|
||||
if (next === 'gecko') {
|
||||
@@ -1274,10 +1311,14 @@ function SalamanderUdpMaskSettings({
|
||||
noStyle
|
||||
rules={[{ validator: validateSalamanderPassword }]}
|
||||
>
|
||||
<Input placeholder="Obfuscation password" style={{ width: 'calc(100% - 32px)' }} />
|
||||
<Input
|
||||
aria-label="Password"
|
||||
placeholder="Obfuscation password"
|
||||
style={{ width: 'calc(100% - 32px)' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
aria-label={t('regenerate')}
|
||||
onClick={() =>
|
||||
form.setFieldValue(
|
||||
@@ -1304,9 +1345,11 @@ function SalamanderUdpMaskSettings({
|
||||
}
|
||||
|
||||
function GeckoPacketSizeInput({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
id?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}) {
|
||||
@@ -1315,6 +1358,7 @@ function GeckoPacketSizeInput({
|
||||
return (
|
||||
<Space.Compact block>
|
||||
<InputNumber
|
||||
id={id}
|
||||
prefix="Min"
|
||||
min={GECKO_MIN_PACKET_SIZE}
|
||||
max={GECKO_MAX_PACKET_SIZE}
|
||||
@@ -1325,6 +1369,7 @@ function GeckoPacketSizeInput({
|
||||
style={{ width: '50%' }}
|
||||
/>
|
||||
<InputNumber
|
||||
aria-label="Packet size max"
|
||||
prefix="Max"
|
||||
min={GECKO_MIN_PACKET_SIZE}
|
||||
max={GECKO_MAX_PACKET_SIZE}
|
||||
@@ -1348,14 +1393,12 @@ function UdpHeaderCustom({
|
||||
absoluteSettingsPath: (string | number)[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Mode"
|
||||
name={[udpFieldName, 'settings', 'mode']}
|
||||
tooltip="Prefix puts the header on every packet; Standalone exchanges it as separate handshake packets first"
|
||||
>
|
||||
<Form.Item label="Mode" name={[udpFieldName, 'settings', 'mode']} tooltip={UDP_MODE_HINT}>
|
||||
<Select
|
||||
aria-describedby={`${uid}-mode-hint`}
|
||||
allowClear
|
||||
placeholder="Prefix"
|
||||
options={[
|
||||
@@ -1364,6 +1407,9 @@ function UdpHeaderCustom({
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<span id={`${uid}-mode-hint`} hidden>
|
||||
{UDP_MODE_HINT}
|
||||
</span>
|
||||
{(['client', 'server'] as const).map((groupKey) => (
|
||||
<Form.List key={groupKey} name={[udpFieldName, 'settings', groupKey]}>
|
||||
{(items, { add, remove }) => (
|
||||
@@ -1372,7 +1418,7 @@ function UdpHeaderCustom({
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => add(defaultUdpClientServerItem())}
|
||||
/>
|
||||
@@ -1418,6 +1464,7 @@ const XDNS_RECORD_TYPE_OPTIONS = [
|
||||
// Resolvers and extraPoll are read by clients only; a server keeps them for the share link.
|
||||
function XdnsSettings({ udpFieldName }: { udpFieldName: number }) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
return (
|
||||
<>
|
||||
<Form.List name={[udpFieldName, 'settings', 'domains']}>
|
||||
@@ -1427,7 +1474,7 @@ function XdnsSettings({ udpFieldName }: { udpFieldName: number }) {
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => add({ names: [], edns0: XDNS_LEGACY_EDNS0 })}
|
||||
/>
|
||||
@@ -1481,7 +1528,7 @@ function XdnsSettings({ udpFieldName }: { udpFieldName: number }) {
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => add({ addrs: [] })}
|
||||
/>
|
||||
@@ -1513,17 +1560,29 @@ function XdnsSettings({ udpFieldName }: { udpFieldName: number }) {
|
||||
<Form.Item
|
||||
label="DoH TLS"
|
||||
name={[resolver.name, 'tlsConfig']}
|
||||
tooltip="tlsSettings for the https:// addresses, e.g. a serverName to front the domain"
|
||||
tooltip={DOH_TLS_HINT}
|
||||
>
|
||||
<JsonObjectInput placeholder='{"serverName": "dns.google", "fingerprint": "chrome"}' />
|
||||
<JsonObjectInput
|
||||
aria-describedby={`${uid}-doh-tls-hint-${ri}`}
|
||||
placeholder='{"serverName": "dns.google", "fingerprint": "chrome"}'
|
||||
/>
|
||||
</Form.Item>
|
||||
<span id={`${uid}-doh-tls-hint-${ri}`} hidden>
|
||||
{DOH_TLS_HINT}
|
||||
</span>
|
||||
<Form.Item
|
||||
label="DoH QUIC"
|
||||
name={[resolver.name, 'quicConfig']}
|
||||
tooltip="QUIC parameters for an https:// address reached over h3"
|
||||
tooltip={DOH_QUIC_HINT}
|
||||
>
|
||||
<JsonObjectInput placeholder='{"congestion": "bbr"}' />
|
||||
<JsonObjectInput
|
||||
aria-describedby={`${uid}-doh-quic-hint-${ri}`}
|
||||
placeholder='{"congestion": "bbr"}'
|
||||
/>
|
||||
</Form.Item>
|
||||
<span id={`${uid}-doh-quic-hint-${ri}`} hidden>
|
||||
{DOH_QUIC_HINT}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@@ -1619,25 +1678,32 @@ function NoiseItems({
|
||||
form: FormInstance;
|
||||
absoluteSettingsPath: (string | number)[];
|
||||
}) {
|
||||
const uid = useId();
|
||||
const script = Form.useWatch([...absoluteSettingsPath, 'script'], form) as string | undefined;
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Reset"
|
||||
name={[udpFieldName, 'settings', 'reset']}
|
||||
tooltip="Seconds before the noise is sent to a destination again; 0 sends it once"
|
||||
tooltip={NOISE_RESET_HINT}
|
||||
rules={[{ validator: validateNoiseReset }]}
|
||||
>
|
||||
<Input placeholder="0 or 60-120" />
|
||||
<Input placeholder="0 or 60-120" aria-describedby={`${uid}-reset-hint`} />
|
||||
</Form.Item>
|
||||
<span id={`${uid}-reset-hint`} hidden>
|
||||
{NOISE_RESET_HINT}
|
||||
</span>
|
||||
<Form.Item
|
||||
label="Script"
|
||||
name={[udpFieldName, 'settings', 'script']}
|
||||
tooltip="A Lua file xray-core runs to build the noise, found beside the xray binary; replaces the noise list"
|
||||
tooltip={NOISE_SCRIPT_HINT}
|
||||
normalize={(value: string) => value || undefined}
|
||||
>
|
||||
<Input allowClear placeholder="noise.lua" />
|
||||
<Input allowClear placeholder="noise.lua" aria-describedby={`${uid}-script-hint`} />
|
||||
</Form.Item>
|
||||
<span id={`${uid}-script-hint`} hidden>
|
||||
{NOISE_SCRIPT_HINT}
|
||||
</span>
|
||||
{!script && (
|
||||
<NoiseItemList
|
||||
udpFieldName={udpFieldName}
|
||||
@@ -1668,7 +1734,7 @@ function NoiseItemList({
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => add(defaultNoiseItem())}
|
||||
/>
|
||||
@@ -1728,10 +1794,14 @@ function validateCustomVarName(_: unknown, value: unknown) {
|
||||
|
||||
// Hands the form only a parsed JSON object, so half-typed text never reaches the config.
|
||||
function JsonObjectInput({
|
||||
id,
|
||||
'aria-describedby': ariaDescribedBy,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
id?: string;
|
||||
'aria-describedby'?: string;
|
||||
value?: Record<string, unknown>;
|
||||
onChange?: (next: Record<string, unknown> | undefined) => void;
|
||||
placeholder?: string;
|
||||
@@ -1741,6 +1811,8 @@ function JsonObjectInput({
|
||||
return (
|
||||
<>
|
||||
<Input.TextArea
|
||||
id={id}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
value={text}
|
||||
autoSize={{ minRows: 2, maxRows: 8 }}
|
||||
placeholder={placeholder}
|
||||
@@ -1783,6 +1855,7 @@ function ItemEditor({
|
||||
onRemove?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const [kind, setKind] = useState<ItemKind>(() =>
|
||||
allowVariables ? itemKindOf(form.getFieldValue(absoluteItemPath)) : 'bytes',
|
||||
);
|
||||
@@ -1816,8 +1889,13 @@ function ItemEditor({
|
||||
return (
|
||||
<>
|
||||
{allowVariables && (
|
||||
<Form.Item label="Kind">
|
||||
<Select value={kind} onChange={onKindChange} options={ITEM_KIND_OPTIONS} />
|
||||
<Form.Item label="Kind" htmlFor={`${uid}-kind`}>
|
||||
<Select
|
||||
id={`${uid}-kind`}
|
||||
value={kind}
|
||||
onChange={onKindChange}
|
||||
options={ITEM_KIND_OPTIONS}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{kind === 'bytes' && (
|
||||
@@ -1848,27 +1926,40 @@ function ItemEditor({
|
||||
)}
|
||||
|
||||
{kind === 'reuse' && (
|
||||
<Form.Item
|
||||
label="Reuse"
|
||||
name={[fieldName, 'reuse']}
|
||||
tooltip="Writes the bytes an earlier item captured under this name"
|
||||
rules={[
|
||||
{ required: true, message: 'name the captured variable' },
|
||||
{ validator: validateCustomVarName },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="variable name" />
|
||||
</Form.Item>
|
||||
<>
|
||||
<Form.Item
|
||||
label="Reuse"
|
||||
name={[fieldName, 'reuse']}
|
||||
tooltip={REUSE_HINT}
|
||||
rules={[
|
||||
{ required: true, message: 'name the captured variable' },
|
||||
{ validator: validateCustomVarName },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="variable name" aria-describedby={`${uid}-reuse-hint`} />
|
||||
</Form.Item>
|
||||
<span id={`${uid}-reuse-hint`} hidden>
|
||||
{REUSE_HINT}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{kind === 'transform' && (
|
||||
<Form.Item
|
||||
label="Transform"
|
||||
name={[fieldName, 'transform']}
|
||||
tooltip='An expression tree, e.g. {"op": "concat", "args": [{"type": "str", "bytes": "OK "}, {"reuse": "nonce"}]}'
|
||||
rules={[{ required: true, message: 'enter the transform expression' }]}
|
||||
>
|
||||
<JsonObjectInput placeholder='{"op": "concat", "args": [{"reuse": "nonce"}]}' />
|
||||
</Form.Item>
|
||||
<>
|
||||
<Form.Item
|
||||
label="Transform"
|
||||
name={[fieldName, 'transform']}
|
||||
tooltip={TRANSFORM_HINT}
|
||||
rules={[{ required: true, message: 'enter the transform expression' }]}
|
||||
>
|
||||
<JsonObjectInput
|
||||
aria-describedby={`${uid}-transform-hint`}
|
||||
placeholder='{"op": "concat", "args": [{"reuse": "nonce"}]}'
|
||||
/>
|
||||
</Form.Item>
|
||||
<span id={`${uid}-transform-hint`} hidden>
|
||||
{TRANSFORM_HINT}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
@@ -1910,10 +2001,14 @@ function ItemEditor({
|
||||
<Form.Item label="Packet">
|
||||
<Space.Compact block>
|
||||
<Form.Item name={[fieldName, 'packet']} noStyle>
|
||||
<Input placeholder="binary data" style={{ width: 'calc(100% - 32px)' }} />
|
||||
<Input
|
||||
aria-label="Packet"
|
||||
placeholder="binary data"
|
||||
style={{ width: 'calc(100% - 32px)' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
aria-label={t('regenerate')}
|
||||
onClick={() =>
|
||||
form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())
|
||||
@@ -1932,15 +2027,20 @@ function ItemEditor({
|
||||
</Form.Item>
|
||||
|
||||
{allowVariables && (
|
||||
<Form.Item
|
||||
label="Capture"
|
||||
name={[fieldName, 'capture']}
|
||||
tooltip="Saves the bytes this item writes or reads under a name later items can reuse"
|
||||
normalize={(v) => (v === '' ? undefined : v)}
|
||||
rules={[{ validator: validateCustomVarName }]}
|
||||
>
|
||||
<Input placeholder="variable name" />
|
||||
</Form.Item>
|
||||
<>
|
||||
<Form.Item
|
||||
label="Capture"
|
||||
name={[fieldName, 'capture']}
|
||||
tooltip={CAPTURE_HINT}
|
||||
normalize={(v) => (v === '' ? undefined : v)}
|
||||
rules={[{ validator: validateCustomVarName }]}
|
||||
>
|
||||
<Input placeholder="variable name" aria-describedby={`${uid}-capture-hint`} />
|
||||
</Form.Item>
|
||||
<span id={`${uid}-capture-hint`} hidden>
|
||||
{CAPTURE_HINT}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -1955,6 +2055,7 @@ function QuicParamsForm({
|
||||
form: FormInstance;
|
||||
showUdpHop: boolean;
|
||||
}) {
|
||||
const uid = useId();
|
||||
const congestion = Form.useWatch([...base, 'congestion'], form) as string | undefined;
|
||||
const udpHop = Form.useWatch([...base, 'udpHop'], { form, preserve: true }) as
|
||||
| Record<string, unknown>
|
||||
@@ -2012,16 +2113,18 @@ function QuicParamsForm({
|
||||
itself listens on its own port. */}
|
||||
{showUdpHop && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="UDP Hop"
|
||||
tooltip="Advertised to clients as a hop range; the server listens on its own port only"
|
||||
>
|
||||
<Form.Item htmlFor={`${uid}-udp-hop`} label="UDP Hop" tooltip={UDP_HOP_HINT}>
|
||||
<Switch
|
||||
id={`${uid}-udp-hop`}
|
||||
aria-describedby={`${uid}-udp-hop-hint`}
|
||||
checked={hasUdpHop}
|
||||
onChange={(v) => {
|
||||
form.setFieldValue([...base, 'udpHop'], v ? defaultUdpHop() : undefined);
|
||||
}}
|
||||
/>
|
||||
<span id={`${uid}-udp-hop-hint`} hidden>
|
||||
{UDP_HOP_HINT}
|
||||
</span>
|
||||
</Form.Item>
|
||||
{hasUdpHop && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user