mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-29 01:04:59 +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:
@@ -754,9 +754,11 @@ export default function InboundsPage() {
|
||||
value={0}
|
||||
formatter={() => (
|
||||
<span>
|
||||
<ArrowUpOutlined /> {SizeFormatter.sizeFormat(totals.up)}
|
||||
<ArrowUpOutlined aria-hidden />{' '}
|
||||
{SizeFormatter.sizeFormat(totals.up)}
|
||||
{' / '}
|
||||
<ArrowDownOutlined /> {SizeFormatter.sizeFormat(totals.down)}
|
||||
<ArrowDownOutlined aria-hidden />{' '}
|
||||
{SizeFormatter.sizeFormat(totals.down)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
@@ -765,14 +767,14 @@ export default function InboundsPage() {
|
||||
<Statistic
|
||||
title={t('pages.inbounds.totalUsage')}
|
||||
value={SizeFormatter.sizeFormat(totals.up + totals.down)}
|
||||
prefix={<PieChartOutlined />}
|
||||
prefix={<PieChartOutlined aria-hidden />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
<Statistic
|
||||
title={t('pages.inbounds.inboundCount')}
|
||||
value={String(dbInbounds.length)}
|
||||
prefix={<BarsOutlined />}
|
||||
prefix={<BarsOutlined aria-hidden />}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -32,7 +32,13 @@ export default function FallbacksCard({
|
||||
|
||||
const addButtons = (
|
||||
<Space size={8} wrap>
|
||||
<Button type="primary" ghost size="small" icon={<PlusOutlined />} onClick={addFallback}>
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
onClick={addFallback}
|
||||
>
|
||||
{t('pages.inbounds.fallbacks.add') || 'Add fallback'}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -90,25 +96,26 @@ export default function FallbacksCard({
|
||||
disabled={idx === 0}
|
||||
onClick={() => moveFallback(idx, -1)}
|
||||
title={t('pages.inbounds.form.moveUp')}
|
||||
icon={<ArrowUpOutlined />}
|
||||
icon={<ArrowUpOutlined aria-hidden />}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('pages.inbounds.form.moveDown')}
|
||||
disabled={idx === fallbacks.length - 1}
|
||||
onClick={() => moveFallback(idx, 1)}
|
||||
title={t('pages.inbounds.form.moveDown')}
|
||||
icon={<ArrowDownOutlined />}
|
||||
icon={<ArrowDownOutlined aria-hidden />}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('delete')}
|
||||
danger
|
||||
onClick={() => removeFallback(idx)}
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteOutlined aria-hidden />}
|
||||
/>
|
||||
</Space.Compact>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Input
|
||||
aria-label="SNI"
|
||||
prefix="SNI"
|
||||
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
|
||||
value={record.name}
|
||||
@@ -117,6 +124,7 @@ export default function FallbacksCard({
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Input
|
||||
aria-label="ALPN"
|
||||
prefix="ALPN"
|
||||
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
|
||||
value={record.alpn}
|
||||
@@ -125,6 +133,7 @@ export default function FallbacksCard({
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Input
|
||||
aria-label="Path"
|
||||
prefix="Path"
|
||||
placeholder="/"
|
||||
value={record.path}
|
||||
@@ -133,6 +142,7 @@ export default function FallbacksCard({
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Input
|
||||
aria-label="Dest"
|
||||
prefix="Dest"
|
||||
placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
|
||||
value={record.dest}
|
||||
@@ -141,6 +151,7 @@ export default function FallbacksCard({
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<InputNumber
|
||||
aria-label="xver"
|
||||
prefix="xver"
|
||||
min={0}
|
||||
max={2}
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Alert,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Radio,
|
||||
Select,
|
||||
Switch,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { Alert, Form, Input, InputNumber, Modal, Radio, Select, Switch, Tabs, message } from 'antd';
|
||||
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
|
||||
@@ -99,16 +87,6 @@ import SniffingTab from './SniffingTab';
|
||||
import type { DBInbound } from '@/models/dbinbound';
|
||||
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||
|
||||
/* Render a field label with a hover tooltip icon instead of an `extra` help line below. */
|
||||
const labelWithHint = (label: string, hint: string) => (
|
||||
<span>
|
||||
{label}
|
||||
<Tooltip title={hint}>
|
||||
<QuestionCircleOutlined style={{ marginInlineStart: 4, color: 'rgba(128,128,128,0.65)' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
|
||||
const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
|
||||
const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
|
||||
const SHARE_ADDR_HOSTNAME_RE =
|
||||
@@ -240,6 +218,7 @@ export default function InboundFormModal({
|
||||
availableNodesFetched = true,
|
||||
}: InboundFormModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const methods = useForm<InboundFormValues>({ defaultValues: buildAddModeValues() });
|
||||
@@ -668,7 +647,8 @@ export default function InboundFormModal({
|
||||
|
||||
<FormField
|
||||
name="listen"
|
||||
label={labelWithHint(t('pages.inbounds.address'), t('pages.inbounds.form.listenHelp'))}
|
||||
label={t('pages.inbounds.address')}
|
||||
tooltip={t('pages.inbounds.form.listenHelp')}
|
||||
>
|
||||
<Input placeholder={t('pages.inbounds.monitorDesc')} />
|
||||
</FormField>
|
||||
@@ -677,10 +657,8 @@ export default function InboundFormModal({
|
||||
<>
|
||||
<FormField
|
||||
name="shareAddrStrategy"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.shareAddrStrategy'),
|
||||
t('pages.inbounds.form.shareAddrStrategyHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.shareAddrStrategy')}
|
||||
tooltip={t('pages.inbounds.form.shareAddrStrategyHelp')}
|
||||
>
|
||||
<Select
|
||||
options={SHARE_ADDR_STRATEGIES.filter(
|
||||
@@ -695,10 +673,8 @@ export default function InboundFormModal({
|
||||
{shareAddrStrategy === 'custom' && (
|
||||
<FormField
|
||||
name="shareAddr"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.shareAddr'),
|
||||
t('pages.inbounds.form.shareAddrHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.shareAddr')}
|
||||
tooltip={t('pages.inbounds.form.shareAddrHelp')}
|
||||
rules={{
|
||||
validate: (value) =>
|
||||
isValidShareAddrInput(String(value ?? '')) ||
|
||||
@@ -713,10 +689,8 @@ export default function InboundFormModal({
|
||||
|
||||
<FormField
|
||||
name="subSortIndex"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.subSortIndex'),
|
||||
t('pages.inbounds.form.subSortIndexHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.subSortIndex')}
|
||||
tooltip={t('pages.inbounds.form.subSortIndexHelp')}
|
||||
>
|
||||
<InputNumber />
|
||||
</FormField>
|
||||
@@ -724,10 +698,8 @@ export default function InboundFormModal({
|
||||
<FormField
|
||||
name="excludeFromSub"
|
||||
valueProp="checked"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.excludeFromSub'),
|
||||
t('pages.inbounds.form.excludeFromSubHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.excludeFromSub')}
|
||||
tooltip={t('pages.inbounds.form.excludeFromSubHelp')}
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
@@ -736,10 +708,8 @@ export default function InboundFormModal({
|
||||
<FormField
|
||||
name="disableFlow"
|
||||
valueProp="checked"
|
||||
label={labelWithHint(
|
||||
t('pages.inbounds.form.disableFlow'),
|
||||
t('pages.inbounds.form.disableFlowHelp'),
|
||||
)}
|
||||
label={t('pages.inbounds.form.disableFlow')}
|
||||
tooltip={t('pages.inbounds.form.disableFlowHelp')}
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
@@ -754,13 +724,13 @@ export default function InboundFormModal({
|
||||
</FormField>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title={t('pages.inbounds.meansNoLimit')}>
|
||||
{t('pages.inbounds.totalFlow')}
|
||||
</Tooltip>
|
||||
}
|
||||
htmlFor={`${uid}-totalFlow`}
|
||||
label={t('pages.inbounds.totalFlow')}
|
||||
tooltip={t('pages.inbounds.meansNoLimit')}
|
||||
>
|
||||
<InputNumber
|
||||
id={`${uid}-totalFlow`}
|
||||
aria-describedby={`${uid}-totalFlow-hint`}
|
||||
value={wTotal ? Math.round((wTotal / SizeFormatter.ONE_GB) * 100) / 100 : 0}
|
||||
min={0}
|
||||
step={1}
|
||||
@@ -769,6 +739,9 @@ export default function InboundFormModal({
|
||||
setV('total', bytes);
|
||||
}}
|
||||
/>
|
||||
<span id={`${uid}-totalFlow-hint`} hidden>
|
||||
{t('pages.inbounds.meansNoLimit')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
|
||||
<FormField name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
|
||||
@@ -791,16 +764,19 @@ export default function InboundFormModal({
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>
|
||||
{t('pages.inbounds.expireDate')}
|
||||
</Tooltip>
|
||||
}
|
||||
htmlFor={`${uid}-expiryTime`}
|
||||
label={t('pages.inbounds.expireDate')}
|
||||
tooltip={t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||
>
|
||||
<DateTimePicker
|
||||
id={`${uid}-expiryTime`}
|
||||
aria-describedby={`${uid}-expiryTime-hint`}
|
||||
value={wExpiry > 0 ? dayjs(wExpiry) : null}
|
||||
onChange={(d) => setV('expiryTime', d ? d.valueOf() : 0)}
|
||||
/>
|
||||
<span id={`${uid}-expiryTime-hint`} hidden>
|
||||
{t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
@@ -936,8 +912,9 @@ export default function InboundFormModal({
|
||||
const streamTab = (
|
||||
<>
|
||||
{hasSelectableTransport && (
|
||||
<Form.Item label={t('transmission')}>
|
||||
<Form.Item htmlFor={`${uid}-network`} label={t('transmission')}>
|
||||
<Select
|
||||
id={`${uid}-network`}
|
||||
style={{ width: '75%' }}
|
||||
value={network}
|
||||
onChange={onNetworkChange}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function AccountsList() {
|
||||
})
|
||||
}
|
||||
>
|
||||
<PlusOutlined /> {t('add')}
|
||||
<PlusOutlined aria-hidden /> {t('add')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
{fields.length > 0 && (
|
||||
@@ -32,13 +32,13 @@ export default function AccountsList() {
|
||||
<Space.Compact key={field.id} className="mb-8" block>
|
||||
<InputAddon>{String(idx + 1)}</InputAddon>
|
||||
<FormField name={['settings', 'accounts', idx, 'user']} noStyle>
|
||||
<Input placeholder={t('username')} />
|
||||
<Input aria-label={`${t('username')} ${idx + 1}`} placeholder={t('username')} />
|
||||
</FormField>
|
||||
<FormField name={['settings', 'accounts', idx, 'pass']} noStyle>
|
||||
<Input placeholder={t('password')} />
|
||||
<Input aria-label={`${t('password')} ${idx + 1}`} placeholder={t('password')} />
|
||||
</FormField>
|
||||
<Button aria-label={t('remove')} onClick={() => remove(idx)}>
|
||||
<MinusOutlined />
|
||||
<MinusOutlined aria-hidden />
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
))}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
@@ -16,22 +17,23 @@ export default function AmneziawgFields({
|
||||
regenInboundAwgObfuscation,
|
||||
}: AmneziawgFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
return (
|
||||
<>
|
||||
<Form.Item label={t('pages.xray.amneziawg.privateKey')}>
|
||||
<Form.Item htmlFor={`${uid}-privateKey`} label={t('pages.xray.amneziawg.privateKey')}>
|
||||
<Space.Compact block>
|
||||
<FormField name={['settings', 'server', 'privateKey']} noStyle>
|
||||
<Input style={{ width: 'calc(100% - 32px)' }} />
|
||||
<Input id={`${uid}-privateKey`} style={{ width: 'calc(100% - 32px)' }} />
|
||||
</FormField>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={regenInboundAwg}
|
||||
/>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.xray.amneziawg.publicKey')}>
|
||||
<Input value={awgPubKey} disabled />
|
||||
<Form.Item htmlFor={`${uid}-publicKey`} label={t('pages.xray.amneziawg.publicKey')}>
|
||||
<Input id={`${uid}-publicKey`} value={awgPubKey} disabled />
|
||||
</Form.Item>
|
||||
<FormField
|
||||
name={['settings', 'server', 'subnetIp']}
|
||||
@@ -89,7 +91,7 @@ export default function AmneziawgFields({
|
||||
<Input placeholder="eth0" />
|
||||
</FormField>
|
||||
<Form.Item label={t('pages.xray.amneziawg.obfuscation')}>
|
||||
<Button icon={<ReloadOutlined />} onClick={regenInboundAwgObfuscation}>
|
||||
<Button icon={<ReloadOutlined aria-hidden />} onClick={regenInboundAwgObfuscation}>
|
||||
{t('pages.xray.amneziawg.regenerateObfuscation')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, InputNumber, Select, Switch } from 'antd';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
@@ -9,6 +10,7 @@ const MASQ_PATH = ['streamSettings', 'hysteriaSettings', 'masquerade'];
|
||||
|
||||
export default function HysteriaFields() {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const { control, setValue } = useFormContext();
|
||||
const masq = useWatch({ control, name: 'streamSettings.hysteriaSettings.masquerade' }) as
|
||||
| { type?: string }
|
||||
@@ -32,8 +34,9 @@ export default function HysteriaFields() {
|
||||
<InputNumber min={2} max={600} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
|
||||
<Form.Item label={t('pages.inbounds.form.masquerade')}>
|
||||
<Form.Item htmlFor={`${uid}-masquerade`} label={t('pages.inbounds.form.masquerade')}>
|
||||
<Switch
|
||||
id={`${uid}-masquerade`}
|
||||
checked={!!masq}
|
||||
onChange={(checked) =>
|
||||
setValue(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, Select, Space, Switch } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
@@ -13,6 +14,7 @@ interface ShadowsocksFieldsProps {
|
||||
|
||||
export default function ShadowsocksFields({ isSSWith2022 }: ShadowsocksFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const { getValues, setValue } = useFormContext();
|
||||
return (
|
||||
<>
|
||||
@@ -26,14 +28,14 @@ export default function ShadowsocksFields({ isSSWith2022 }: ShadowsocksFieldsPro
|
||||
<Select options={SSInboundMethodSchema.options.map((m) => ({ value: m, label: m }))} />
|
||||
</FormField>
|
||||
{isSSWith2022 && (
|
||||
<Form.Item label={t('password')}>
|
||||
<Form.Item htmlFor={`${uid}-password`} label={t('password')}>
|
||||
<Space.Compact block>
|
||||
<FormField name={['settings', 'password']} noStyle>
|
||||
<Input style={{ width: 'calc(100% - 32px)' }} />
|
||||
<Input id={`${uid}-password`} style={{ width: 'calc(100% - 32px)' }} />
|
||||
</FormField>
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined />}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={() => {
|
||||
const method = getValues('settings.method');
|
||||
setValue(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AutoComplete,
|
||||
@@ -21,6 +21,7 @@ import { HttpUtil } from '@/utils';
|
||||
|
||||
export default function TuicFields() {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const { control, setValue } = useFormContext();
|
||||
const [loadingPanelCert, setLoadingPanelCert] = useState(false);
|
||||
@@ -147,8 +148,9 @@ export default function TuicFields() {
|
||||
return (
|
||||
<>
|
||||
{messageContextHolder}
|
||||
<Form.Item label={t('pages.inbounds.publicKey')}>
|
||||
<Form.Item htmlFor={`${uid}-certificate`} label={t('pages.inbounds.publicKey')}>
|
||||
<AutoComplete
|
||||
id={`${uid}-certificate`}
|
||||
value={certificate}
|
||||
options={certOptions}
|
||||
onChange={(v) => setValue('settings.server.certificate', v)}
|
||||
@@ -156,8 +158,9 @@ export default function TuicFields() {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('pages.inbounds.privatekey')}>
|
||||
<Form.Item htmlFor={`${uid}-privateKey`} label={t('pages.inbounds.privatekey')}>
|
||||
<AutoComplete
|
||||
id={`${uid}-privateKey`}
|
||||
value={privateKey}
|
||||
options={keyOptions}
|
||||
onChange={(v) => setValue('settings.server.private_key', v)}
|
||||
@@ -169,13 +172,13 @@ export default function TuicFields() {
|
||||
<Space wrap style={{ display: 'flex', flexWrap: 'wrap', width: '100%' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CloudDownloadOutlined />}
|
||||
icon={<CloudDownloadOutlined aria-hidden />}
|
||||
loading={loadingPanelCert}
|
||||
onClick={setCertFromPanel}
|
||||
>
|
||||
{t('pages.inbounds.setDefaultCert')}
|
||||
</Button>
|
||||
<Button icon={<SyncOutlined />} onClick={autofillFromSni}>
|
||||
<Button icon={<SyncOutlined aria-hidden />} onClick={autofillFromSni}>
|
||||
{t('pages.inbounds.form.autoFill')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -217,12 +220,21 @@ export default function TuicFields() {
|
||||
|
||||
<Divider titlePlacement="start">{t('pages.xray.tuic.profileOptions')}</Divider>
|
||||
|
||||
<Form.Item label={t('pages.xray.tuic.sni')} tooltip={t('pages.xray.tuic.sniHint')}>
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-sni`}
|
||||
label={t('pages.xray.tuic.sni')}
|
||||
tooltip={t('pages.xray.tuic.sniHint')}
|
||||
>
|
||||
<Input
|
||||
id={`${uid}-sni`}
|
||||
aria-describedby={`${uid}-sni-hint`}
|
||||
value={sni}
|
||||
placeholder="example.com"
|
||||
onChange={(e) => handleSniChange(e.target.value)}
|
||||
/>
|
||||
<span id={`${uid}-sni-hint`} hidden>
|
||||
{t('pages.xray.tuic.sniHint')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
|
||||
<FormField
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useId, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, InputNumber, Select, Space, Switch, Tooltip } from 'antd';
|
||||
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
|
||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
@@ -9,28 +9,39 @@ import { FormField } from '@/components/form/rhf';
|
||||
interface StringListProps {
|
||||
name: string[];
|
||||
label: ReactNode;
|
||||
tooltip?: string;
|
||||
placeholder: (index: number) => string;
|
||||
}
|
||||
|
||||
function StringList({ name, label, placeholder }: StringListProps) {
|
||||
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={label}>
|
||||
<Form.Item label={<span id={labelId}>{label}</span>} tooltip={tooltip}>
|
||||
<Button aria-label={t('add')} size="small" onClick={() => append('')}>
|
||||
<PlusOutlined />
|
||||
<PlusOutlined aria-hidden />
|
||||
</Button>
|
||||
{fields.map((field, j) => (
|
||||
<Space.Compact key={field.id} block className="mt-4">
|
||||
<FormField name={[...name, j]} noStyle>
|
||||
<Input placeholder={placeholder(j)} />
|
||||
<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 />
|
||||
<MinusOutlined aria-hidden />
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
))}
|
||||
{tooltip && (
|
||||
<span id={`${labelId}-hint`} hidden>
|
||||
{tooltip}
|
||||
</span>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
@@ -73,20 +84,14 @@ export default function TunFields() {
|
||||
</FormField>
|
||||
<StringList
|
||||
name={['settings', 'autoSystemRoutingTable']}
|
||||
label={
|
||||
<Tooltip title={t('pages.inbounds.form.autoSystemRoutesTooltip')}>
|
||||
{t('pages.inbounds.info.autoSystemRoutes')}
|
||||
</Tooltip>
|
||||
}
|
||||
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={
|
||||
<Tooltip title={t('pages.inbounds.form.autoOutboundsInterfaceTooltip')}>
|
||||
{t('pages.inbounds.form.autoOutboundsInterface')}
|
||||
</Tooltip>
|
||||
}
|
||||
label={t('pages.inbounds.form.autoOutboundsInterface')}
|
||||
tooltip={t('pages.inbounds.form.autoOutboundsInterfaceTooltip')}
|
||||
>
|
||||
<Input placeholder="auto" />
|
||||
</FormField>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
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';
|
||||
@@ -26,6 +26,7 @@ export default function VlessFields({
|
||||
clearVlessEnc,
|
||||
}: VlessFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const { control } = useFormContext();
|
||||
const [authKind, setAuthKind] = useState<VlessAuthKind>(vlessAuthKind ?? 'x25519');
|
||||
|
||||
@@ -47,9 +48,10 @@ export default function VlessFields({
|
||||
<FormField name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
|
||||
<Input />
|
||||
</FormField>
|
||||
<Form.Item label={t('pages.inbounds.vlessAuthGenerate')}>
|
||||
<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}
|
||||
@@ -80,6 +82,7 @@ export default function VlessFields({
|
||||
defaultValue={def}
|
||||
render={({ field }) => (
|
||||
<InputNumber
|
||||
aria-label={`${t('pages.inbounds.form.visionTestseed')} ${i + 1}`}
|
||||
min={1}
|
||||
style={{ width: '25%' }}
|
||||
value={field.value as number}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
@@ -11,18 +12,23 @@ interface WireguardFieldsProps {
|
||||
|
||||
export default function WireguardFields({ wgPubKey, regenInboundWg }: WireguardFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
return (
|
||||
<>
|
||||
<Form.Item label={t('pages.xray.wireguard.secretKey')}>
|
||||
<Form.Item htmlFor={`${uid}-secretKey`} label={t('pages.xray.wireguard.secretKey')}>
|
||||
<Space.Compact block>
|
||||
<FormField name={['settings', 'secretKey']} noStyle>
|
||||
<Input style={{ width: 'calc(100% - 32px)' }} />
|
||||
<Input id={`${uid}-secretKey`} style={{ width: 'calc(100% - 32px)' }} />
|
||||
</FormField>
|
||||
<Button aria-label={t('regenerate')} icon={<ReloadOutlined />} onClick={regenInboundWg} />
|
||||
<Button
|
||||
aria-label={t('regenerate')}
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
onClick={regenInboundWg}
|
||||
/>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.xray.wireguard.publicKey')}>
|
||||
<Input value={wgPubKey} disabled />
|
||||
<Form.Item htmlFor={`${uid}-publicKey`} label={t('pages.xray.wireguard.publicKey')}>
|
||||
<Input id={`${uid}-publicKey`} value={wgPubKey} disabled />
|
||||
</Form.Item>
|
||||
<FormField name={['settings', 'subnetIp']} label={t('pages.xray.wireguard.subnetIp')}>
|
||||
<Input placeholder="10.0.0.0" />
|
||||
|
||||
@@ -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']}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Switch } from 'antd';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
@@ -7,6 +8,7 @@ import { FormField } from '@/components/form/rhf';
|
||||
|
||||
export default function RawForm() {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const { control, setValue } = useFormContext();
|
||||
const headerType = (useWatch({
|
||||
control,
|
||||
@@ -21,8 +23,9 @@ export default function RawForm() {
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
<Form.Item label={`HTTP ${t('camouflage')}`}>
|
||||
<Form.Item htmlFor={`${uid}-httpCamouflage`} label={`HTTP ${t('camouflage')}`}>
|
||||
<Switch
|
||||
id={`${uid}-httpCamouflage`}
|
||||
checked={headerType === 'http'}
|
||||
onChange={(v) => {
|
||||
setValue(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Form, InputNumber, Segmented, Select, Switch } from 'antd';
|
||||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||
@@ -28,6 +29,7 @@ export default function SockoptForm({
|
||||
network: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId();
|
||||
const { control, getValues, setValue } = useFormContext();
|
||||
const sock = useWatch({ control, name: 'streamSettings.sockopt' });
|
||||
const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
|
||||
@@ -101,6 +103,8 @@ export default function SockoptForm({
|
||||
tooltip={t('pages.inbounds.form.realClientIpHint')}
|
||||
>
|
||||
<Segmented
|
||||
aria-label={t('pages.inbounds.form.realClientIp')}
|
||||
aria-describedby={`${uid}-real-client-ip-hint`}
|
||||
value={presetValue}
|
||||
onChange={(v) => applyRealClientIpPreset(v as RealClientIpPreset)}
|
||||
options={[
|
||||
@@ -112,6 +116,9 @@ export default function SockoptForm({
|
||||
{ value: 'proxy', label: t('pages.inbounds.form.realClientIpPresetProxyProtocol') },
|
||||
]}
|
||||
/>
|
||||
<span id={`${uid}-real-client-ip-hint`} hidden>
|
||||
{t('pages.inbounds.form.realClientIpHint')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
{trustedMismatch && (
|
||||
<Alert
|
||||
|
||||
@@ -533,7 +533,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(clientSettings.tgId, t)}
|
||||
/>
|
||||
@@ -552,7 +552,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(link.link, t)}
|
||||
/>
|
||||
@@ -573,7 +573,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(subLink, t)}
|
||||
/>
|
||||
@@ -595,7 +595,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(subJsonLink, t)}
|
||||
/>
|
||||
@@ -714,7 +714,7 @@ export default function InboundInfoModal({
|
||||
<Button
|
||||
size="small"
|
||||
className="value-copy"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(encryptionLabel, t)}
|
||||
/>
|
||||
@@ -984,7 +984,7 @@ export default function InboundInfoModal({
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(`${account.user}:${account.pass}`, t)}
|
||||
/>
|
||||
@@ -1100,7 +1100,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(`${account.user}:${account.pass}`, t)}
|
||||
/>
|
||||
@@ -1201,7 +1201,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(cfg, t)}
|
||||
/>
|
||||
@@ -1209,7 +1209,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
icon={<DownloadOutlined aria-hidden />}
|
||||
aria-label={t('download')}
|
||||
onClick={() =>
|
||||
downloadText(cfg, peerConfFileName(idx, j, all.length))
|
||||
@@ -1230,7 +1230,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(link, t)}
|
||||
/>
|
||||
@@ -1261,7 +1261,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(cfg, t)}
|
||||
/>
|
||||
@@ -1269,7 +1269,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
icon={<DownloadOutlined aria-hidden />}
|
||||
aria-label={t('download')}
|
||||
onClick={() => downloadText(cfg, peerConfFileName(idx, j, all.length))}
|
||||
/>
|
||||
@@ -1288,7 +1288,7 @@ export default function InboundInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(link, t)}
|
||||
/>
|
||||
|
||||
@@ -193,20 +193,24 @@ export default function InboundList({
|
||||
|
||||
const generalActionsMenu: MenuProps = {
|
||||
items: [
|
||||
{ key: 'import', icon: <ImportOutlined />, label: t('pages.inbounds.importInbound') },
|
||||
{ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') },
|
||||
{
|
||||
key: 'import',
|
||||
icon: <ImportOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.importInbound'),
|
||||
},
|
||||
{ key: 'export', icon: <ExportOutlined aria-hidden />, label: t('pages.inbounds.export') },
|
||||
...(subEnable
|
||||
? [
|
||||
{
|
||||
key: 'subs',
|
||||
icon: <ExportOutlined />,
|
||||
icon: <ExportOutlined aria-hidden />,
|
||||
label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'resetInbounds',
|
||||
icon: <ReloadOutlined />,
|
||||
icon: <ReloadOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.resetAllTraffic'),
|
||||
},
|
||||
],
|
||||
@@ -221,7 +225,7 @@ export default function InboundList({
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onAddInbound}
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusOutlined aria-hidden />}
|
||||
aria-label={t('pages.inbounds.addInbound')}
|
||||
>
|
||||
{!isMobile && t('pages.inbounds.addInbound')}
|
||||
@@ -229,7 +233,7 @@ export default function InboundList({
|
||||
<Dropdown trigger={['click']} menu={generalActionsMenu}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<MenuOutlined />}
|
||||
icon={<MenuOutlined aria-hidden />}
|
||||
aria-label={t('pages.inbounds.generalActions')}
|
||||
>
|
||||
{!isMobile && t('pages.inbounds.generalActions')}
|
||||
@@ -251,7 +255,7 @@ export default function InboundList({
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder={t('search')}
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
prefix={<SearchOutlined aria-hidden />}
|
||||
style={{ maxWidth: isMobile ? 110 : 200 }}
|
||||
aria-label={t('search')}
|
||||
/>
|
||||
@@ -267,7 +271,7 @@ export default function InboundList({
|
||||
</Tag>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteOutlined aria-hidden />}
|
||||
onClick={handleBulkDelete}
|
||||
aria-label={t('delete')}
|
||||
>
|
||||
@@ -283,7 +287,7 @@ export default function InboundList({
|
||||
<div className="inbound-cards">
|
||||
{visibleInbounds.length === 0 ? (
|
||||
<div className="card-empty">
|
||||
<ImportOutlined style={{ fontSize: 28, opacity: 0.5 }} />
|
||||
<ImportOutlined aria-hidden style={{ fontSize: 28, opacity: 0.5 }} />
|
||||
<div>{t('noData')}</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -327,6 +331,7 @@ export default function InboundList({
|
||||
/>
|
||||
</Tooltip>
|
||||
<Switch
|
||||
aria-label={t('pages.inbounds.enable')}
|
||||
checked={record.enable}
|
||||
size="small"
|
||||
onChange={(next) => onSwitchEnable(record, next)}
|
||||
@@ -350,7 +355,7 @@ export default function InboundList({
|
||||
type="text"
|
||||
size="small"
|
||||
className="row-action-trigger"
|
||||
icon={<MoreOutlined />}
|
||||
icon={<MoreOutlined aria-hidden />}
|
||||
aria-label={t('more')}
|
||||
/>
|
||||
</Dropdown>
|
||||
@@ -377,7 +382,7 @@ export default function InboundList({
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div className="card-empty">
|
||||
<ImportOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
||||
<ImportOutlined aria-hidden style={{ fontSize: 32, marginBottom: 8 }} />
|
||||
<div>{t('noData')}</div>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -40,69 +40,82 @@ export function buildRowActionsMenu({
|
||||
}): MenuProps['items'] {
|
||||
const items: MenuProps['items'] = [];
|
||||
if (isMobile) {
|
||||
items.push({ key: 'edit', icon: <EditOutlined />, label: t('edit') });
|
||||
items.push({ key: 'edit', icon: <EditOutlined aria-hidden />, label: t('edit') });
|
||||
}
|
||||
if (isInboundMultiUser(record)) {
|
||||
items.push({ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') });
|
||||
items.push({
|
||||
key: 'export',
|
||||
icon: <ExportOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.export'),
|
||||
});
|
||||
if (subEnable) {
|
||||
items.push({
|
||||
key: 'subs',
|
||||
icon: <ExportOutlined />,
|
||||
icon: <ExportOutlined aria-hidden />,
|
||||
label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
key: 'showInfo',
|
||||
icon: <InfoCircleOutlined />,
|
||||
icon: <InfoCircleOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.inboundInfo'),
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
key: 'clipboard',
|
||||
icon: <CopyOutlined />,
|
||||
icon: <CopyOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.exportInbound'),
|
||||
});
|
||||
items.push({
|
||||
key: 'resetTraffic',
|
||||
icon: <RetweetOutlined />,
|
||||
icon: <RetweetOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.resetTraffic'),
|
||||
});
|
||||
items.push({ key: 'clone', icon: <BlockOutlined />, label: t('pages.inbounds.clone') });
|
||||
items.push({
|
||||
key: 'clone',
|
||||
icon: <BlockOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.clone'),
|
||||
});
|
||||
if (isInboundMultiUser(record)) {
|
||||
items.push({
|
||||
key: 'attachExisting',
|
||||
icon: <UsergroupAddOutlined />,
|
||||
icon: <UsergroupAddOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.attachExistingClients'),
|
||||
});
|
||||
}
|
||||
if (isInboundMultiUser(record) && hasClients) {
|
||||
items.push({
|
||||
key: 'attachClients',
|
||||
icon: <UsergroupAddOutlined />,
|
||||
icon: <UsergroupAddOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.attachClients'),
|
||||
});
|
||||
items.push({
|
||||
key: 'detachClients',
|
||||
icon: <UsergroupDeleteOutlined />,
|
||||
icon: <UsergroupDeleteOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.detachClients'),
|
||||
});
|
||||
items.push({
|
||||
key: 'addToGroup',
|
||||
icon: <TagsOutlined />,
|
||||
icon: <TagsOutlined aria-hidden />,
|
||||
label: t('pages.inbounds.addClientsToGroup'),
|
||||
});
|
||||
items.push({ type: 'divider' });
|
||||
items.push({
|
||||
key: 'delAllClients',
|
||||
icon: <UsergroupDeleteOutlined />,
|
||||
icon: <UsergroupDeleteOutlined aria-hidden />,
|
||||
danger: true,
|
||||
label: t('pages.inbounds.delAllClients'),
|
||||
});
|
||||
} else {
|
||||
items.push({ type: 'divider' });
|
||||
}
|
||||
items.push({ key: 'delete', icon: <DeleteOutlined />, danger: true, label: t('delete') });
|
||||
items.push({
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined aria-hidden />,
|
||||
danger: true,
|
||||
label: t('delete'),
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -114,7 +127,7 @@ export function RowActionsCell({ record, subEnable, hasClients, onClick }: RowAc
|
||||
type="text"
|
||||
size="small"
|
||||
style={{ fontSize: 16 }}
|
||||
icon={<EditOutlined />}
|
||||
icon={<EditOutlined aria-hidden />}
|
||||
aria-label={t('edit')}
|
||||
onClick={() => onClick('edit')}
|
||||
/>
|
||||
@@ -129,7 +142,7 @@ export function RowActionsCell({ record, subEnable, hasClients, onClick }: RowAc
|
||||
type="text"
|
||||
size="small"
|
||||
style={{ fontSize: 16 }}
|
||||
icon={<MoreOutlined />}
|
||||
icon={<MoreOutlined aria-hidden />}
|
||||
aria-label={t('more')}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
@@ -130,7 +130,11 @@ export function useInboundColumns({
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: (_, record) => (
|
||||
<Switch checked={record.enable} onChange={(next) => onSwitchEnable(record, next)} />
|
||||
<Switch
|
||||
aria-label={t('pages.inbounds.enable')}
|
||||
checked={record.enable}
|
||||
onChange={(next) => onSwitchEnable(record, next)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -293,7 +297,7 @@ export function useInboundColumns({
|
||||
className="client-count-tag"
|
||||
style={{ margin: 0, marginRight: 4, padding: '0 2px' }}
|
||||
>
|
||||
<TeamOutlined /> {cc.clients}
|
||||
<TeamOutlined aria-hidden /> {cc.clients}
|
||||
</Tag>
|
||||
{cc.active.length > 0 ? (
|
||||
<Popover
|
||||
|
||||
@@ -102,13 +102,18 @@ export default function QrPanel({
|
||||
{remark}
|
||||
</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button size="small" icon={<CopyOutlined />} aria-label={t('copy')} onClick={copy} />
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined aria-hidden />}
|
||||
aria-label={t('copy')}
|
||||
onClick={copy}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showQr && (
|
||||
<Tooltip title={t('downloadImage')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PictureOutlined />}
|
||||
icon={<PictureOutlined aria-hidden />}
|
||||
aria-label={t('downloadImage')}
|
||||
onClick={downloadImage}
|
||||
/>
|
||||
@@ -118,7 +123,7 @@ export default function QrPanel({
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
icon={<DownloadOutlined aria-hidden />}
|
||||
aria-label={t('download')}
|
||||
onClick={download}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user