From 60316c831f4c27deedb2faf48d8bfc860a26993f Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 14 Jul 2026 03:38:14 +0200 Subject: [PATCH] fix(frontend): resolve every axe accessibility violation in the component library Running the stories under axe surfaced real panel defects, not just story cosmetics. FormField never associated its Form.Item label with the wrapped control, so no RHF form field in the panel had a programmatic label; it now generates an id and wires htmlFor. Unnamed controls get accessible names: the prompt and text modal inputs (from the modal title), the client traffic progress bar (used/limit values), the CPU and RAM threshold inputs in the notification groups (event label threaded through the extra renderer), and the JSON editor's contenteditable surface. ConfigBlock's collapse header carried role=button around focusable action buttons; collapsible=header scopes the toggle to the label. Light theme gains contrast-safe tokens shared by the panel and Storybook: darker description, placeholder, error and success text, a darker primary button blue, and a readable gold tag, all meeting the WCAG AA 4.5:1 ratio. The infinity badge swaps a prohibited bare aria-label for role=img. --- .../src/components/clients/ClientTrafficCell.tsx | 3 ++- frontend/src/components/clients/ConfigBlock.css | 4 ++++ frontend/src/components/clients/ConfigBlock.tsx | 1 + frontend/src/components/feedback/PromptModal.tsx | 2 ++ frontend/src/components/feedback/TextModal.tsx | 1 + frontend/src/components/form/JsonEditor.tsx | 1 + frontend/src/components/form/rhf/FormField.tsx | 7 ++++++- .../ui/notifications/EmailNotifications.tsx | 8 ++++---- .../ui/notifications/NotificationGroup.tsx | 1 + .../ui/notifications/TelegramNotifications.tsx | 8 ++++---- frontend/src/components/ui/notifications/types.ts | 2 +- frontend/src/hooks/useTheme.tsx | 15 +++++++++++++++ 12 files changed, 42 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/clients/ClientTrafficCell.tsx b/frontend/src/components/clients/ClientTrafficCell.tsx index ed2a5f3fb..a99f643da 100644 --- a/frontend/src/components/clients/ClientTrafficCell.tsx +++ b/frontend/src/components/clients/ClientTrafficCell.tsx @@ -64,6 +64,7 @@ export default function ClientTrafficCell({ {SizeFormatter.sizeFormat(display.used)} {display.isUnlimited ? ( - + ) : ( diff --git a/frontend/src/components/clients/ConfigBlock.css b/frontend/src/components/clients/ConfigBlock.css index c0b4193df..5a7567786 100644 --- a/frontend/src/components/clients/ConfigBlock.css +++ b/frontend/src/components/clients/ConfigBlock.css @@ -12,6 +12,10 @@ min-width: 0; } +body.light .config-block .ant-tag.ant-tag-filled.ant-tag-gold { + color: #874d00; +} + .config-block .ant-collapse-extra { display: flex; align-items: center; diff --git a/frontend/src/components/clients/ConfigBlock.tsx b/frontend/src/components/clients/ConfigBlock.tsx index 71bc6f5b9..dc7b87ac3 100644 --- a/frontend/src/components/clients/ConfigBlock.tsx +++ b/frontend/src/components/clients/ConfigBlock.tsx @@ -68,6 +68,7 @@ export default function ConfigBlock({ {messageContextHolder} { textareaRef.current = (el as unknown as { resizableTextArea?: { textArea: HTMLTextAreaElement } })?.resizableTextArea?.textArea ?? null; }} + aria-label={title} value={value} onChange={(e) => setValue(e.target.value)} autoSize={{ minRows: 10, maxRows: 20 }} @@ -80,6 +81,7 @@ export default function PromptModal({ ) : ( setValue(e.target.value)} onKeyDown={onKeydown} diff --git a/frontend/src/components/feedback/TextModal.tsx b/frontend/src/components/feedback/TextModal.tsx index 978f445a7..fcb73b218 100644 --- a/frontend/src/components/feedback/TextModal.tsx +++ b/frontend/src/components/feedback/TextModal.tsx @@ -75,6 +75,7 @@ export default function TextModal({ open, onClose, title, content, fileName = '' ) : ( (function JsonEd doc: value, extensions: [ basicSetup, + EditorView.contentAttributes.of({ 'aria-label': t('jsonEditor') }), keymap.of([indentWithTab]), json(), linter(jsonParseLinter()), diff --git a/frontend/src/components/form/rhf/FormField.tsx b/frontend/src/components/form/rhf/FormField.tsx index 0645d2418..89c1245e7 100644 --- a/frontend/src/components/form/rhf/FormField.tsx +++ b/frontend/src/components/form/rhf/FormField.tsx @@ -1,4 +1,4 @@ -import { cloneElement } from 'react'; +import { cloneElement, useId } from 'react'; import type { CSSProperties, ReactElement, ReactNode } from 'react'; import { Controller } from 'react-hook-form'; import type { Control, ControllerProps, FieldValues, Path } from 'react-hook-form'; @@ -48,6 +48,9 @@ export function FormField({ }: FormFieldProps) { const { t } = useTranslation(); const dottedName = toDotted(name) as Path; + const generatedId = useId(); + const explicitId = (children as ReactElement<{ id?: string }>).props.id; + const fieldId = explicitId ?? generatedId; return ( ({ ? t(fieldState.error.message, { defaultValue: fieldState.error.message }) : undefined; const childProps: Record = { + id: fieldId, [valueProp]: displayValue, onChange: (...args: unknown[]) => { const raw = normalizeAntdOnChange(args, valueProp); @@ -73,6 +77,7 @@ export function FormField({ return ( ( - + extra: ({ value, onChange, ariaLabel }) => ( + ), }, { key: 'memory.high', label: 'eventMemoryHigh', settingKey: 'smtpMemory', - extra: ({ value, onChange }) => ( - + extra: ({ value, onChange, ariaLabel }) => ( + ), }, ], diff --git a/frontend/src/components/ui/notifications/NotificationGroup.tsx b/frontend/src/components/ui/notifications/NotificationGroup.tsx index a9c881bac..aec981105 100644 --- a/frontend/src/components/ui/notifications/NotificationGroup.tsx +++ b/frontend/src/components/ui/notifications/NotificationGroup.tsx @@ -51,6 +51,7 @@ export function NotificationGroup({ config, selected, onToggle, onToggleAll, all {event.extra?.({ value: Number((allSetting as unknown as Record)[event.settingKey]) || 0, onChange: (v) => updateSetting({ [event.settingKey]: v }), + ariaLabel: t(`pages.settings.${event.label}`), })} ))} diff --git a/frontend/src/components/ui/notifications/TelegramNotifications.tsx b/frontend/src/components/ui/notifications/TelegramNotifications.tsx index 2ce63c4b1..21ba70c0b 100644 --- a/frontend/src/components/ui/notifications/TelegramNotifications.tsx +++ b/frontend/src/components/ui/notifications/TelegramNotifications.tsx @@ -37,16 +37,16 @@ const GROUPS: NotificationGroupConfig[] = [ key: 'cpu.high', label: 'eventCPUHigh', settingKey: 'tgCpu', - extra: ({ value, onChange }) => ( - + extra: ({ value, onChange, ariaLabel }) => ( + ), }, { key: 'memory.high', label: 'eventMemoryHigh', settingKey: 'tgMemory', - extra: ({ value, onChange }) => ( - + extra: ({ value, onChange, ariaLabel }) => ( + ), }, ], diff --git a/frontend/src/components/ui/notifications/types.ts b/frontend/src/components/ui/notifications/types.ts index baff64b95..208ee77a3 100644 --- a/frontend/src/components/ui/notifications/types.ts +++ b/frontend/src/components/ui/notifications/types.ts @@ -4,7 +4,7 @@ export interface NotificationEventConfig { key: string; label: string; settingKey: string; - extra?: (props: { value: number; onChange: (v: number | null) => void }) => ReactNode; + extra?: (props: { value: number; onChange: (v: number | null) => void; ariaLabel: string }) => ReactNode; } export interface NotificationGroupConfig { diff --git a/frontend/src/hooks/useTheme.tsx b/frontend/src/hooks/useTheme.tsx index 665e46f18..4ef11a081 100644 --- a/frontend/src/hooks/useTheme.tsx +++ b/frontend/src/hooks/useTheme.tsx @@ -78,13 +78,28 @@ const STATISTIC_TOKENS = { contentFontSize: 17, titleFontSize: 11, }; +const LIGHT_CONTRAST_TOKENS = { + colorTextDescription: 'rgba(0, 0, 0, 0.58)', + colorTextTertiary: 'rgba(0, 0, 0, 0.58)', + colorTextPlaceholder: '#767676', + colorError: '#cf1322', + colorErrorText: '#cf1322', + colorSuccessText: '#237804', +}; +const LIGHT_BUTTON_TOKENS = { + colorPrimary: '#0958d9', + colorPrimaryHover: '#2468e5', + colorPrimaryActive: '#073ea8', +}; export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeConfig { if (!isDark) { return { algorithm: antdTheme.defaultAlgorithm, + token: LIGHT_CONTRAST_TOKENS, components: { Statistic: STATISTIC_TOKENS, + Button: LIGHT_BUTTON_TOKENS, }, }; }