Files
grimoire/ACCESSIBILITY_PLAN.md
Claude cdad01dc03 docs: add codebase analysis and accessibility plan
CODEBASE_ANALYSIS.md:
- Comprehensive architecture review
- State management analysis (Jotai + EventStore + Dexie)
- Component patterns assessment
- Security audit findings (zero vulnerabilities)
- Performance analysis
- 12-week S-tier improvement roadmap

ACCESSIBILITY_PLAN.md:
- Current state assessment (16% ARIA coverage)
- WCAG 2.1 AA compliance roadmap
- Phased implementation plan:
  - Phase 1: Foundation (keyboard nav, focus management)
  - Phase 2: Screen reader support
  - Phase 3: Visual accessibility
  - Phase 4: Advanced features
- Component-specific accessibility requirements
2025-12-22 12:03:33 +00:00

11 KiB

Accessibility Improvement Plan

This document outlines the accessibility improvements planned for Grimoire to achieve WCAG 2.1 AA compliance.

Current State Assessment

Current Coverage: ~16% of components have ARIA attributes

Category Status Details
Keyboard Navigation ⚠️ Partial Cmd+K works, limited elsewhere
Screen Reader Support ⚠️ Partial Basic labels, missing live regions
Focus Management Good Visible focus rings
Color Contrast ⚠️ Unchecked No WCAG verification
Loading States Good Skeletons with aria-busy
Error Handling ⚠️ Partial Errors not announced

Phase 1: Foundation (Priority: High)

1.1 Keyboard Navigation Improvements

Files to update: CommandLauncher.tsx, Home.tsx, TabBar.tsx

// Add keyboard shortcuts help modal (Cmd+?)
const KEYBOARD_SHORTCUTS = [
  { keys: ['⌘', 'K'], description: 'Open command palette' },
  { keys: ['⌘', '1-9'], description: 'Switch workspace' },
  { keys: ['Escape'], description: 'Close dialog/modal' },
  { keys: ['↑', '↓'], description: 'Navigate list items' },
  { keys: ['Enter'], description: 'Select/confirm' },
];

Tasks:

  • Create KeyboardShortcutsDialog component
  • Add Cmd+? shortcut to show help
  • Add keyboard navigation to window tiles (focus, close, resize)
  • Implement roving tabindex for command list
  • Add skip links for main content areas

1.2 Focus Management

Files to update: components/ui/dialog.tsx, GlobalAuthPrompt.tsx

// Focus trap for modals
import { FocusTrap } from '@radix-ui/react-focus-trap';

// Return focus after dialog close
const previousFocusRef = useRef<HTMLElement | null>(null);

useEffect(() => {
  if (open) {
    previousFocusRef.current = document.activeElement as HTMLElement;
  } else {
    previousFocusRef.current?.focus();
  }
}, [open]);

Tasks:

  • Verify all dialogs trap focus properly
  • Return focus to trigger element on close
  • Add autoFocus to first interactive element in dialogs
  • Prevent focus from leaving modal while open

1.3 Screen Reader Announcements (Live Regions)

Create new file: src/components/ui/Announcer.tsx

import { createContext, useContext, useState, useCallback } from 'react';

interface AnnouncerContextValue {
  announce: (message: string, politeness?: 'polite' | 'assertive') => void;
}

const AnnouncerContext = createContext<AnnouncerContextValue | null>(null);

export function AnnouncerProvider({ children }: { children: React.ReactNode }) {
  const [politeMessage, setPoliteMessage] = useState('');
  const [assertiveMessage, setAssertiveMessage] = useState('');

  const announce = useCallback((message: string, politeness: 'polite' | 'assertive' = 'polite') => {
    if (politeness === 'assertive') {
      setAssertiveMessage(message);
      setTimeout(() => setAssertiveMessage(''), 1000);
    } else {
      setPoliteMessage(message);
      setTimeout(() => setPoliteMessage(''), 1000);
    }
  }, []);

  return (
    <AnnouncerContext.Provider value={{ announce }}>
      {children}
      <div
        aria-live="polite"
        aria-atomic="true"
        className="sr-only"
      >
        {politeMessage}
      </div>
      <div
        aria-live="assertive"
        aria-atomic="true"
        className="sr-only"
      >
        {assertiveMessage}
      </div>
    </AnnouncerContext.Provider>
  );
}

export function useAnnounce() {
  const context = useContext(AnnouncerContext);
  if (!context) throw new Error('useAnnounce must be used within AnnouncerProvider');
  return context.announce;
}

Integration points:

  • Wrap app in AnnouncerProvider
  • Announce when command executes: "Opening profile viewer"
  • Announce when window closes: "Window closed"
  • Announce loading complete: "Timeline loaded, 50 events"
  • Announce errors: "Error: Failed to load profile"

Phase 2: Form Accessibility (Priority: High)

2.1 Form Error Association

Pattern to implement across all forms:

interface FormFieldProps {
  id: string;
  label: string;
  error?: string;
  description?: string;
}

function FormField({ id, label, error, description, children }: FormFieldProps) {
  const errorId = `${id}-error`;
  const descriptionId = `${id}-description`;

  return (
    <div>
      <Label htmlFor={id}>{label}</Label>
      {description && (
        <span id={descriptionId} className="text-sm text-muted-foreground">
          {description}
        </span>
      )}
      {React.cloneElement(children as React.ReactElement, {
        id,
        'aria-describedby': [
          description ? descriptionId : null,
          error ? errorId : null,
        ].filter(Boolean).join(' ') || undefined,
        'aria-invalid': !!error,
      })}
      {error && (
        <span id={errorId} role="alert" className="text-sm text-destructive">
          {error}
        </span>
      )}
    </div>
  );
}

Files to update:

  • SpellDialog.tsx - Spell creation form
  • SettingsDialog.tsx - Settings inputs
  • WorkspaceSettings.tsx - Workspace name input
  • CommandLauncher.tsx - Command input

2.2 Required Field Indicators

// Add to Label component
function Label({ required, children, ...props }) {
  return (
    <label {...props}>
      {children}
      {required && <span aria-hidden="true" className="text-destructive ml-1">*</span>}
      {required && <span className="sr-only"> (required)</span>}
    </label>
  );
}

Phase 3: Component ARIA Improvements (Priority: Medium)

3.1 Event Renderers

Base pattern for all renderers:

// BaseEventRenderer.tsx additions
<article
  aria-label={`${kindName} by ${authorName}`}
  aria-describedby={`event-${event.id}-content`}
>
  <header>
    <UserName pubkey={pubkey} aria-label={`Author: ${displayName}`} />
    <time dateTime={isoDate} aria-label={`Posted ${relativeTime}`}>
      {relativeTime}
    </time>
  </header>
  <div id={`event-${event.id}-content`}>
    {children}
  </div>
</article>

Tasks:

  • Add article landmark to event containers
  • Add proper time elements with dateTime
  • Add aria-labels to interactive elements
  • Ensure all buttons have labels (close, menu, copy, etc.)

3.2 Feed/Timeline Components

Files to update: Feed.tsx, ReqViewer.tsx

// Add feed landmarks
<section aria-label="Event timeline" role="feed" aria-busy={loading}>
  <h2 className="sr-only">Timeline</h2>
  {events.map((event, index) => (
    <article
      key={event.id}
      aria-posinset={index + 1}
      aria-setsize={events.length}
    >
      <KindRenderer event={event} />
    </article>
  ))}
</section>

Tasks:

  • Add role="feed" to timeline containers
  • Add aria-posinset and aria-setsize for virtual lists
  • Add aria-busy during loading
  • Announce when new events arrive

3.3 Collapsible/Accordion

Files to update: ui/accordion.tsx, ui/collapsible.tsx

// Ensure proper ARIA states
<button
  aria-expanded={isOpen}
  aria-controls={contentId}
>
  Toggle
</button>
<div
  id={contentId}
  aria-hidden={!isOpen}
  role="region"
>
  {children}
</div>

Phase 4: Color & Visual Accessibility (Priority: Medium)

4.1 Color Contrast Audit

Tool: Use axe-core for automated checking

npm install -D @axe-core/react
// Development-only accessibility audit
if (process.env.NODE_ENV === 'development') {
  import('@axe-core/react').then(axe => {
    axe.default(React, ReactDOM, 1000);
  });
}

Known issues to check:

  • Muted foreground text (hsl(215 20.2% 70%))
  • Gradient text (.text-grimoire-gradient)
  • Disabled state opacity (50%)
  • Placeholder text color

4.2 High Contrast Mode

Create theme option:

/* Add to index.css */
@media (prefers-contrast: more) {
  :root {
    --foreground: 0 0% 0%;
    --background: 0 0% 100%;
    --muted-foreground: 0 0% 30%;
    --border: 0 0% 0%;
  }
  .dark {
    --foreground: 0 0% 100%;
    --background: 0 0% 0%;
    --muted-foreground: 0 0% 80%;
    --border: 0 0% 100%;
  }
}

Tasks:

  • Add system preference detection
  • Create high-contrast theme variables
  • Test with Windows High Contrast Mode
  • Add manual toggle in settings

4.3 Reduced Motion

/* Already partially implemented, verify coverage */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Tasks:

  • Audit all animations (Framer Motion, CSS transitions)
  • Ensure skeleton pulse respects preference
  • Verify window transitions can be disabled

Phase 5: Testing & Documentation (Priority: High)

5.1 Automated Testing

Add to CI pipeline:

# .github/workflows/accessibility.yml
name: Accessibility Checks
on: [push, pull_request]
jobs:
  a11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run build
      - name: Run axe-core
        run: npx @axe-core/cli http://localhost:4173

5.2 Manual Testing Checklist

Keyboard-only testing:

  • Can navigate entire app without mouse
  • Focus order is logical
  • All interactive elements are reachable
  • Can dismiss dialogs with Escape
  • Can activate buttons with Enter/Space

Screen reader testing (with VoiceOver/NVDA):

  • Page structure is announced correctly
  • Links and buttons describe their purpose
  • Form fields have associated labels
  • Errors are announced when they occur
  • Loading states are announced

Visual testing:

  • Content readable at 200% zoom
  • No horizontal scrolling at 320px width (for non-tiling views)
  • Focus indicators visible
  • Color not sole means of conveying info

5.3 Accessibility Documentation

Create docs/ACCESSIBILITY.md:

  • Document keyboard shortcuts
  • List known limitations
  • Provide screen reader recommendations
  • Document testing procedures

Implementation Phases

Phase 1: Foundation (2-3 weeks)

  • Live region announcer
  • Keyboard shortcuts help
  • Focus management fixes

Phase 2: Forms (1-2 weeks)

  • Error association pattern
  • Required field indicators
  • Form validation feedback

Phase 3: Components (2-3 weeks)

  • Event renderer improvements
  • Feed landmarks
  • Dialog ARIA fixes

Phase 4: Visual (1-2 weeks)

  • Color contrast audit
  • High contrast mode
  • Reduced motion support

Phase 5: Testing (Ongoing)

  • Automated CI checks
  • Manual testing protocol
  • Documentation

Success Metrics

Metric Current Target
axe-core violations Unknown 0 critical, <5 minor
ARIA coverage 16% 90%+
Keyboard accessibility Partial Full
Color contrast ratio Unknown 4.5:1 minimum
WCAG 2.1 Level Unknown AA

Resources