mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-26 15:55:02 +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:
@@ -44,7 +44,12 @@ export default function ClientHwidListModal({
|
||||
zIndex={zIndex}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="refresh" icon={<ReloadOutlined />} loading={loading} onClick={onRefresh}>
|
||||
<Button
|
||||
key="refresh"
|
||||
icon={<ReloadOutlined aria-hidden />}
|
||||
loading={loading}
|
||||
onClick={onRefresh}
|
||||
>
|
||||
{t('refresh')}
|
||||
</Button>,
|
||||
<Popconfirm
|
||||
@@ -122,7 +127,7 @@ export default function ClientHwidListModal({
|
||||
type="text"
|
||||
size="small"
|
||||
aria-label={t('pages.clients.deleteHwid')}
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteOutlined aria-hidden />}
|
||||
loading={deletingId === entry.id}
|
||||
/>
|
||||
</Popconfirm>
|
||||
|
||||
@@ -38,12 +38,17 @@ export default function ConfigBlock({
|
||||
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
<div className="config-block-actions" onClick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<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>
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
icon={<DownloadOutlined aria-hidden />}
|
||||
aria-label={t('download')}
|
||||
onClick={() => FileManager.downloadTextFile(text, fileName)}
|
||||
/>
|
||||
@@ -56,7 +61,11 @@ export default function ConfigBlock({
|
||||
content={<QrPanel value={text} remark={qrRemark || label} size={220} />}
|
||||
>
|
||||
<Tooltip title={t('pages.clients.qrCode')}>
|
||||
<Button size="small" icon={<QrcodeOutlined />} aria-label={t('pages.clients.qrCode')} />
|
||||
<Button
|
||||
size="small"
|
||||
icon={<QrcodeOutlined aria-hidden />}
|
||||
aria-label={t('pages.clients.qrCode')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
@@ -262,9 +262,9 @@ export default function CommandPalette() {
|
||||
title: c.email,
|
||||
subtitle: `${trafficUsed} / ${trafficTotal}${c.comment ? ` · ${c.comment}` : ''}`,
|
||||
icon: isOnline ? (
|
||||
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
||||
<CheckCircleFilled aria-hidden style={{ color: '#52c41a' }} />
|
||||
) : (
|
||||
<CloseCircleFilled style={{ color: '#ff4d4f' }} />
|
||||
<CloseCircleFilled aria-hidden style={{ color: '#ff4d4f' }} />
|
||||
),
|
||||
action: () => {
|
||||
close();
|
||||
@@ -274,7 +274,7 @@ export default function CommandPalette() {
|
||||
c.subId && allSetting.subURI
|
||||
? {
|
||||
label: t('commandPalette.copySubscription'),
|
||||
icon: <CopyOutlined />,
|
||||
icon: <CopyOutlined aria-hidden />,
|
||||
execute: (e) => {
|
||||
e.stopPropagation();
|
||||
copySubscription(c);
|
||||
@@ -331,7 +331,7 @@ export default function CommandPalette() {
|
||||
category: 'inbounds',
|
||||
title: ib.remark || ib.tag || `Inbound #${ib.id}`,
|
||||
subtitle: `Port ${ib.port || ''}`,
|
||||
icon: <ImportOutlined style={{ color: '#1677ff' }} />,
|
||||
icon: <ImportOutlined aria-hidden style={{ color: '#1677ff' }} />,
|
||||
tag:
|
||||
tags.length > 0 ? (
|
||||
<div style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap' }}>{tags}</div>
|
||||
@@ -348,7 +348,7 @@ export default function CommandPalette() {
|
||||
path: '/',
|
||||
title: t('menu.dashboard'),
|
||||
keywords: ['overview', 'dashboard', 'cpu', 'ram', 'memory', 'traffic', 'speed'],
|
||||
icon: <DashboardOutlined />,
|
||||
icon: <DashboardOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/inbounds',
|
||||
@@ -365,67 +365,67 @@ export default function CommandPalette() {
|
||||
'hysteria',
|
||||
'masque',
|
||||
],
|
||||
icon: <ImportOutlined />,
|
||||
icon: <ImportOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/clients',
|
||||
title: t('menu.clients'),
|
||||
keywords: ['clients', 'users', 'sub', 'traffic', 'quota'],
|
||||
icon: <TeamOutlined />,
|
||||
icon: <TeamOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/groups',
|
||||
title: t('menu.groups'),
|
||||
keywords: ['groups', 'tags', 'batch'],
|
||||
icon: <TagsOutlined />,
|
||||
icon: <TagsOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/nodes',
|
||||
title: t('menu.nodes'),
|
||||
keywords: ['nodes', 'servers', 'cluster', 'remote nodes'],
|
||||
icon: <ClusterOutlined />,
|
||||
icon: <ClusterOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/hosts',
|
||||
title: t('menu.hosts'),
|
||||
keywords: ['hosts', 'sni', 'domains'],
|
||||
icon: <GlobalOutlined />,
|
||||
icon: <GlobalOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/outbound',
|
||||
title: t('menu.outbounds'),
|
||||
keywords: ['outbounds', 'freedom', 'blackhole', 'socks', 'http', 'warp', 'nord', 'pia'],
|
||||
icon: <ExportOutlined />,
|
||||
icon: <ExportOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/routing',
|
||||
title: t('menu.routing'),
|
||||
keywords: ['routing', 'rules', 'geoip', 'geosite', 'direct', 'block'],
|
||||
icon: <SwapOutlined />,
|
||||
icon: <SwapOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
title: t('menu.settings'),
|
||||
keywords: ['settings', 'config', 'port', 'password', 'ssl', 'telegram'],
|
||||
icon: <SettingOutlined />,
|
||||
icon: <SettingOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray',
|
||||
title: t('menu.xray'),
|
||||
keywords: ['xray', 'templates', 'balancer', 'dns'],
|
||||
icon: <ToolOutlined />,
|
||||
icon: <ToolOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/api-docs',
|
||||
title: t('menu.apiDocs'),
|
||||
keywords: ['api', 'api docs', 'swagger', 'rest api', 'endpoints'],
|
||||
icon: <ApiOutlined />,
|
||||
icon: <ApiOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/sponsors',
|
||||
title: t('menu.sponsors'),
|
||||
keywords: ['sponsors', 'sponsor', 'partners'],
|
||||
icon: <CrownOutlined />,
|
||||
icon: <CrownOutlined aria-hidden />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -451,63 +451,63 @@ export default function CommandPalette() {
|
||||
title: `${t('menu.settings')} · ${t('pages.settings.panelSettings')}`,
|
||||
subtitle: t('pages.settings.panelSettings'),
|
||||
keywords: ['general', 'webPort', 'webBasePath', 'listenIP', 'ssl', 'certificate'],
|
||||
icon: <SettingOutlined />,
|
||||
icon: <SettingOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/settings#security',
|
||||
title: `${t('menu.settings')} · ${t('pages.settings.securitySettings')}`,
|
||||
subtitle: t('pages.settings.securitySettings'),
|
||||
keywords: ['security', 'password', 'username', '2fa', 'two factor', 'login limit'],
|
||||
icon: <SafetyOutlined />,
|
||||
icon: <SafetyOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/settings#telegram',
|
||||
title: `${t('menu.settings')} · ${t('pages.settings.TGBotSettings')}`,
|
||||
subtitle: t('pages.settings.TGBotSettings'),
|
||||
keywords: ['telegram', 'tgbot', 'bot token', 'chat id', 'notifications', 'alerts'],
|
||||
icon: <MessageOutlined />,
|
||||
icon: <MessageOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/settings#email',
|
||||
title: `${t('menu.settings')} · ${t('pages.settings.emailSettings')}`,
|
||||
subtitle: t('pages.settings.emailSettings'),
|
||||
keywords: ['email', 'smtp', 'mail', 'crash alerts'],
|
||||
icon: <MailOutlined />,
|
||||
icon: <MailOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/settings#discord',
|
||||
title: `${t('menu.settings')} · ${t('pages.settings.discordSettings')}`,
|
||||
subtitle: t('pages.settings.discordSettings'),
|
||||
keywords: ['discord', 'bot', 'channel', 'notifications', 'alerts'],
|
||||
icon: <DiscordOutlined />,
|
||||
icon: <DiscordOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/settings#subscription',
|
||||
title: `${t('menu.settings')} · ${t('pages.settings.subSettings')}`,
|
||||
subtitle: t('pages.settings.subSettings'),
|
||||
keywords: ['subscription', 'subPort', 'subURI', 'subDomain', 'reverse proxy'],
|
||||
icon: <CloudServerOutlined />,
|
||||
icon: <CloudServerOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/settings#subscription-formats',
|
||||
title: `${t('menu.settings')} · ${t('menu.subFormats')}`,
|
||||
subtitle: t('menu.subFormats'),
|
||||
keywords: ['formats', 'clash', 'sing-box', 'v2ray', 'json', 'sub formats'],
|
||||
icon: <CodeOutlined />,
|
||||
icon: <CodeOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/settings#subscription-balancers',
|
||||
title: `${t('menu.settings')} · ${t('pages.settings.subBalancers.menu')}`,
|
||||
subtitle: t('pages.settings.subBalancers.menu'),
|
||||
keywords: ['balancers', 'sub balancers', 'balancer nodes'],
|
||||
icon: <ApartmentOutlined />,
|
||||
icon: <ApartmentOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray#basic',
|
||||
title: `${t('menu.xray')} · ${t('pages.xray.basicTemplate')}`,
|
||||
subtitle: t('pages.xray.basicTemplate'),
|
||||
keywords: ['basics', 'freedom strategy', 'happy eyeballs', 'torrent', 'connection'],
|
||||
icon: <ToolOutlined />,
|
||||
icon: <ToolOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray#basic',
|
||||
@@ -522,56 +522,56 @@ export default function CommandPalette() {
|
||||
'statsOutbound',
|
||||
'metrics_out',
|
||||
],
|
||||
icon: <DashboardOutlined />,
|
||||
icon: <DashboardOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray#basic',
|
||||
title: `${t('menu.xray')} · ${t('pages.xray.connectionLimits')}`,
|
||||
subtitle: t('pages.xray.connectionLimits'),
|
||||
keywords: ['limits', 'idle timeout', 'bufferSize', 'connIdle', 'timeout'],
|
||||
icon: <ClockCircleOutlined />,
|
||||
icon: <ClockCircleOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray#basic',
|
||||
title: `${t('menu.xray')} · ${t('pages.xray.logConfigs')}`,
|
||||
subtitle: t('pages.xray.logConfigs'),
|
||||
keywords: ['logs', 'access log', 'error log', 'dns log', 'mask address', 'loglevel'],
|
||||
icon: <FileTextOutlined />,
|
||||
icon: <FileTextOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray#balancer',
|
||||
title: `${t('menu.xray')} · ${t('pages.xray.Balancers')}`,
|
||||
subtitle: t('pages.xray.Balancers'),
|
||||
keywords: ['balancers', 'leastPing', 'roundRobin', 'fallback', 'strategy'],
|
||||
icon: <ClusterOutlined />,
|
||||
icon: <ClusterOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray#dns',
|
||||
title: `${t('menu.xray')} · DNS`,
|
||||
subtitle: 'DNS',
|
||||
keywords: ['dns', 'dns servers', 'hosts', 'doh', 'dot', 'cloudflare dns'],
|
||||
icon: <DatabaseOutlined />,
|
||||
icon: <DatabaseOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray#outbound',
|
||||
title: `${t('menu.xray')} · ${t('pages.xray.Outbounds')}`,
|
||||
subtitle: t('pages.xray.Outbounds'),
|
||||
keywords: ['outbound', 'freedom', 'direct', 'proxy outbounds'],
|
||||
icon: <ExportOutlined />,
|
||||
icon: <ExportOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray#routing',
|
||||
title: `${t('menu.xray')} · ${t('pages.xray.basicRouting')}`,
|
||||
subtitle: t('pages.xray.basicRouting'),
|
||||
keywords: ['routing', 'routing rules', 'geoip', 'geosite', 'block', 'direct'],
|
||||
icon: <SwapOutlined />,
|
||||
icon: <SwapOutlined aria-hidden />,
|
||||
},
|
||||
{
|
||||
path: '/xray#advanced',
|
||||
title: `${t('menu.xray')} · ${t('pages.xray.advancedTemplate')}`,
|
||||
subtitle: t('pages.xray.advancedTemplate'),
|
||||
keywords: ['advanced', 'json template', 'advanced config', 'custom json'],
|
||||
icon: <CodeOutlined />,
|
||||
icon: <CodeOutlined aria-hidden />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -599,7 +599,7 @@ export default function CommandPalette() {
|
||||
title: t('commandPalette.restartXray'),
|
||||
subtitle: t('pages.index.restartXray'),
|
||||
keywords: ['restart', 'xray restart', 'reboot xray'],
|
||||
icon: <ReloadOutlined style={{ color: '#faad14' }} />,
|
||||
icon: <ReloadOutlined aria-hidden style={{ color: '#faad14' }} />,
|
||||
action: restartXray,
|
||||
},
|
||||
{
|
||||
@@ -608,7 +608,7 @@ export default function CommandPalette() {
|
||||
title: t('menu.theme'),
|
||||
subtitle: isUltra ? 'Ultra Dark' : isDark ? 'Dark' : 'Light',
|
||||
keywords: ['theme', 'light', 'dark', 'ultra'],
|
||||
icon: isDark ? <SunOutlined /> : <MoonOutlined />,
|
||||
icon: isDark ? <SunOutlined aria-hidden /> : <MoonOutlined aria-hidden />,
|
||||
action: cycleTheme,
|
||||
},
|
||||
{
|
||||
@@ -617,7 +617,7 @@ export default function CommandPalette() {
|
||||
title: t('pages.inbounds.addInbound'),
|
||||
subtitle: t('menu.inbounds'),
|
||||
keywords: ['add inbound', 'create inbound', 'new port', 'new inbound'],
|
||||
icon: <PlusOutlined style={{ color: '#52c41a' }} />,
|
||||
icon: <PlusOutlined aria-hidden style={{ color: '#52c41a' }} />,
|
||||
action: () => {
|
||||
close();
|
||||
navigate('/inbounds');
|
||||
@@ -629,7 +629,7 @@ export default function CommandPalette() {
|
||||
title: t('pages.clients.addClient'),
|
||||
subtitle: t('menu.clients'),
|
||||
keywords: ['add client', 'create user', 'new client', 'new user'],
|
||||
icon: <PlusOutlined style={{ color: '#52c41a' }} />,
|
||||
icon: <PlusOutlined aria-hidden style={{ color: '#52c41a' }} />,
|
||||
action: () => {
|
||||
close();
|
||||
navigate('/clients');
|
||||
@@ -705,9 +705,9 @@ export default function CommandPalette() {
|
||||
>
|
||||
<div className="command-palette-header">
|
||||
{isClientSearching ? (
|
||||
<LoadingOutlined className="command-palette-search-icon spinning" />
|
||||
<LoadingOutlined aria-hidden className="command-palette-search-icon spinning" />
|
||||
) : (
|
||||
<SearchOutlined className="command-palette-search-icon" />
|
||||
<SearchOutlined aria-hidden className="command-palette-search-icon" />
|
||||
)}
|
||||
<input
|
||||
ref={inputRef}
|
||||
|
||||
@@ -69,11 +69,11 @@ export default function TextModal({
|
||||
footer={
|
||||
<>
|
||||
{fileName && (
|
||||
<Button icon={<DownloadOutlined />} onClick={download}>
|
||||
<Button icon={<DownloadOutlined aria-hidden />} onClick={download}>
|
||||
{fileName}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" icon={<CopyOutlined />} onClick={copy}>
|
||||
<Button type="primary" icon={<CopyOutlined aria-hidden />} onClick={copy}>
|
||||
{t('copy')}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -19,6 +19,9 @@ interface DateTimePickerProps {
|
||||
disabled?: boolean;
|
||||
allowClear?: boolean;
|
||||
maxDate?: Dayjs;
|
||||
id?: string;
|
||||
'aria-label'?: string;
|
||||
'aria-describedby'?: string;
|
||||
}
|
||||
|
||||
const LIGHT_THEME = {
|
||||
@@ -57,6 +60,9 @@ export default function DateTimePicker({
|
||||
disabled = false,
|
||||
allowClear = true,
|
||||
maxDate,
|
||||
id,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-describedby': ariaDescribedBy,
|
||||
}: DateTimePickerProps) {
|
||||
const { t } = useTranslation();
|
||||
const { datepicker } = useDatepicker();
|
||||
@@ -97,7 +103,11 @@ export default function DateTimePicker({
|
||||
useEffect(() => {
|
||||
if (datepicker !== 'jalalian') return;
|
||||
const input = jalaliRef.current?.querySelector('input');
|
||||
if (input) input.placeholder = placeholder;
|
||||
if (!input) return;
|
||||
input.placeholder = placeholder;
|
||||
if (id) input.id = id;
|
||||
if (ariaLabel) input.setAttribute('aria-label', ariaLabel);
|
||||
if (ariaDescribedBy) input.setAttribute('aria-describedby', ariaDescribedBy);
|
||||
});
|
||||
|
||||
if (datepicker === 'jalalian') {
|
||||
@@ -137,7 +147,7 @@ export default function DateTimePicker({
|
||||
setClearNonce((n) => n + 1);
|
||||
}}
|
||||
>
|
||||
<CloseCircleFilled />
|
||||
<CloseCircleFilled aria-hidden />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -156,6 +166,9 @@ export default function DateTimePicker({
|
||||
disabled={disabled}
|
||||
allowClear={allowClear}
|
||||
maxDate={maxDate}
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -125,21 +125,23 @@ export default function HeaderMapEditor({ mode, value, onChange }: HeaderMapEdit
|
||||
<Input
|
||||
value={row.name}
|
||||
placeholder="Name"
|
||||
aria-label={t('pages.inbounds.form.headerName')}
|
||||
onChange={(e) => setRow(idx, { name: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
value={row.value}
|
||||
placeholder="Value"
|
||||
aria-label={t('pages.inbounds.form.headerValue')}
|
||||
onChange={(e) => setRow(idx, { value: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('remove')}
|
||||
icon={<MinusOutlined />}
|
||||
icon={<MinusOutlined aria-hidden />}
|
||||
onClick={() => removeRow(idx)}
|
||||
/>
|
||||
</Space.Compact>
|
||||
))}
|
||||
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={addRow}>
|
||||
<Button size="small" type="primary" icon={<PlusOutlined aria-hidden />} onClick={addRow}>
|
||||
Add
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -22,6 +22,9 @@ interface RemarkTemplateFieldProps {
|
||||
multiline?: boolean;
|
||||
rows?: number;
|
||||
metadataOnly?: boolean;
|
||||
id?: string;
|
||||
'aria-label'?: string;
|
||||
'aria-labelledby'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,6 +40,9 @@ export default function RemarkTemplateField({
|
||||
multiline = false,
|
||||
rows,
|
||||
metadataOnly = false,
|
||||
id,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-labelledby': ariaLabelledBy,
|
||||
}: RemarkTemplateFieldProps) {
|
||||
const { t } = useTranslation();
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
@@ -76,7 +82,7 @@ export default function RemarkTemplateField({
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CodeOutlined />}
|
||||
icon={<CodeOutlined aria-hidden />}
|
||||
aria-label={t('pages.hosts.remarkVars.title')}
|
||||
style={{ marginInlineEnd: -7 }}
|
||||
/>
|
||||
@@ -90,6 +96,9 @@ export default function RemarkTemplateField({
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
|
||||
<Input.TextArea
|
||||
ref={textAreaRef}
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
value={value}
|
||||
maxLength={maxLength}
|
||||
placeholder={placeholder}
|
||||
@@ -101,6 +110,9 @@ export default function RemarkTemplateField({
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
value={value}
|
||||
maxLength={maxLength}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -49,8 +49,13 @@ export function FormField<T extends FieldValues = FieldValues>({
|
||||
const { t } = useTranslation();
|
||||
const dottedName = toDotted(name) as Path<T>;
|
||||
const generatedId = useId();
|
||||
const explicitId = (children as ReactElement<{ id?: string }>).props.id;
|
||||
const fieldId = explicitId ?? generatedId;
|
||||
const hintId = useId();
|
||||
const childOwnProps = (children as ReactElement<{ id?: string; 'aria-describedby'?: string }>)
|
||||
.props;
|
||||
const fieldId = childOwnProps.id ?? generatedId;
|
||||
const describedBy = tooltip
|
||||
? [childOwnProps['aria-describedby'], hintId].filter(Boolean).join(' ')
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
@@ -74,6 +79,7 @@ export function FormField<T extends FieldValues = FieldValues>({
|
||||
onBlur: field.onBlur,
|
||||
ref: field.ref,
|
||||
};
|
||||
if (describedBy) childProps['aria-describedby'] = describedBy;
|
||||
return (
|
||||
<Form.Item
|
||||
label={label}
|
||||
@@ -88,6 +94,12 @@ export function FormField<T extends FieldValues = FieldValues>({
|
||||
style={style}
|
||||
>
|
||||
{cloneElement(children as ReactElement<Record<string, unknown>>, childProps)}
|
||||
{/* The tooltip only opens on hover, so screen readers get its text as a description. */}
|
||||
{tooltip && (
|
||||
<span id={hintId} hidden>
|
||||
{tooltip}
|
||||
</span>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -332,6 +332,7 @@ export default function GeoBrowserModal({
|
||||
value={categoryQuery}
|
||||
onChange={(event) => setCategoryQuery(event.target.value)}
|
||||
placeholder={t('pages.xray.geoBrowser.searchCategory')}
|
||||
aria-label={t('pages.xray.geoBrowser.searchCategory')}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
@@ -392,6 +393,7 @@ export default function GeoBrowserModal({
|
||||
value={entryQuery}
|
||||
onChange={(event) => setEntryQuery(event.target.value)}
|
||||
placeholder={t('pages.xray.geoBrowser.searchEntries')}
|
||||
aria-label={t('pages.xray.geoBrowser.searchEntries')}
|
||||
allowClear
|
||||
className="geo-entry-filter"
|
||||
/>
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface GeoTokenInputProps {
|
||||
kind: GeoTokenKind;
|
||||
placeholder?: string;
|
||||
id?: string;
|
||||
'aria-describedby'?: string;
|
||||
ref?: Ref<InputRef>;
|
||||
}
|
||||
|
||||
@@ -40,6 +41,7 @@ export default function GeoTokenInput({
|
||||
kind,
|
||||
placeholder,
|
||||
id,
|
||||
'aria-describedby': ariaDescribedBy,
|
||||
ref,
|
||||
}: GeoTokenInputProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -92,6 +94,7 @@ export default function GeoTokenInput({
|
||||
<Input
|
||||
ref={ref}
|
||||
id={id}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
@@ -99,7 +102,7 @@ export default function GeoTokenInput({
|
||||
/>
|
||||
<Tooltip title={t('pages.xray.geoBrowser.openTooltip')}>
|
||||
<Button
|
||||
icon={<DatabaseOutlined />}
|
||||
icon={<DatabaseOutlined aria-hidden />}
|
||||
aria-label={t('pages.xray.geoBrowser.openTooltip')}
|
||||
onClick={() => setBrowsing(true)}
|
||||
/>
|
||||
|
||||
@@ -63,13 +63,13 @@ export default function SponsorCard({ sponsor, variant = 'banner', onClose }: Sp
|
||||
<span className="sponsor-aside">
|
||||
<span className="sponsor-tag">{tag}</span>
|
||||
<span className="sponsor-visit">
|
||||
{t('pages.sponsors.visit')} <ExportOutlined />
|
||||
{t('pages.sponsors.visit')} <ExportOutlined aria-hidden />
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{variant === 'card' && (
|
||||
<span className="sponsor-visit">
|
||||
{t('pages.sponsors.visit')} <ExportOutlined />
|
||||
{t('pages.sponsors.visit')} <ExportOutlined aria-hidden />
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
@@ -81,7 +81,7 @@ export default function SponsorCard({ sponsor, variant = 'banner', onClose }: Sp
|
||||
title={t('close')}
|
||||
onClick={onClose}
|
||||
>
|
||||
<CloseOutlined />
|
||||
<CloseOutlined aria-hidden />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types';
|
||||
|
||||
const GROUPS: NotificationGroupConfig[] = [
|
||||
{
|
||||
icon: <CloudServerOutlined />,
|
||||
icon: <CloudServerOutlined aria-hidden />,
|
||||
title: 'eventGroupOutbound',
|
||||
events: [
|
||||
{ key: 'outbound.down', label: 'eventOutboundDown', settingKey: '' },
|
||||
@@ -21,12 +21,12 @@ const GROUPS: NotificationGroupConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <ThunderboltOutlined />,
|
||||
icon: <ThunderboltOutlined aria-hidden />,
|
||||
title: 'eventGroupXray',
|
||||
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
|
||||
},
|
||||
{
|
||||
icon: <DesktopOutlined />,
|
||||
icon: <DesktopOutlined aria-hidden />,
|
||||
title: 'eventGroupNode',
|
||||
events: [
|
||||
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||
@@ -34,7 +34,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <DashboardOutlined />,
|
||||
icon: <DashboardOutlined aria-hidden />,
|
||||
title: 'eventGroupSystem',
|
||||
events: [
|
||||
{
|
||||
@@ -72,7 +72,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <SafetyOutlined />,
|
||||
icon: <SafetyOutlined aria-hidden />,
|
||||
title: 'eventGroupSecurity',
|
||||
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types';
|
||||
|
||||
const GROUPS: NotificationGroupConfig[] = [
|
||||
{
|
||||
icon: <CloudServerOutlined />,
|
||||
icon: <CloudServerOutlined aria-hidden />,
|
||||
title: 'eventGroupOutbound',
|
||||
events: [
|
||||
{
|
||||
@@ -36,12 +36,12 @@ const GROUPS: NotificationGroupConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <ThunderboltOutlined />,
|
||||
icon: <ThunderboltOutlined aria-hidden />,
|
||||
title: 'eventGroupXray',
|
||||
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
|
||||
},
|
||||
{
|
||||
icon: <DesktopOutlined />,
|
||||
icon: <DesktopOutlined aria-hidden />,
|
||||
title: 'eventGroupNode',
|
||||
events: [
|
||||
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||
@@ -49,7 +49,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <DashboardOutlined />,
|
||||
icon: <DashboardOutlined aria-hidden />,
|
||||
title: 'eventGroupSystem',
|
||||
events: [
|
||||
{
|
||||
@@ -87,7 +87,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <SafetyOutlined />,
|
||||
icon: <SafetyOutlined aria-hidden />,
|
||||
title: 'eventGroupSecurity',
|
||||
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types';
|
||||
|
||||
const GROUPS: NotificationGroupConfig[] = [
|
||||
{
|
||||
icon: <CloudServerOutlined />,
|
||||
icon: <CloudServerOutlined aria-hidden />,
|
||||
title: 'eventGroupOutbound',
|
||||
events: [
|
||||
{
|
||||
@@ -36,12 +36,12 @@ const GROUPS: NotificationGroupConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <ThunderboltOutlined />,
|
||||
icon: <ThunderboltOutlined aria-hidden />,
|
||||
title: 'eventGroupXray',
|
||||
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
|
||||
},
|
||||
{
|
||||
icon: <DesktopOutlined />,
|
||||
icon: <DesktopOutlined aria-hidden />,
|
||||
title: 'eventGroupNode',
|
||||
events: [
|
||||
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||
@@ -49,7 +49,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <DashboardOutlined />,
|
||||
icon: <DashboardOutlined aria-hidden />,
|
||||
title: 'eventGroupSystem',
|
||||
events: [
|
||||
{
|
||||
@@ -87,7 +87,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <SafetyOutlined />,
|
||||
icon: <SafetyOutlined aria-hidden />,
|
||||
title: 'eventGroupSecurity',
|
||||
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user