mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-05 20:55:29 +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.
60 lines
1.8 KiB
TypeScript
60 lines
1.8 KiB
TypeScript
import { Select } from 'antd';
|
|
import type { SelectProps } from 'antd';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { TLS_CIPHER_OPTION, TLS_CIPHER_PRESETS } from '@/schemas/primitives';
|
|
import { selectTagRender } from './SelectTag';
|
|
|
|
const CIPHER_SUITE_OPTIONS = Object.values(TLS_CIPHER_OPTION).map((v) => ({ value: v, label: v }));
|
|
|
|
const PRESET_PREFIX = 'preset:';
|
|
const PRESET_OPTIONS = Object.keys(TLS_CIPHER_PRESETS).map((name) => ({
|
|
value: PRESET_PREFIX + name,
|
|
label: name,
|
|
}));
|
|
|
|
type CipherSuitesSelectProps = Omit<
|
|
SelectProps<string[]>,
|
|
'value' | 'onChange' | 'mode' | 'options'
|
|
> & {
|
|
// Injected by FormField:
|
|
value?: string;
|
|
onChange?: (value: string) => void;
|
|
};
|
|
|
|
function expandPreset(next: string[]): string[] {
|
|
const picked = [...next].reverse().find((v) => v.startsWith(PRESET_PREFIX));
|
|
if (!picked) return next;
|
|
const name = picked.slice(PRESET_PREFIX.length) as keyof typeof TLS_CIPHER_PRESETS;
|
|
return TLS_CIPHER_PRESETS[name].split(':');
|
|
}
|
|
|
|
// xray splits cipherSuites on ':' into a list, so the picker edits tags while
|
|
// the stored value stays the single colon-joined string xray reads.
|
|
export default function CipherSuitesSelect({
|
|
value = '',
|
|
onChange,
|
|
...rest
|
|
}: CipherSuitesSelectProps) {
|
|
const { t } = useTranslation();
|
|
const suites = value
|
|
.split(':')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
return (
|
|
<Select
|
|
allowClear
|
|
tokenSeparators={[':', ',']}
|
|
{...rest}
|
|
mode="tags"
|
|
tagRender={selectTagRender}
|
|
options={[
|
|
{ label: t('pages.inbounds.form.presets'), options: PRESET_OPTIONS },
|
|
{ label: t('pages.inbounds.form.cipherSuites'), options: CIPHER_SUITE_OPTIONS },
|
|
]}
|
|
value={suites}
|
|
onChange={(next) => onChange?.(expandPreset(next).join(':'))}
|
|
/>
|
|
);
|
|
}
|