Complete accessibility audit (Task 5.9)

Color Contrast (WCAG AA 4.5:1 compliance):
- accent-highlight: #cd853f → #7a4f1d (5.23:1)
- warning: #b8860b → #705308 (5.29:1)
- dark theme muted: #8a7e6e → #9a8e7e (5.39:1)
- dark theme warning: #d4a32c → #c99020 (6.17:1)

Accessibility Utilities (_base.scss):
- .sr-only for screen reader only content
- .skip-link visible on focus
- .interactive-row with focus-visible styles
- prefers-reduced-motion media query support

Interactive Element Improvements:
- Save/attack rows: role="button", tabindex="0", aria-label
- Stat inputs: proper label for= associations
- Decorative icons: aria-hidden="true"
- Keyboard activation via _setupKeyboardAccessibility()

Editor Fix:
- Simplified editor-wrapper styles to restore ProseMirror toggle button
- Resize feature remains deferred to Task 5.11

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-12-15 15:56:52 -06:00
parent 8b9daa1f36
commit 26cac676f0
7 changed files with 115 additions and 37 deletions

View File

@ -710,10 +710,11 @@
"id": "5.9",
"name": "Accessibility audit",
"description": "Verify color contrast ratios, focus indicators, screen reader compatibility",
"completed": false,
"completed": true,
"tested": false,
"priority": "high",
"dependencies": ["5.4", "5.5", "5.6"]
"dependencies": ["5.4", "5.5", "5.6"],
"notes": "Fixed WCAG AA compliance: accent-highlight (#7a4f1d, 5.23:1), warning (#705308, 5.29:1), dark theme muted text (#9a8e7e, 5.39:1). Added .sr-only utility, .interactive-row with focus-visible, prefers-reduced-motion support. Interactive rows (saves, attacks) have role=button, tabindex=0, aria-labels. Stat inputs have proper label for= associations. Keyboard accessibility via _setupKeyboardAccessibility() for Enter/Space activation."
},
{
"id": "5.10",

View File

@ -285,6 +285,31 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor
// Initialize any content-editable fields
this._initializeEditors();
// Add keyboard accessibility for interactive rows
this._setupKeyboardAccessibility();
}
/**
* Set up keyboard event listeners for elements with role="button".
* This enables Enter/Space key activation for accessibility.
* @protected
*/
_setupKeyboardAccessibility() {
if (!this.element) return;
// Find all elements with role="button" that have data-action
const interactiveElements = this.element.querySelectorAll('[role="button"][data-action]');
for (const el of interactiveElements) {
el.addEventListener("keydown", (event) => {
// Trigger click on Enter or Space
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
el.click();
}
});
}
}
/**

View File

@ -140,6 +140,69 @@
}
}
// Accessibility utilities
.vagabond {
// Screen reader only (visually hidden but accessible)
.sr-only {
@include sr-only;
}
// Skip link (visible on focus)
.skip-link {
@include sr-only;
&:focus {
position: fixed;
top: $spacing-2;
left: $spacing-2;
z-index: $z-tooltip;
width: auto;
height: auto;
padding: $spacing-2 $spacing-4;
margin: 0;
overflow: visible;
clip: auto;
white-space: normal;
background-color: var(--color-bg-primary);
border: 2px solid var(--color-accent-primary);
border-radius: $radius-md;
color: var(--color-text-primary);
}
}
// Interactive row styling (for keyboard accessible rows)
.interactive-row {
cursor: pointer;
transition: background-color $transition-fast;
&:hover {
background-color: var(--color-bg-highlight);
}
&:focus-visible {
outline: 2px solid var(--color-accent-primary);
outline-offset: -2px;
background-color: var(--color-bg-highlight);
}
// Active state for touch/click feedback
&:active {
background-color: var(--color-bg-tertiary);
}
}
// Ensure reduced motion preference is respected
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
}
// Foundry sheet wrapper styles
.sheet.vagabond {
@include custom-scrollbar;

View File

@ -94,7 +94,7 @@ body.theme-dark .vagabond:not(.theme-light),
// Light text colors (parchment-tinted)
--color-text-primary: #e8dcc8;
--color-text-secondary: #c4b8a4;
--color-text-muted: #8a7e6e;
--color-text-muted: #9a8e7e; // Lightened for WCAG AA contrast (4.5:1)
--color-text-inverse: #1e1a16;
// Accent colors (slightly brighter for dark bg)
@ -105,7 +105,7 @@ body.theme-dark .vagabond:not(.theme-light),
// Semantic colors (adjusted for dark backgrounds)
--color-success: #4a9f42;
--color-danger: #c94444;
--color-warning: #d4a32c;
--color-warning: #c99020; // Adjusted for contrast
--color-info: #4a8080;
// Stat colors (brighter for dark bg)

View File

@ -19,12 +19,12 @@ $color-text-inverse: #f5f0e1; // Text on dark backgrounds
// Accent colors
$color-accent-primary: #8b4513; // Saddle brown - primary actions
$color-accent-secondary: #654321; // Dark brown - secondary
$color-accent-highlight: #cd853f; // Peru - highlights/hover
$color-accent-highlight: #7a4f1d; // Dark bronze - highlights/hover (5.23:1 WCAG AA)
// Semantic colors
$color-success: #2d5a27; // Dark green - success/healing
$color-danger: #8b0000; // Dark red - damage/danger
$color-warning: #b8860b; // Dark goldenrod - warnings
$color-warning: #705308; // Dark gold - warnings (5.29:1 WCAG AA)
$color-info: #2f4f4f; // Dark slate gray - info
// Stat colors (for visual distinction)

View File

@ -328,9 +328,6 @@
border-radius: $radius-sm;
background-color: var(--color-bg-input);
min-height: 120px;
max-height: 400px;
overflow: auto;
resize: vertical;
// Static content display (non-editable)
> .editor-content {
@ -340,13 +337,11 @@
// ProseMirror custom element styling
prose-mirror {
display: flex;
flex-direction: column;
display: block;
min-height: 100px;
// The menu bar that appears when editing
> menu.editor-menu {
flex: 0 0 auto;
position: sticky;
top: 0;
z-index: 1;
@ -356,21 +351,18 @@
// Foundry's inner editor-container
> .editor-container {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 80px;
// The actual editable content area
> .editor-content {
flex: 1 1 auto;
padding: $spacing-2;
min-height: 60px;
}
}
// Toggle button
// Toggle button - keep visible for editing
> button.toggle {
display: none; // Hide since we use toggled="false"
// Don't hide - let Foundry handle it
}
}
}
@ -602,9 +594,6 @@
border-radius: $radius-sm;
background-color: var(--color-bg-input);
min-height: 80px;
max-height: 300px;
overflow: auto;
resize: vertical;
// Static content display (non-editable)
> .editor-content {
@ -614,13 +603,11 @@
// ProseMirror custom element styling
prose-mirror {
display: flex;
flex-direction: column;
display: block;
min-height: 60px;
// The menu bar that appears when editing
> menu.editor-menu {
flex: 0 0 auto;
position: sticky;
top: 0;
z-index: 1;
@ -630,21 +617,18 @@
// Foundry's inner editor-container
> .editor-container {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 40px;
// The actual editable content area
> .editor-content {
flex: 1 1 auto;
padding: $spacing-2;
min-height: 30px;
}
}
// Toggle button
// Toggle button - keep visible for editing
> button.toggle {
display: none;
// Don't hide - let Foundry handle it
}
}
}

View File

@ -7,9 +7,10 @@
<div class="stats-grid">
{{#each stats}}
<div class="stat-block {{this.color}}">
<label class="stat-label" data-tooltip="{{localize this.label}}">{{this.abbr}}</label>
<input type="number" class="stat-value" name="{{this.path}}"
value="{{this.value}}" min="1" max="10" />
<label class="stat-label" for="stat-{{this.id}}" data-tooltip="{{localize this.label}}">{{this.abbr}}</label>
<input type="number" class="stat-value" id="stat-{{this.id}}" name="{{this.path}}"
value="{{this.value}}" min="1" max="10"
aria-label="{{localize this.label}}" />
</div>
{{/each}}
</div>
@ -22,11 +23,13 @@
<h2 class="section-header">{{localize "VAGABOND.Saves"}}</h2>
<div class="saves-list">
{{#each saves}}
<div class="save-row" data-action="rollSave" data-save="{{this.id}}">
<div class="save-row interactive-row" role="button" tabindex="0"
data-action="rollSave" data-save="{{this.id}}"
aria-label="{{localize this.label}} {{localize 'VAGABOND.Save'}}: {{localize 'VAGABOND.Difficulty'}} {{this.difficulty}}">
<span class="save-label">{{localize this.label}}</span>
<span class="save-stats">({{this.stats}})</span>
<span class="save-difficulty">{{this.difficulty}}</span>
<i class="fa-solid fa-dice-d20 roll-icon"></i>
<i class="fa-solid fa-dice-d20 roll-icon" aria-hidden="true"></i>
</div>
{{/each}}
</div>
@ -73,13 +76,15 @@
<h2 class="section-header">{{localize "VAGABOND.Attacks"}}</h2>
<div class="attack-skills-grid">
{{#each attackSkills}}
<div class="attack-skill-row" data-action="rollAttack" data-attack-skill="{{this.id}}">
<div class="attack-skill-row interactive-row" role="button" tabindex="0"
data-action="rollAttack" data-attack-skill="{{this.id}}"
aria-label="{{localize this.label}} {{localize 'VAGABOND.Attack'}}: {{localize 'VAGABOND.Difficulty'}} {{this.difficulty}}">
<span class="attack-name">{{localize this.label}}</span>
<span class="attack-stat">({{this.statAbbr}})</span>
<span class="attack-difficulty">{{this.difficulty}}</span>
{{#if this.hasCritBonus}}
<span class="attack-crit">
<i class="fa-solid fa-star"></i>{{this.critThreshold}}
<i class="fa-solid fa-star" aria-hidden="true"></i>{{this.critThreshold}}
</span>
{{/if}}
</div>