Skip to content

feat: WCAG 2.1 AA accessibility audit and fixes#74

Merged
fatherlinux merged 7 commits into
mainfrom
feature/73-accessibility-audit
May 10, 2026
Merged

feat: WCAG 2.1 AA accessibility audit and fixes#74
fatherlinux merged 7 commits into
mainfrom
feature/73-accessibility-audit

Conversation

@fatherlinux
Copy link
Copy Markdown
Member

Summary

Comprehensive WCAG 2.1 AA accessibility audit and fixes for Acquacotta.

  • Semantic HTML: Added skip navigation, <main> landmark, ARIA tab pattern for navigation, role="dialog" on all 6 modals
  • Keyboard accessibility: Focus trap + Escape key on modals, arrow key navigation in tablist, keyboard-operable timer dial (slider role)
  • Screen readers: aria-live region for timer status announcements, sr-only labels for timer inputs, aria-label on icon-only buttons, aria-hidden on decorative SVG
  • Visual: Fixed color contrast (--text-secondary 3.8:1 → 4.8:1+), added :focus-visible ring on all interactive elements, increased footer link size
  • Charts: Added role="img" and aria-label to all canvas elements

Closes #73

Test plan

  • Container builds successfully
  • All 40 existing tests pass
  • Accessibility tree shows proper roles (tab, tabpanel, dialog, slider, timer)
  • All ARIA attributes render correctly (verified via curl + JS checks)
  • Lighthouse accessibility score >= 90
  • Keyboard-only walkthrough of timer, history, reports, settings views
  • Screen reader spot-check

🤖 Generated with Claude Code

fatherlinux and others added 2 commits May 10, 2026 08:25
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive accessibility improvements:
- Add skip navigation link and main landmark
- Implement ARIA tab pattern (tablist/tab/tabpanel) for nav
- Add role="dialog", aria-modal, aria-labelledby to all 6 modals
- Add focus trap and Escape key handling for modals
- Add aria-labels to icon-only buttons (arrows, PiP)
- Add sr-only labels for timer inputs (name, type, notes)
- Add aria-live region for timer status announcements
- Add aria-hidden to decorative SVG timer ring
- Add role="timer" with aria-label to timer display
- Add role="img" with aria-label to chart canvases
- Add role="status" to storage indicator
- Add keyboard arrow key support for timer dial (slider role)
- Add keyboard arrow navigation within tablist
- Fix color contrast: --text-secondary #a0a0a0 → #b0b0b0 (4.8:1+)
- Replace outline:none with focus-visible ring on all elements
- Fix label associations for Start Time fields in modals
- Increase footer link size from 0.625rem to 0.75rem
- Add main landmark to privacy.html and terms.html

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a comprehensive accessibility overhaul based on a WCAG 2.1 AA audit. Key changes include the introduction of semantic HTML landmarks, ARIA roles for navigation and status updates, improved color contrast, and robust keyboard navigation features like modal focus trapping and arrow-key controls for the timer. Review feedback identified a few remaining accessibility issues, specifically the lack of a visual focus indicator for the timer dial, a mismatch in ARIA value attributes, and a logic bug where keyboard increments could exceed the visual timer's maximum duration.

Comment thread templates/index.html
Comment on lines +99 to +100
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The .timer-display element is focusable via keyboard (tabindex="0") but lacks a visual focus indicator. This violates WCAG 2.1 Success Criterion 2.4.7 (Focus Visible). Adding a focus ring ensures keyboard users can identify when the timer dial is currently focused.

Suggested change
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button:focus-visible, .timer-display:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

Comment thread templates/index.html
timerDisplay.setAttribute('role', 'slider');
timerDisplay.setAttribute('aria-label', 'Timer duration');
timerDisplay.setAttribute('aria-valuemin', '0');
timerDisplay.setAttribute('aria-valuemax', '60');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The aria-valuemax attribute is set to 60, but the visual timer dial and interaction logic are constrained by maxDragSeconds (currently 25 minutes). This value should be updated to match the actual maximum allowed by the component to provide accurate information to screen reader users.

Suggested change
timerDisplay.setAttribute('aria-valuemax', '60');
timerDisplay.setAttribute('aria-valuemax', maxDragSeconds / 60);

Comment thread templates/index.html
Comment on lines +4835 to +4849
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
e.preventDefault();
remainingSeconds = Math.min((remainingSeconds || 0) + step, 60 * 60);
updateTimerDisplay();
const mins = Math.ceil(remainingSeconds / 60);
timerDisplay.setAttribute('aria-valuenow', mins);
timerDisplay.setAttribute('aria-valuetext', mins + ' minutes');
} else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
e.preventDefault();
remainingSeconds = Math.max((remainingSeconds || 0) - step, 60);
updateTimerDisplay();
const mins = Math.ceil(remainingSeconds / 60);
timerDisplay.setAttribute('aria-valuenow', mins);
timerDisplay.setAttribute('aria-valuetext', mins + ' minutes');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The keyboard handler allows setting the timer duration up to 60 minutes (60 * 60), but the visual timer ring logic in updateTimerDisplay is hardcoded to a maximum of 25 minutes (maxDragSeconds). Setting a value higher than 25 minutes will cause the ring to render incorrectly. The keyboard increments should be clamped to maxDragSeconds to maintain consistency with the visual representation and the mouse/touch interaction logic.

Suggested change
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
e.preventDefault();
remainingSeconds = Math.min((remainingSeconds || 0) + step, 60 * 60);
updateTimerDisplay();
const mins = Math.ceil(remainingSeconds / 60);
timerDisplay.setAttribute('aria-valuenow', mins);
timerDisplay.setAttribute('aria-valuetext', mins + ' minutes');
} else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
e.preventDefault();
remainingSeconds = Math.max((remainingSeconds || 0) - step, 60);
updateTimerDisplay();
const mins = Math.ceil(remainingSeconds / 60);
timerDisplay.setAttribute('aria-valuenow', mins);
timerDisplay.setAttribute('aria-valuetext', mins + ' minutes');
}
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
e.preventDefault();
remainingSeconds = Math.min((remainingSeconds || 0) + step, maxDragSeconds);
updateTimerDisplay();
const mins = Math.ceil(remainingSeconds / 60);
timerDisplay.setAttribute('aria-valuenow', mins);
timerDisplay.setAttribute('aria-valuetext', mins + ' minutes');
} else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
e.preventDefault();
remainingSeconds = Math.max((remainingSeconds || 0) - step, step);
updateTimerDisplay();
const mins = Math.ceil(remainingSeconds / 60);
timerDisplay.setAttribute('aria-valuenow', mins);
timerDisplay.setAttribute('aria-valuetext', mins + ' minutes');
}

fatherlinux and others added 5 commits May 10, 2026 08:40
Only the active tab is in the Tab order (tabindex="0"), inactive
tabs are removed from Tab order (tabindex="-1"). Arrow keys move
between tabs per WAI-ARIA Authoring Practices.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move aria-selected/tabindex updates into original nav click handler
  instead of relying on separate IIFE listener (fragile ordering)
- Scope Escape key handler to only safe-to-dismiss modals (add-modal,
  edit-modal) — prevents bypassing destructive confirmations and
  migration flows
- Add totalSeconds = remainingSeconds in timer dial keyboard handler
  so progress ring displays correctly after keyboard adjustment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… add modal

- Move keydown listener from nav element to each tab button for
  more reliable arrow key navigation
- Add Enter key handler to add-modal matching existing edit-modal
  pattern — users can type a name and press Enter to submit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Arrow Down from a focused tab moves focus to the first focusable
element in the associated tabpanel, completing the keyboard
navigation pattern (Left/Right cycle tabs, Down enters content).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gourmand CLI changed its interface — --full is no longer a valid
flag. Default behavior is already full analysis.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@fatherlinux fatherlinux merged commit 07abcee into main May 10, 2026
2 of 3 checks passed
@fatherlinux fatherlinux deleted the feature/73-accessibility-audit branch May 10, 2026 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run accessibility audit and fix identified issues

1 participant