docs: add WCAG 2.1 AA accessibility design
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
219
docs/plans/2026-01-11-wcag-accessibility-design.md
Normal file
219
docs/plans/2026-01-11-wcag-accessibility-design.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# WCAG 2.1 AA Accessibility Implementation Design
|
||||
|
||||
## Overview
|
||||
|
||||
Implement WCAG 2.1 AA compliance app-wide, ensuring accessibility for users with visual impairments, motor disabilities, and those using assistive technologies like VoiceOver.
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope:**
|
||||
- Color contrast certification for Monochrome theme (light + dark modes)
|
||||
- VoiceOver support for all view files - accessibility labels, hints, and traits
|
||||
- Theme picker update - accessible badge on Monochrome
|
||||
- Touch targets - minimum 44x44pt for all interactive elements
|
||||
- Focus management - logical focus order, visible focus indicators
|
||||
- Developer guidelines - patterns and rules for future development
|
||||
|
||||
**Out of scope:**
|
||||
- Certifying the other 6 themes (kept as "visual preference" options)
|
||||
- PDF export accessibility (separate concern, fixed dimensions)
|
||||
- Motion reduction (iOS handles via system setting)
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Default theme**: Monochrome is the guaranteed-compliant theme
|
||||
2. **Other themes**: Kept unchanged, no warnings - only Monochrome shows "✓ Accessible" badge
|
||||
3. **Color changes**: Fix Monochrome palette for all users (no conditional/toggle)
|
||||
4. **Coverage**: Full accessibility across all screens
|
||||
|
||||
## Monochrome Theme Color Fixes
|
||||
|
||||
### Current Values
|
||||
|
||||
| Role | Dark Mode | Light Mode |
|
||||
|------|-----------|------------|
|
||||
| Text muted | `#707070` | `#707070` |
|
||||
| Primary accent | `#808080` | `#808080` |
|
||||
| Background 1 | `#121212` | `#FAFAFA` |
|
||||
| Card background | `#1C1C1C` | `#FFFFFF` |
|
||||
|
||||
### Issues
|
||||
|
||||
- Text muted (`#707070`) on dark card (`#1C1C1C`) = ~3.5:1 (needs 4.5:1)
|
||||
|
||||
### Fixes
|
||||
|
||||
| Role | Dark Mode | Light Mode |
|
||||
|------|-----------|------------|
|
||||
| Text muted | `#707070` → `#8A8A8A` | `#707070` → `#595959` |
|
||||
|
||||
New contrast ratios:
|
||||
- Dark: `#8A8A8A` on `#1C1C1C` = ~5.1:1 ✓
|
||||
- Light: `#595959` on `#FFFFFF` = ~7:1 ✓
|
||||
|
||||
## VoiceOver Support Strategy
|
||||
|
||||
### Element Labeling
|
||||
|
||||
| Element Type | Modifier | Example |
|
||||
|--------------|----------|---------|
|
||||
| Buttons | `.accessibilityLabel()` | "Create trip" |
|
||||
| Icons with meaning | `.accessibilityLabel()` | "Baseball" for MLB icon |
|
||||
| Decorative images | `.accessibilityHidden(true)` | Background gradients |
|
||||
| Status indicators | `.accessibilityLabel()` + `.accessibilityValue()` | "Progress: 12 of 30 stadiums visited" |
|
||||
| Toggle/Picker | `.accessibilityLabel()` + `.accessibilityHint()` | Label: "Theme", Hint: "Double tap to change" |
|
||||
| Cards (tappable) | `.accessibilityElement(children: .combine)` | Combines child text into single announcement |
|
||||
| Lists | `.accessibilityLabel()` per row | "Yankees at Red Sox, April 15, 7:05 PM" |
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Labels** describe what it IS: "Create trip button"
|
||||
- **Hints** describe what happens: "Opens trip planner"
|
||||
- **Values** describe current state: "Selected" or "3 of 5"
|
||||
|
||||
### Files to Modify (20 view files)
|
||||
|
||||
**Home:**
|
||||
- `HomeView.swift` - Tab bar, suggested trip cards, quick actions
|
||||
- `SuggestedTripCard.swift` - Card accessibility
|
||||
- `LoadingTripsView.swift` - Loading state announcements
|
||||
|
||||
**Trip:**
|
||||
- `TripCreationView.swift` - Sport picker, date picker, region selector, mode selector
|
||||
- `TripDetailView.swift` - Timeline items, game cards, travel segments, export button
|
||||
- `RegionMapSelector.swift` - Region selection
|
||||
- `TimelineItemView.swift` - Timeline accessibility
|
||||
|
||||
**Schedule:**
|
||||
- `ScheduleListView.swift` - Game list rows, sport filter, date navigation
|
||||
|
||||
**Progress:**
|
||||
- `ProgressTabView.swift` - Tab navigation
|
||||
- `ProgressMapView.swift` - Stadium map, markers
|
||||
- `VisitDetailView.swift` - Visit information
|
||||
- `StadiumVisitSheet.swift` - Stadium details
|
||||
- `AchievementsListView.swift` - Achievement badges
|
||||
- `GameMatchConfirmationView.swift` - Confirmation dialog
|
||||
- `PhotoImportView.swift` - Photo selection
|
||||
|
||||
**Settings:**
|
||||
- `SettingsView.swift` - All rows, theme picker with accessible badge
|
||||
|
||||
**Theme:**
|
||||
- `ViewModifiers.swift` - Reusable component accessibility
|
||||
- `AnimatedComponents.swift` - Animated element accessibility
|
||||
- `SportSelectorGrid.swift` - Sport grid accessibility
|
||||
|
||||
**App:**
|
||||
- `SportsTimeApp.swift` - Root accessibility configuration
|
||||
|
||||
## Theme Picker UI
|
||||
|
||||
Update `SettingsView.swift` theme section:
|
||||
|
||||
- Monochrome shows "✓ Accessible" badge beneath theme name
|
||||
- Other 6 themes display normally with no indicators
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Theme │
|
||||
├─────────────────────────────────────┤
|
||||
│ [███] Monochrome │
|
||||
│ ✓ Accessible │
|
||||
├─────────────────────────────────────┤
|
||||
│ [███] Teal │
|
||||
├─────────────────────────────────────┤
|
||||
│ [███] Orbit │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Touch Targets
|
||||
|
||||
Minimum 44x44pt hit area for all interactive elements.
|
||||
|
||||
**Areas to audit:**
|
||||
- Icon-only buttons (export, share, close)
|
||||
- Inline links
|
||||
- Compact list rows
|
||||
|
||||
**Fix pattern:**
|
||||
```swift
|
||||
Button(action: { }) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.frame(minWidth: 44, minHeight: 44)
|
||||
```
|
||||
|
||||
## Focus Management
|
||||
|
||||
SwiftUI defaults are sufficient. Ensure:
|
||||
- Logical tab order (top-to-bottom, left-to-right)
|
||||
- Focus indicators visible (system default rings)
|
||||
- Modal dismissal returns focus to trigger
|
||||
- No keyboard traps
|
||||
|
||||
## Developer Guidelines
|
||||
|
||||
Create `docs/ACCESSIBILITY.md` with:
|
||||
|
||||
### Color Rules
|
||||
- Monochrome theme: verify 4.5:1 contrast for text, 3:1 for UI components
|
||||
- Don't rely solely on color to convey meaning (add icons, text, or patterns)
|
||||
|
||||
### Accessibility Label Rules
|
||||
- Every `Button` needs `.accessibilityLabel()` unless text is self-descriptive
|
||||
- Every meaningful `Image` needs `.accessibilityLabel()`
|
||||
- Decorative images get `.accessibilityHidden(true)`
|
||||
- Group related elements with `.accessibilityElement(children: .combine)`
|
||||
|
||||
### Touch Target Rules
|
||||
- All tappable elements minimum 44x44pt hit area
|
||||
- Use `.contentShape(Rectangle())` to expand hit areas when needed
|
||||
|
||||
### Testing Requirements
|
||||
- Enable VoiceOver and navigate every new screen before PR
|
||||
- Test with Dynamic Type at Accessibility XXL
|
||||
- Test Monochrome theme in both light and dark modes
|
||||
|
||||
### PR Checklist
|
||||
```
|
||||
## Accessibility
|
||||
- [ ] All new buttons have accessibility labels
|
||||
- [ ] Images labeled or hidden appropriately
|
||||
- [ ] Touch targets are 44x44pt minimum
|
||||
- [ ] Tested with VoiceOver
|
||||
- [ ] Tested with large Dynamic Type
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Manual Testing with Accessibility Inspector
|
||||
- Xcode → Open Developer Tool → Accessibility Inspector
|
||||
- Audit each screen for contrast warnings and missing labels
|
||||
|
||||
### VoiceOver Testing (per screen)
|
||||
1. Enable VoiceOver (Settings → Accessibility → VoiceOver)
|
||||
2. Navigate entire screen with swipe gestures
|
||||
3. Verify all interactive elements are announced correctly
|
||||
4. Verify logical reading order
|
||||
5. Test activation (double-tap) of buttons and controls
|
||||
|
||||
### Dynamic Type Testing
|
||||
- Settings → Accessibility → Display & Text Size → Larger Text
|
||||
- Test at default, Large, and Accessibility XXL sizes
|
||||
- Check for truncation, overlap, or layout breaks
|
||||
|
||||
### Contrast Validation
|
||||
- Use WebAIM Contrast Checker or similar tool
|
||||
- Validate all Monochrome text/background combinations
|
||||
- Document final contrast ratios in ACCESSIBILITY.md
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Fix Monochrome theme colors in `Theme.swift`
|
||||
2. Update `SettingsView.swift` theme picker with accessible badge
|
||||
3. Add accessibility labels to all 20 view files (alphabetically by feature)
|
||||
4. Audit touch targets and fix undersized elements
|
||||
5. Create `docs/ACCESSIBILITY.md` with guidelines
|
||||
6. Full VoiceOver testing pass
|
||||
7. Full contrast validation pass
|
||||
Reference in New Issue
Block a user