mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Add test IDs and accessibility to editor and preview components
Third iteration of comprehensive accessibility improvements: - Added test IDs to CodeEditorSection (preview checkbox, editor containers, error message) - Added test IDs and roles to ReactPreview component (loading, error, unsupported states) - Enhanced preview components with proper ARIA labels and status roles - Added test IDs to demo feature cards with region role - Improved error states with data-testid for error messages - Added aria-label attributes to preview state containers Component updates: - CodeEditorSection: Enable preview checkbox, editor/preview containers, error display - ReactPreview: Loading state, error state, unsupported language state - DemoFeatureCards: Feature cards grid with region role - Preview state management: Added proper ARIA roles (status, alert) These additions enable comprehensive testing of: - Code editor workflows (editing, preview toggling, error states) - Component preview rendering and error handling - Different preview states (loading, error, unsupported) - Demo feature cards navigation and display Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
357
ACCESSIBILITY_IMPROVEMENTS.md
Normal file
357
ACCESSIBILITY_IMPROVEMENTS.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# Accessibility & Testing Improvements
|
||||
|
||||
## Overview
|
||||
This document summarizes all accessibility enhancements and data-testid attributes added to the CodeSnippet application. These improvements make the application more accessible to screen reader users and easier to test programmatically.
|
||||
|
||||
---
|
||||
|
||||
## Navigation Components
|
||||
|
||||
### Navigation.tsx
|
||||
- **Added:** `aria-expanded` - Indicates navigation menu state
|
||||
- **Added:** `aria-controls="navigation-sidebar"` - Links button to controlled element
|
||||
- **Added:** `data-testid="navigation-toggle-btn"` - Test identifier for menu toggle
|
||||
- **Added:** `aria-hidden="true"` to icon - Hides decorative icon from screen readers
|
||||
|
||||
### NavigationSidebar.tsx
|
||||
✅ **Already had:** `data-testid` attributes on overlay and sidebar
|
||||
- **Added:** `id="navigation-sidebar"` - Enables aria-controls reference
|
||||
- **Added:** `role="navigation"` - Explicit semantic role
|
||||
- **Added:** `aria-label="Main navigation menu"` - Descriptive label
|
||||
- **Navigation items:** `data-testid={`nav-link-${item.path.replace(/\//g, '-')}`}` for dynamic test IDs
|
||||
- **Sidebar close button:** `data-testid="navigation-sidebar-close-btn"`
|
||||
|
||||
### PageLayout.tsx
|
||||
- **Added:** `data-testid="page-layout"` to main container
|
||||
- **Added:** `data-testid="page-header"` to header element
|
||||
- **Added:** `data-testid="logo-text"` to logo
|
||||
- **Added:** `data-testid="main-content"` to main element
|
||||
- **Added:** `aria-hidden="true"` to decorative grid-pattern
|
||||
|
||||
---
|
||||
|
||||
## Snippet Manager Components
|
||||
|
||||
### SnippetManagerRedux.tsx
|
||||
- **Added:** `data-testid="snippet-manager"` to main container
|
||||
- **Added:** `data-testid="snippet-manager-loading"` to loading state
|
||||
|
||||
### SnippetToolbar.tsx
|
||||
✅ **Already had:** Comprehensive data-testid and aria attributes
|
||||
- **Enhanced:** Added `data-testid` for template menu items:
|
||||
- `snippet-template-react-${template.id}`
|
||||
- `snippet-template-python-${template.id}`
|
||||
- `snippet-template-javascript-${template.id}`
|
||||
- **Already present:**
|
||||
- `data-testid="snippet-search-input"` with `aria-label`
|
||||
- `data-testid="snippet-selection-mode-btn"` with `aria-pressed`
|
||||
- `data-testid="snippet-create-menu-trigger"`
|
||||
|
||||
### SnippetCard.tsx
|
||||
✅ **Already had:** `data-testid={`snippet-card-${snippet.id}`}` and `role="article"`
|
||||
|
||||
### SnippetCardActions.tsx
|
||||
✅ **Already had:** Comprehensive data-testid attributes:
|
||||
- `snippet-card-view-btn`
|
||||
- `snippet-card-copy-btn`
|
||||
- `snippet-card-edit-btn`
|
||||
- `snippet-card-actions-menu` (three-dot menu)
|
||||
- `snippet-card-move-submenu`
|
||||
- `move-to-namespace-${namespace.id}`
|
||||
- `snippet-card-delete-btn`
|
||||
- All buttons have appropriate `aria-label` attributes
|
||||
|
||||
### SelectionControls.tsx
|
||||
- **Added:** `data-testid="selection-controls"` to main container
|
||||
- **Added:** `data-testid="select-all-btn"` with `aria-label`
|
||||
- **Added:** `data-testid="selection-count"` to count display
|
||||
- **Added:** `data-testid="bulk-move-menu-trigger"` with `aria-label`
|
||||
- **Added:** `data-testid="bulk-move-menu"` to menu container
|
||||
- **Added:** `data-testid={`bulk-move-to-namespace-${namespace.id}`}` for menu items
|
||||
- **Added:** `aria-hidden="true"` to folder icon
|
||||
|
||||
### SnippetGrid.tsx
|
||||
✅ **Already had:** `data-testid="snippet-grid"` with `role="region"` and `aria-label`
|
||||
|
||||
### NamespaceSelector.tsx
|
||||
✅ **Already had:** Comprehensive data-testid:
|
||||
- `namespace-selector-trigger`
|
||||
- `namespace-selector-content`
|
||||
- `namespace-option-${namespace.id}`
|
||||
|
||||
---
|
||||
|
||||
## Form Components
|
||||
|
||||
### SnippetFormFields.tsx
|
||||
✅ **Already had:** Excellent accessibility implementation:
|
||||
- **Title input:**
|
||||
- `data-testid="snippet-title-input"`
|
||||
- `aria-invalid={!!errors.title}`
|
||||
- `aria-describedby="title-error"` (when error present)
|
||||
- **Language select:**
|
||||
- `data-testid="snippet-language-select"` with `aria-label`
|
||||
- `data-testid="snippet-language-options"` on content
|
||||
- `data-testid={`language-option-${lang}`}` on items
|
||||
- **Description textarea:**
|
||||
- `data-testid="snippet-description-textarea"`
|
||||
- `aria-label="Snippet description"`
|
||||
|
||||
### SnippetDialog.tsx
|
||||
✅ **Already had:**
|
||||
- `data-testid="snippet-dialog"` on DialogContent
|
||||
- `data-testid="snippet-dialog-cancel-btn"`
|
||||
- `data-testid="snippet-dialog-save-btn"`
|
||||
|
||||
### SnippetViewer.tsx
|
||||
- **Added:** `data-testid="snippet-viewer-dialog"` on DialogContent
|
||||
|
||||
### SnippetViewerHeader.tsx
|
||||
- **Added:** `data-testid="snippet-viewer-toggle-preview-btn"` with `aria-pressed` and `aria-label`
|
||||
- **Added:** `data-testid="snippet-viewer-copy-btn"` with descriptive `aria-label`
|
||||
- **Added:** `data-testid="snippet-viewer-edit-btn"` with `aria-label`
|
||||
- **Added:** `aria-hidden="true"` to all icons
|
||||
|
||||
---
|
||||
|
||||
## UI Primitive Components
|
||||
|
||||
### button.tsx
|
||||
✅ **Passes through all props** - Supports data-testid and accessibility attributes
|
||||
|
||||
### input.tsx
|
||||
✅ **Passes through all props** - Supports data-testid and accessibility attributes
|
||||
|
||||
### dialog.tsx
|
||||
- **DialogOverlay:**
|
||||
- **Added:** `aria-hidden="true"` - Hides scrim from accessibility tree
|
||||
- **Added:** `data-testid="dialog-overlay"` - Test identifier
|
||||
- **DialogContent close button:**
|
||||
- **Enhanced:** `aria-label="Close dialog"` (more descriptive)
|
||||
- **Added:** `data-testid="dialog-close-btn"`
|
||||
|
||||
### sonner.tsx (Toast component)
|
||||
- **Added:** `data-testid="toast-container"` to Toaster component
|
||||
- **Added:** `position="bottom-right"` for consistent positioning
|
||||
- ✅ Sonner library automatically manages `aria-live` and `role="status"` on toast notifications
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Patterns Implemented
|
||||
|
||||
### ARIA Attributes
|
||||
| Attribute | Component | Purpose |
|
||||
|-----------|-----------|---------|
|
||||
| `aria-label` | Multiple | Provides accessible name for buttons and regions |
|
||||
| `aria-expanded` | Navigation toggle | Indicates menu open/closed state |
|
||||
| `aria-pressed` | Toggle buttons | Indicates pressed state for toggles |
|
||||
| `aria-hidden` | Decorative elements | Hides non-semantic elements from screen readers |
|
||||
| `aria-controls` | Navigation button | Links trigger to controlled element |
|
||||
| `aria-modal` | Dialog | Marks dialog as modal |
|
||||
| `role="dialog"` | Dialog container | Semantic dialog role |
|
||||
| `aria-describedby` | Form inputs | Links errors to input fields |
|
||||
| `aria-invalid` | Form inputs | Marks invalid form fields |
|
||||
| `aria-label` | Regions | Descriptive labels for landmark regions |
|
||||
|
||||
### Semantic HTML
|
||||
- ✅ `<nav>` - Navigation sections
|
||||
- ✅ `<main>` - Main content area
|
||||
- ✅ `<header>` - Page header
|
||||
- ✅ `<footer>` - Page footer
|
||||
- ✅ `<aside>` - Sidebar (if present)
|
||||
- ✅ `<article>` - Snippet cards
|
||||
- ✅ `role="region"` - Content grids
|
||||
|
||||
---
|
||||
|
||||
## Testing Identifiers (data-testid)
|
||||
|
||||
### Navigation
|
||||
```
|
||||
- navigation-toggle-btn
|
||||
- navigation-sidebar
|
||||
- navigation-sidebar-overlay
|
||||
- navigation-sidebar-close-btn
|
||||
- navigation-items
|
||||
- nav-link-{path}
|
||||
```
|
||||
|
||||
### Layout
|
||||
```
|
||||
- page-layout
|
||||
- page-header
|
||||
- logo-text
|
||||
- main-content
|
||||
- dialog-overlay
|
||||
- dialog-close-btn
|
||||
```
|
||||
|
||||
### Snippet Manager
|
||||
```
|
||||
- snippet-manager
|
||||
- snippet-manager-loading
|
||||
- snippet-search-input
|
||||
- snippet-selection-mode-btn
|
||||
- snippet-create-menu-trigger
|
||||
- snippet-template-react-{id}
|
||||
- snippet-template-python-{id}
|
||||
- snippet-template-javascript-{id}
|
||||
- snippet-card-{id}
|
||||
- snippet-grid
|
||||
```
|
||||
|
||||
### Snippet Actions
|
||||
```
|
||||
- snippet-card-view-btn
|
||||
- snippet-card-copy-btn
|
||||
- snippet-card-edit-btn
|
||||
- snippet-card-actions-menu
|
||||
- snippet-card-move-submenu
|
||||
- move-to-namespace-{id}
|
||||
- snippet-card-delete-btn
|
||||
```
|
||||
|
||||
### Selection & Bulk Operations
|
||||
```
|
||||
- selection-controls
|
||||
- select-all-btn
|
||||
- selection-count
|
||||
- bulk-move-menu-trigger
|
||||
- bulk-move-menu
|
||||
- bulk-move-to-namespace-{id}
|
||||
```
|
||||
|
||||
### Forms
|
||||
```
|
||||
- snippet-title-input
|
||||
- snippet-language-select
|
||||
- snippet-language-options
|
||||
- language-option-{lang}
|
||||
- snippet-description-textarea
|
||||
- snippet-dialog
|
||||
- snippet-dialog-cancel-btn
|
||||
- snippet-dialog-save-btn
|
||||
```
|
||||
|
||||
### Snippet Viewer
|
||||
```
|
||||
- snippet-viewer-dialog
|
||||
- snippet-viewer-toggle-preview-btn
|
||||
- snippet-viewer-copy-btn
|
||||
- snippet-viewer-edit-btn
|
||||
```
|
||||
|
||||
### Namespaces
|
||||
```
|
||||
- namespace-selector-trigger
|
||||
- namespace-selector-content
|
||||
- namespace-option-{id}
|
||||
```
|
||||
|
||||
### Toasts
|
||||
```
|
||||
- toast-container
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Applied
|
||||
|
||||
### 1. **Icon Accessibility**
|
||||
- All decorative icons have `aria-hidden="true"`
|
||||
- Icon buttons always have `aria-label` or contextual button text
|
||||
|
||||
### 2. **Form Accessibility**
|
||||
- Error messages linked via `aria-describedby`
|
||||
- Invalid fields marked with `aria-invalid="true"`
|
||||
- Labels properly associated with inputs via `htmlFor`
|
||||
|
||||
### 3. **Interactive Elements**
|
||||
- Buttons have descriptive labels
|
||||
- Toggle buttons use `aria-pressed` for state
|
||||
- Dropdowns have clear trigger labels
|
||||
|
||||
### 4. **Screen Reader Support**
|
||||
- All interactive regions have `aria-label` or `aria-labelledby`
|
||||
- Decorative elements properly hidden
|
||||
- Loading states announced with text
|
||||
- Status messages using Sonner's built-in `aria-live="polite"`
|
||||
|
||||
### 5. **Testing Support**
|
||||
- Semantic data-testid naming convention
|
||||
- IDs include context (e.g., `snippet-card-{id}`)
|
||||
- Dynamic IDs parameterized for consistency
|
||||
|
||||
---
|
||||
|
||||
## Testing Usage Examples
|
||||
|
||||
### Selecting Elements in Tests
|
||||
```typescript
|
||||
// Navigation
|
||||
cy.get('[data-testid="navigation-toggle-btn"]').click()
|
||||
cy.get('[data-testid="navigation-sidebar"]').should('be.visible')
|
||||
|
||||
// Snippet Management
|
||||
cy.get('[data-testid="snippet-search-input"]').type('react')
|
||||
cy.get('[data-testid="snippet-card-abc123"]').click()
|
||||
cy.get('[data-testid="snippet-card-copy-btn"]').click()
|
||||
|
||||
// Forms
|
||||
cy.get('[data-testid="snippet-title-input"]').type('New Snippet')
|
||||
cy.get('[data-testid="snippet-dialog-save-btn"]').click()
|
||||
|
||||
// Selection
|
||||
cy.get('[data-testid="select-all-btn"]').click()
|
||||
cy.get('[data-testid="bulk-move-menu-trigger"]').click()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Testing Checklist
|
||||
|
||||
### Screen Reader Testing
|
||||
- [ ] Test with NVDA (Windows) or JAWS
|
||||
- [ ] Test with VoiceOver (macOS)
|
||||
- [ ] Verify all buttons have descriptive labels
|
||||
- [ ] Verify form errors are announced
|
||||
|
||||
### Keyboard Navigation
|
||||
- [ ] All interactive elements are keyboard accessible
|
||||
- [ ] Tab order is logical (automatic with semantic HTML)
|
||||
- [ ] Modal dialogs trap focus
|
||||
- [ ] Escape key closes modals
|
||||
|
||||
### Visual Accessibility
|
||||
- [ ] Sufficient color contrast (WCAG AA minimum)
|
||||
- [ ] Focus indicators are visible
|
||||
- [ ] Text is resizable
|
||||
- [ ] No information conveyed by color alone
|
||||
|
||||
### Automated Testing
|
||||
- [ ] Use `data-testid` for E2E tests
|
||||
- [ ] Run axe-core or similar for automated checks
|
||||
- [ ] Test responsive design with screen readers
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Add keyboard shortcuts** - Document and implement vim-style shortcuts
|
||||
2. **Enhanced tooltips** - Add aria-describedby tooltips for complex UI
|
||||
3. **Loading states** - Add aria-busy for async operations
|
||||
4. **Pagination** - Add aria-label for pagination controls
|
||||
5. **Code syntax highlighting** - Ensure color contrast in code blocks
|
||||
6. **Animations** - Add prefers-reduced-motion support
|
||||
7. **Focus management** - Restore focus after dialog close
|
||||
8. **Landmark navigation** - Skip to main content link
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
||||
- [Material Design 3 Accessibility](https://m3.material.io/foundations/accessible-design)
|
||||
- [React Accessibility Guide](https://reactjs.org/docs/accessibility.html)
|
||||
|
||||
@@ -4,6 +4,19 @@ import React from 'react'
|
||||
// Make React globally available for components that may reference it
|
||||
global.React = React
|
||||
|
||||
// Mock import.meta for Vite compatibility
|
||||
Object.defineProperty(globalThis, 'import', {
|
||||
value: {
|
||||
meta: {
|
||||
env: {
|
||||
DEV: false,
|
||||
PROD: true,
|
||||
SSR: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Mock Next.js router
|
||||
jest.mock('next/router', () => ({
|
||||
useRouter: () => ({
|
||||
|
||||
@@ -4,8 +4,13 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
export const DemoFeatureCards = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="border-primary/20">
|
||||
<div
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
||||
data-testid="demo-feature-cards"
|
||||
role="region"
|
||||
aria-label="Feature cards"
|
||||
>
|
||||
<Card className="border-primary/20" data-testid="feature-card-realtime">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Real-Time Updates</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -14,7 +19,7 @@ export const DemoFeatureCards = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-accent/20">
|
||||
<Card className="border-accent/20" data-testid="feature-card-resizable">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Resizable Panels</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -23,7 +28,7 @@ export const DemoFeatureCards = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-primary/20">
|
||||
<Card className="border-primary/20" data-testid="feature-card-viewmodes">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Multiple View Modes</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
@@ -36,8 +36,14 @@ export function CodeEditorSection({
|
||||
id="hasPreview"
|
||||
checked={hasPreview}
|
||||
onCheckedChange={(checked) => onPreviewChange(checked as boolean)}
|
||||
data-testid="enable-preview-checkbox"
|
||||
aria-label="Enable live preview"
|
||||
/>
|
||||
<Label htmlFor="hasPreview" className="text-sm font-normal cursor-pointer">
|
||||
<Label
|
||||
htmlFor="hasPreview"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
data-testid="enable-preview-label"
|
||||
>
|
||||
Enable live preview
|
||||
</Label>
|
||||
</div>
|
||||
@@ -46,7 +52,10 @@ export function CodeEditorSection({
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="code">Code *</Label>
|
||||
{hasPreview && isPreviewSupported ? (
|
||||
<div className={errors.code ? 'ring-2 ring-destructive/20 rounded-md' : ''}>
|
||||
<div
|
||||
className={errors.code ? 'ring-2 ring-destructive/20 rounded-md' : ''}
|
||||
data-testid="split-screen-editor-container"
|
||||
>
|
||||
<SplitScreenEditor
|
||||
value={code}
|
||||
onChange={onCodeChange}
|
||||
@@ -61,11 +70,16 @@ export function CodeEditorSection({
|
||||
className={`rounded-md border overflow-hidden ${
|
||||
errors.code ? 'border-destructive ring-2 ring-destructive/20' : 'border-border'
|
||||
}`}
|
||||
data-testid="code-editor-container"
|
||||
>
|
||||
<MonacoEditor value={code} onChange={onCodeChange} language={language} height="400px" />
|
||||
</div>
|
||||
)}
|
||||
{errors.code && <p className="text-sm text-destructive">{errors.code}</p>}
|
||||
{errors.code && (
|
||||
<p className="text-sm text-destructive" id="code-error" data-testid="code-error-message">
|
||||
{errors.code}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -33,9 +33,14 @@ export function ReactPreview({ code, language, functionName, inputParameters }:
|
||||
|
||||
if (!['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center p-6 bg-muted/30">
|
||||
<div
|
||||
className="h-full flex items-center justify-center p-6 bg-muted/30"
|
||||
data-testid="preview-unsupported"
|
||||
role="status"
|
||||
aria-label="Preview not available"
|
||||
>
|
||||
<div className="text-center text-muted-foreground">
|
||||
<WarningCircle className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<WarningCircle className="h-12 w-12 mx-auto mb-3 opacity-50" aria-hidden="true" />
|
||||
<p className="text-sm">Preview not available for {language}</p>
|
||||
<p className="text-xs mt-1">Use JSX, TSX, JavaScript, or TypeScript</p>
|
||||
</div>
|
||||
@@ -45,15 +50,19 @@ export function ReactPreview({ code, language, functionName, inputParameters }:
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6 bg-destructive/5">
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<WarningCircle className="h-4 w-4" />
|
||||
<AlertDescription className="font-mono text-xs whitespace-pre-wrap">
|
||||
<div
|
||||
className="h-full overflow-auto p-6 bg-destructive/5"
|
||||
data-testid="preview-error"
|
||||
role="alert"
|
||||
>
|
||||
<Alert variant="destructive" className="mb-4" data-testid="preview-error-alert">
|
||||
<WarningCircle className="h-4 w-4" aria-hidden="true" />
|
||||
<AlertDescription className="font-mono text-xs whitespace-pre-wrap" data-testid="preview-error-message">
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<AIErrorHelper
|
||||
error={error}
|
||||
<AIErrorHelper
|
||||
error={error}
|
||||
context={`React component preview rendering (Language: ${language})`}
|
||||
/>
|
||||
</div>
|
||||
@@ -62,7 +71,12 @@ export function ReactPreview({ code, language, functionName, inputParameters }:
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center p-6 bg-muted/30">
|
||||
<div
|
||||
className="h-full flex items-center justify-center p-6 bg-muted/30"
|
||||
data-testid="preview-loading"
|
||||
role="status"
|
||||
aria-label="Loading preview"
|
||||
>
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p className="text-sm">Loading preview...</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user