diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json index 0782b6164..7e31aebd5 100644 --- a/frontend/.oxlintrc.json +++ b/frontend/.oxlintrc.json @@ -9,7 +9,9 @@ "jsx-a11y" ], "jsPlugins": [ - "./tools/oxlint/input-number-guard.mjs" + "./tools/oxlint/input-number-guard.mjs", + "./tools/oxlint/control-label-guard.mjs", + "./tools/oxlint/icon-a11y-guard.mjs" ], "categories": { "correctness": "error" @@ -48,7 +50,9 @@ "react-hooks/exhaustive-deps": "error", "jsx-a11y/no-autofocus": "off", "input-number/no-synthetic-clear": "off", - "jsx-a11y/prefer-tag-over-role": "off" + "jsx-a11y/prefer-tag-over-role": "off", + "control-label/has-name": "off", + "antd-icon/explicit-a11y": "off" }, "overrides": [ { @@ -67,6 +71,25 @@ "rules": { "input-number/no-synthetic-clear": "off" } + }, + { + "files": [ + "src/**/*.tsx" + ], + "rules": { + "control-label/has-name": "error", + "antd-icon/explicit-a11y": "error" + } + }, + { + "files": [ + "src/**/*.stories.tsx", + "src/test/**" + ], + "rules": { + "control-label/has-name": "off", + "antd-icon/explicit-a11y": "off" + } } ] } diff --git a/frontend/src/components/clients/ClientHwidList.tsx b/frontend/src/components/clients/ClientHwidList.tsx index 5b3302831..c1444ca34 100644 --- a/frontend/src/components/clients/ClientHwidList.tsx +++ b/frontend/src/components/clients/ClientHwidList.tsx @@ -44,7 +44,12 @@ export default function ClientHwidListModal({ zIndex={zIndex} onCancel={onClose} footer={[ - , } + icon={} loading={deletingId === entry.id} /> diff --git a/frontend/src/components/clients/ConfigBlock.tsx b/frontend/src/components/clients/ConfigBlock.tsx index f9b06d9c2..553980028 100644 --- a/frontend/src/components/clients/ConfigBlock.tsx +++ b/frontend/src/components/clients/ConfigBlock.tsx @@ -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 */
e.stopPropagation()}> - )} - diff --git a/frontend/src/components/form/DateTimePicker.tsx b/frontend/src/components/form/DateTimePicker.tsx index aa30aaf9b..7ef43328f 100644 --- a/frontend/src/components/form/DateTimePicker.tsx +++ b/frontend/src/components/form/DateTimePicker.tsx @@ -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); }} > - + )}
@@ -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%' }} /> ); diff --git a/frontend/src/components/form/HeaderMapEditor.tsx b/frontend/src/components/form/HeaderMapEditor.tsx index 76c3c8607..8a30751b1 100644 --- a/frontend/src/components/form/HeaderMapEditor.tsx +++ b/frontend/src/components/form/HeaderMapEditor.tsx @@ -125,21 +125,23 @@ export default function HeaderMapEditor({ mode, value, onChange }: HeaderMapEdit setRow(idx, { name: e.target.value })} /> setRow(idx, { value: e.target.value })} /> diff --git a/frontend/src/components/form/RemarkTemplateField.tsx b/frontend/src/components/form/RemarkTemplateField.tsx index 546509309..894514e40 100644 --- a/frontend/src/components/form/RemarkTemplateField.tsx +++ b/frontend/src/components/form/RemarkTemplateField.tsx @@ -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(null); @@ -76,7 +82,7 @@ export default function RemarkTemplateField({ )} diff --git a/frontend/src/components/ui/notifications/DiscordNotifications.tsx b/frontend/src/components/ui/notifications/DiscordNotifications.tsx index 3aca818cf..2b99902e3 100644 --- a/frontend/src/components/ui/notifications/DiscordNotifications.tsx +++ b/frontend/src/components/ui/notifications/DiscordNotifications.tsx @@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types'; const GROUPS: NotificationGroupConfig[] = [ { - icon: , + icon: , title: 'eventGroupOutbound', events: [ { key: 'outbound.down', label: 'eventOutboundDown', settingKey: '' }, @@ -21,12 +21,12 @@ const GROUPS: NotificationGroupConfig[] = [ ], }, { - icon: , + icon: , title: 'eventGroupXray', events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }], }, { - icon: , + icon: , title: 'eventGroupNode', events: [ { key: 'node.down', label: 'eventNodeDown', settingKey: '' }, @@ -34,7 +34,7 @@ const GROUPS: NotificationGroupConfig[] = [ ], }, { - icon: , + icon: , title: 'eventGroupSystem', events: [ { @@ -72,7 +72,7 @@ const GROUPS: NotificationGroupConfig[] = [ ], }, { - icon: , + icon: , title: 'eventGroupSecurity', events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }], }, diff --git a/frontend/src/components/ui/notifications/EmailNotifications.tsx b/frontend/src/components/ui/notifications/EmailNotifications.tsx index 902e34e3c..abcdd1e46 100644 --- a/frontend/src/components/ui/notifications/EmailNotifications.tsx +++ b/frontend/src/components/ui/notifications/EmailNotifications.tsx @@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types'; const GROUPS: NotificationGroupConfig[] = [ { - icon: , + icon: , title: 'eventGroupOutbound', events: [ { @@ -36,12 +36,12 @@ const GROUPS: NotificationGroupConfig[] = [ ], }, { - icon: , + icon: , title: 'eventGroupXray', events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }], }, { - icon: , + icon: , title: 'eventGroupNode', events: [ { key: 'node.down', label: 'eventNodeDown', settingKey: '' }, @@ -49,7 +49,7 @@ const GROUPS: NotificationGroupConfig[] = [ ], }, { - icon: , + icon: , title: 'eventGroupSystem', events: [ { @@ -87,7 +87,7 @@ const GROUPS: NotificationGroupConfig[] = [ ], }, { - icon: , + icon: , title: 'eventGroupSecurity', events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }], }, diff --git a/frontend/src/components/ui/notifications/TelegramNotifications.tsx b/frontend/src/components/ui/notifications/TelegramNotifications.tsx index 0af52831d..826ee08ed 100644 --- a/frontend/src/components/ui/notifications/TelegramNotifications.tsx +++ b/frontend/src/components/ui/notifications/TelegramNotifications.tsx @@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types'; const GROUPS: NotificationGroupConfig[] = [ { - icon: , + icon: , title: 'eventGroupOutbound', events: [ { @@ -36,12 +36,12 @@ const GROUPS: NotificationGroupConfig[] = [ ], }, { - icon: , + icon: , title: 'eventGroupXray', events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }], }, { - icon: , + icon: , title: 'eventGroupNode', events: [ { key: 'node.down', label: 'eventNodeDown', settingKey: '' }, @@ -49,7 +49,7 @@ const GROUPS: NotificationGroupConfig[] = [ ], }, { - icon: , + icon: , title: 'eventGroupSystem', events: [ { @@ -87,7 +87,7 @@ const GROUPS: NotificationGroupConfig[] = [ ], }, { - icon: , + icon: , title: 'eventGroupSecurity', events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }], }, diff --git a/frontend/src/entries/login.tsx b/frontend/src/entries/login.tsx index 7107bf210..7d7e3127b 100644 --- a/frontend/src/entries/login.tsx +++ b/frontend/src/entries/login.tsx @@ -5,6 +5,7 @@ import 'antd/dist/reset.css'; import { setupHttp } from '@/api/http-init'; import { applyDocumentTitle } from '@/utils'; import { readyI18n } from '@/i18n/react'; +import { AntdRootConfig } from '@/i18n/AntdRootConfig'; import { ThemeProvider } from '@/hooks/useTheme'; import { QueryProvider } from '@/api/QueryProvider'; import LoginPage from '@/pages/login/LoginPage'; @@ -21,11 +22,13 @@ readyI18n().then(() => { const root = document.getElementById('app'); if (root) { createRoot(root).render( - - - - - , + + + + + + + , ); } }); diff --git a/frontend/src/entries/subpage.tsx b/frontend/src/entries/subpage.tsx index 1c7be6b9f..d77b557aa 100644 --- a/frontend/src/entries/subpage.tsx +++ b/frontend/src/entries/subpage.tsx @@ -3,6 +3,7 @@ import { message } from 'antd'; import 'antd/dist/reset.css'; import { readyI18n } from '@/i18n/react'; +import { AntdRootConfig } from '@/i18n/AntdRootConfig'; import { ThemeProvider } from '@/hooks/useTheme'; import { QueryProvider } from '@/api/QueryProvider'; import SubPage from '@/pages/sub/SubPage'; @@ -16,11 +17,13 @@ readyI18n('subscription').then(() => { const root = document.getElementById('app'); if (root) { createRoot(root).render( - - - - - , + + + + + + + , ); } }); diff --git a/frontend/src/i18n/AntdRootConfig.tsx b/frontend/src/i18n/AntdRootConfig.tsx new file mode 100644 index 000000000..ea33cc4f9 --- /dev/null +++ b/frontend/src/i18n/AntdRootConfig.tsx @@ -0,0 +1,41 @@ +import type { ReactNode } from 'react'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import { ConfigProvider } from 'antd'; +import type { ConfigProviderProps } from 'antd/es/config-provider'; +import type { Locale } from 'antd/es/locale'; + +const antdLocaleLoaders: Record Promise<{ default: Locale }>> = { + 'ar-EG': () => import('antd/es/locale/ar_EG'), + 'es-ES': () => import('antd/es/locale/es_ES'), + 'fa-IR': () => import('antd/es/locale/fa_IR'), + 'id-ID': () => import('antd/es/locale/id_ID'), + 'ja-JP': () => import('antd/es/locale/ja_JP'), + 'pt-BR': () => import('antd/es/locale/pt_BR'), + 'ru-RU': () => import('antd/es/locale/ru_RU'), + 'tr-TR': () => import('antd/es/locale/tr_TR'), + 'uk-UA': () => import('antd/es/locale/uk_UA'), + 'vi-VN': () => import('antd/es/locale/vi_VN'), + 'zh-CN': () => import('antd/es/locale/zh_CN'), + 'zh-TW': () => import('antd/es/locale/zh_TW'), +}; + +// Undefined keeps antd's built-in en_US. +let antdLocale: Locale | undefined; + +export async function loadAntdLocale(language: string): Promise { + antdLocale = (await antdLocaleLoaders[language]?.())?.default; +} + +// A Form.Item tooltip icon sits inside its