Skip to content

Commit c196c28

Browse files
Samuelf27claude
andcommitted
fix(a11y): meet WCAG AA contrast and fix ARIA roles/labels
- styles.css: darken Info/Success/Warning/Danger semantic tokens so Badge and Alert text meets AA (>=4.5:1) on their tinted backgrounds; add dark-mode-tuned values in .dark; keep the solid danger button dark in dark mode so its white text stays AA. - Avatar: add role="img" to the initials span so screen readers announce the name instead of raw initials. - Alert: role is now variant-driven (assertive alert for danger/warning, polite status for info/success) with an optional role override. - Switch: render the label text visibly next to the control while keeping the accessible name. - Input: error span is a role="alert" live region; internal aria-invalid/ aria-describedby now win over spread props. - Add jest-axe a11y tests plus role/ARIA assertions for Alert/Badge/ Avatar/Switch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 219347d commit c196c28

9 files changed

Lines changed: 319 additions & 16 deletions

File tree

package-lock.json

Lines changed: 196 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
},
1515
"./styles.css": "./dist/styles.css"
1616
},
17-
"files": ["dist"],
18-
"sideEffects": ["**/*.css"],
17+
"files": [
18+
"dist"
19+
],
20+
"sideEffects": [
21+
"**/*.css"
22+
],
1923
"scripts": {
2024
"dev": "vite",
2125
"build": "vite build",
@@ -24,12 +28,26 @@
2428
"typecheck": "tsc --noEmit",
2529
"preview": "vite preview"
2630
},
27-
"keywords": ["react", "typescript", "design-system", "ui", "components", "ui-kit", "dark-mode", "accessibility"],
31+
"keywords": [
32+
"react",
33+
"typescript",
34+
"design-system",
35+
"ui",
36+
"components",
37+
"ui-kit",
38+
"dark-mode",
39+
"accessibility"
40+
],
2841
"author": "Samuel Ferreira",
2942
"license": "MIT",
30-
"repository": { "type": "git", "url": "git+https://github.com/Samuelf27/react-ui-kit.git" },
43+
"repository": {
44+
"type": "git",
45+
"url": "git+https://github.com/Samuelf27/react-ui-kit.git"
46+
},
3147
"homepage": "https://samuelf27.github.io/react-ui-kit/",
32-
"publishConfig": { "access": "public" },
48+
"publishConfig": {
49+
"access": "public"
50+
},
3351
"peerDependencies": {
3452
"react": ">=18",
3553
"react-dom": ">=18"
@@ -40,6 +58,7 @@
4058
"@types/react": "^18.3.3",
4159
"@types/react-dom": "^18.3.0",
4260
"@vitejs/plugin-react": "^4.3.1",
61+
"jest-axe": "^9.0.0",
4362
"jsdom": "^24.1.0",
4463
"react": "^18.3.1",
4564
"react-dom": "^18.3.1",

src/components/Alert.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ const ICONS: Record<NonNullable<AlertProps['variant']>, string> = {
1313
danger: '⛔',
1414
};
1515

16+
/** danger/warning são urgentes (assertivo); info/success são passivos (educado). */
17+
const ROLES: Record<NonNullable<AlertProps['variant']>, 'alert' | 'status'> = {
18+
info: 'status',
19+
success: 'status',
20+
warning: 'alert',
21+
danger: 'alert',
22+
};
23+
1624
/** Mensagem contextual (info, sucesso, aviso, erro). */
17-
export function Alert({ variant = 'info', title, children, className = '', ...props }: AlertProps) {
25+
export function Alert({ variant = 'info', title, children, className = '', role, ...props }: AlertProps) {
1826
return (
19-
<div role="alert" className={`ui-alert ui-alert--${variant} ${className}`.trim()} {...props}>
27+
<div role={role ?? ROLES[variant]} className={`ui-alert ui-alert--${variant} ${className}`.trim()} {...props}>
2028
<span className="ui-alert__icon" aria-hidden="true">{ICONS[variant]}</span>
2129
<div className="ui-alert__body">
2230
{title && <div className="ui-alert__title">{title}</div>}

src/components/Avatar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function initials(name: string): string {
1717
export function Avatar({ src, name, size = 'md', className = '', ...props }: AvatarProps) {
1818
return (
1919
<div className={`ui-avatar ui-avatar--${size} ${className}`.trim()} {...props}>
20-
{src ? <img src={src} alt={name} /> : <span aria-label={name}>{initials(name)}</span>}
20+
{src ? <img src={src} alt={name} /> : <span role="img" aria-label={name}>{initials(name)}</span>}
2121
</div>
2222
);
2323
}

src/components/Input.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
2222
<input
2323
ref={ref}
2424
id={inputId}
25-
aria-invalid={error ? true : undefined}
26-
aria-describedby={error ? `${inputId}-error` : undefined}
2725
className={`ui-input ${error ? 'ui-input--error' : ''} ${className}`.trim()}
2826
{...props}
27+
aria-invalid={error ? true : props['aria-invalid']}
28+
aria-describedby={error ? `${inputId}-error` : props['aria-describedby']}
2929
/>
3030
{error && (
31-
<span id={`${inputId}-error`} className="ui-field__error">
31+
<span id={`${inputId}-error`} role="alert" className="ui-field__error">
3232
{error}
3333
</span>
3434
)}

src/components/Switch.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
1414
<span className="ui-switch__track">
1515
<span className="ui-switch__thumb" />
1616
</span>
17+
<span className="ui-switch__label">{label}</span>
1718
</label>
1819
);
1920
},

0 commit comments

Comments
 (0)