mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
13 KiB
13 KiB
Fakemui Accessibility Integration Guide
This document explains how accessibility utilities are integrated throughout fakemui components, enabling data-testid and ARIA attributes automatically.
Overview
All fakemui components now support:
- data-testid attributes for reliable testing
- ARIA attributes for screen reader support
- Keyboard navigation utilities
- Focus management hooks
- Live region announcements
Accessibility Utilities
Location
fakemui/src/utils/
├── accessibility.ts # Core accessibility utilities
├── useAccessible.ts # React hooks for accessibility
└── index.ts # Barrel export
Core Exports
accessibility.ts
- generateTestId() - Creates standardized test IDs
- testId object - 50+ preset test ID generators
- aria object - ARIA attribute patterns
- keyboard object - Keyboard event helpers
- validate object - Accessibility validators
useAccessible.ts - React Hooks
- useAccessible() - Generate test IDs and ARIA attributes
- useKeyboardNavigation() - Handle keyboard events
- useFocusManagement() - Manage focus programmatically
- useLiveRegion() - Announce messages to screen readers
- useFocusTrap() - Trap focus in modals/dialogs
Integration in Components
Button Component
Before (manual ARIA):
<button
disabled={disabled || loading}
aria-busy={loading}
aria-disabled={disabled}
>
Click me
</button>
After (automatic data-testid + ARIA):
import { useAccessible } from '../../../src/utils/useAccessible'
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { children, testId: customTestId, ...restProps } = props
const accessible = useAccessible({
feature: 'form',
component: 'button',
identifier: customTestId || String(children)?.substring(0, 20),
})
return (
<button
data-testid={accessible['data-testid']}
aria-label={accessible['aria-label']}
{...restProps}
>
{children}
</button>
)
}
)
Result:
<button data-testid="form-button-click-click-me" aria-label="form: button">
Click me
</button>
TextField Component
Integration:
import { useAccessible } from '../../../src/utils/useAccessible'
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
({ label, helperText, error, testId: customTestId, ...props }, ref) => {
const id = useId()
const helperTextId = `${id}-helper-text`
const accessible = useAccessible({
feature: 'form',
component: 'input',
identifier: customTestId || String(label)?.substring(0, 20),
ariaDescribedBy: helperText ? helperTextId : undefined,
})
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
ref={ref}
data-testid={accessible['data-testid']}
aria-invalid={error}
aria-describedby={helperText ? helperTextId : undefined}
{...props}
/>
{helperText && (
<span id={helperTextId} role="status">
{helperText}
</span>
)}
</div>
)
}
)
Result:
<div>
<label for="input-1">Email</label>
<input
id="input-1"
data-testid="form-input-email"
aria-invalid="false"
aria-describedby="input-1-helper-text"
type="email"
/>
<span id="input-1-helper-text" role="status">
Enter a valid email
</span>
</div>
Using Accessibility Utilities
Hook: useAccessible()
Generate test IDs and ARIA attributes for any component:
import { useAccessible } from '@metabuilder/fakemui'
export function MyComponent() {
const accessible = useAccessible({
feature: 'canvas', // canvas, settings, navigation, etc.
component: 'button', // button, input, select, etc.
action: 'delete', // Optional: click, drag, delete, etc.
identifier: 'item-123', // Optional: unique identifier
})
return (
<button
data-testid={accessible['data-testid']}
aria-label={accessible['aria-label']}
>
Delete
</button>
)
}
Hook: useKeyboardNavigation()
Handle keyboard events (Enter, Escape, Arrow keys, Tab):
import { useKeyboardNavigation } from '@metabuilder/fakemui'
export function ComboBox() {
const keyboardProps = useKeyboardNavigation({
onEnter: () => selectItem(),
onEscape: () => closeDropdown(),
onArrowUp: () => selectPrevious(),
onArrowDown: () => selectNext(),
})
return <div {...keyboardProps}>ComboBox content</div>
}
Hook: useFocusManagement()
Manage focus programmatically:
import { useFocusManagement } from '@metabuilder/fakemui'
export function SearchBox() {
const { focusRef, focus } = useFocusManagement()
return (
<>
<input ref={focusRef} placeholder="Search..." />
<button onClick={focus}>Focus Search</button>
</>
)
}
Hook: useLiveRegion()
Announce messages to screen readers:
import { useLiveRegion } from '@metabuilder/fakemui'
export function ItemList() {
const { announce, liveRegionProps, message } = useLiveRegion('polite')
const handleDelete = (item) => {
deleteItem(item)
announce(`${item.name} deleted`)
}
return (
<>
<div {...liveRegionProps} className="sr-only">{message}</div>
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => handleDelete(item)}>Delete</button>
</li>
))}
</ul>
</>
)
}
Hook: useFocusTrap()
Trap focus within modals/dialogs:
import { useFocusTrap } from '@metabuilder/fakemui'
export function Modal({ isOpen, onClose }) {
const { focusTrapRef } = useFocusTrap(isOpen)
return (
isOpen && (
<div ref={focusTrapRef} role="dialog" aria-modal="true">
<h2>Dialog Title</h2>
<input placeholder="First focusable" />
<button>Save</button>
<button onClick={onClose}>Close</button>
</div>
)
)
}
Test ID Patterns
Format
{feature}-{component}-{action?}-{identifier?}
Examples
form-button-click-submit // Submit button in form
settings-input-email // Email input in settings
canvas-item-drag-123 // Drag action on canvas item with ID 123
navigation-link-click-home // Home link in navigation
table-row-row-123 // Row 123 in table
modal-close-dialog-close // Close button in dialog
Preset Generators
The testId object provides 50+ preset generators:
import { testId } from '@metabuilder/fakemui'
// Form fields
testId.button('Save') // form-button-click-save
testId.input('email') // form-input-email
testId.checkbox('remember') // form-checkbox-remember
testId.select('language') // form-select-language
// Canvas
testId.canvasItem('item-1') // canvas-item-drag-item-1
testId.canvasZoomIn() // canvas-button-click-zoom-in
// Navigation
testId.navLink('Dashboard') // navigation-button-click-dashboard
testId.navTab('Projects') // navigation-tab-projects
// Modals
testId.modal('confirm') // modal-modal-confirm
testId.modalButton('confirm', 'ok') // modal-button-click-confirm-ok
// Tables
testId.table('users') // table-table-users
testId.tableRow('users', 'row-1') // table-item-users-row-1
// And more...
ARIA Attribute Patterns
The aria object provides ARIA attribute patterns:
import { aria } from '@metabuilder/fakemui'
// Button
<button {...aria.button('Delete item')}>Delete</button>
// Toggle
<div {...aria.toggle('Dark mode', isDark)}>Toggle</div>
// Combobox
<div {...aria.combobox(isOpen)}>Dropdown</div>
// Dialog
<div {...aria.dialog('Confirm action')}>Confirm?</div>
// Tab system
<div {...aria.tablist()}>
<button {...aria.tab(isSelected, 'tab-panel-1')}>Tab 1</button>
</div>
// Status messages
<div {...aria.status('Loading...', 'info')}>Loading...</div>
// Live regions
<div {...aria.liveRegion('assertive')}>Important update</div>
// And more...
Keyboard Navigation
Handle keyboard events:
import { keyboard } from '@metabuilder/fakemui'
function handleKeyDown(e: React.KeyboardEvent) {
if (keyboard.isActivation(e.key)) {
// Enter or Space pressed
}
if (keyboard.isArrow(e.key)) {
const direction = keyboard.getArrowDirection(e.key)
// -1, 0, or 1
}
if (keyboard.isEscape(e.key)) {
// Escape pressed
}
if (keyboard.isTab(e.key)) {
// Tab pressed
}
}
Testing with Accessibility Utilities
Example: Testing a Button
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from '@metabuilder/fakemui'
describe('Button', () => {
it('should render with data-testid', () => {
render(<Button testId="submit">Submit</Button>)
const button = screen.getByTestId('form-button-click-submit')
expect(button).toBeInTheDocument()
})
it('should have accessible aria-label', () => {
render(<Button>Save Changes</Button>)
const button = screen.getByRole('button', { name: /save changes/i })
expect(button).toBeInTheDocument()
})
it('should be keyboard accessible', async () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
const button = screen.getByRole('button')
await userEvent.keyboard('{Enter}')
expect(handleClick).toHaveBeenCalled()
})
})
Example: Testing a TextField
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TextField } from '@metabuilder/fakemui'
describe('TextField', () => {
it('should have helper text announced', () => {
render(
<TextField
label="Email"
helperText="Enter a valid email"
/>
)
const input = screen.getByRole('textbox', { name: /email/i })
const helper = screen.getByText('Enter a valid email')
expect(input).toHaveAttribute('aria-describedby')
expect(helper).toHaveAttribute('role', 'status')
})
it('should show error state accessibly', () => {
render(
<TextField
label="Password"
error
helperText="Password too short"
/>
)
const input = screen.getByRole('textbox', { name: /password/i })
expect(input).toHaveAttribute('aria-invalid', 'true')
})
})
Accessibility Checklist
When integrating accessibility utilities into components:
- Component has
data-testidattribute (viauseAccessible) - Component has proper
aria-labelor semantic HTML - Error states use
aria-invalidandaria-describedby - Helper text uses
role="status"for announcements - Buttons have semantic role (native
<button>element) - Keyboard navigation works (via
useKeyboardNavigation) - Focus is visible (outline not removed)
- Focus is managed in modals (via
useFocusTrap) - Live regions announce important updates (via
useLiveRegion) - Color is not the only indicator (use icons + text)
- Images have alt text
- Component tested with screen readers (NVDA, JAWS, VoiceOver)
Best Practices
- Always use semantic HTML -
<button>,<input>,<label>, etc. - Use built-in hooks - Don't manually add ARIA attributes
- Test with real screen readers - Automated tools miss edge cases
- Keyboard first - If it works with keyboard, it works with assistive tech
- Focus visible - Never remove focus outlines
- Meaningful IDs - Test IDs should be identifiable (not just UUIDs)
- Live regions for updates - Announce changes that don't move focus
- Group related inputs - Use
<fieldset>and<legend> - Provide feedback - Let users know actions succeeded/failed
- Test continuously - Accessibility is not a one-time effort
Migration Status
| Component | Status | data-testid | ARIA | Keyboard Nav |
|---|---|---|---|---|
| Button | ✅ Updated | ✅ | ✅ | ✅ |
| TextField | ✅ Updated | ✅ | ✅ | ✅ |
| Input | ⏳ Pending | - | - | - |
| Select | ⏳ Pending | - | - | - |
| Dialog | ⏳ Pending | - | - | - |
| Tabs | ⏳ Pending | - | - | - |
| ... more | ⏳ Pending | - | - | - |
Resources
File Structure
fakemui/
├── src/utils/
│ ├── accessibility.ts # Core utilities (472 lines)
│ ├── accessibility.module.scss # Styling (180 lines)
│ ├── useAccessible.ts # React hooks (250+ lines)
│ └── index.ts # Barrel export
├── react/components/
│ ├── inputs/
│ │ ├── Button.tsx # Updated ✅
│ │ ├── TextField.tsx # Updated ✅
│ │ ├── Input.tsx # To update
│ │ └── ...
│ └── ...
└── docs/
└── ACCESSIBILITY_INTEGRATION.md # This file