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", "id": "5.9",
"name": "Accessibility audit", "name": "Accessibility audit",
"description": "Verify color contrast ratios, focus indicators, screen reader compatibility", "description": "Verify color contrast ratios, focus indicators, screen reader compatibility",
"completed": false, "completed": true,
"tested": false, "tested": false,
"priority": "high", "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", "id": "5.10",

View File

@ -285,6 +285,31 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor
// Initialize any content-editable fields // Initialize any content-editable fields
this._initializeEditors(); 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 // Foundry sheet wrapper styles
.sheet.vagabond { .sheet.vagabond {
@include custom-scrollbar; @include custom-scrollbar;

View File

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

View File

@ -19,12 +19,12 @@ $color-text-inverse: #f5f0e1; // Text on dark backgrounds
// Accent colors // Accent colors
$color-accent-primary: #8b4513; // Saddle brown - primary actions $color-accent-primary: #8b4513; // Saddle brown - primary actions
$color-accent-secondary: #654321; // Dark brown - secondary $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 // Semantic colors
$color-success: #2d5a27; // Dark green - success/healing $color-success: #2d5a27; // Dark green - success/healing
$color-danger: #8b0000; // Dark red - damage/danger $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 $color-info: #2f4f4f; // Dark slate gray - info
// Stat colors (for visual distinction) // Stat colors (for visual distinction)

View File

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

View File

@ -7,9 +7,10 @@
<div class="stats-grid"> <div class="stats-grid">
{{#each stats}} {{#each stats}}
<div class="stat-block {{this.color}}"> <div class="stat-block {{this.color}}">
<label class="stat-label" data-tooltip="{{localize this.label}}">{{this.abbr}}</label> <label class="stat-label" for="stat-{{this.id}}" data-tooltip="{{localize this.label}}">{{this.abbr}}</label>
<input type="number" class="stat-value" name="{{this.path}}" <input type="number" class="stat-value" id="stat-{{this.id}}" name="{{this.path}}"
value="{{this.value}}" min="1" max="10" /> value="{{this.value}}" min="1" max="10"
aria-label="{{localize this.label}}" />
</div> </div>
{{/each}} {{/each}}
</div> </div>
@ -22,11 +23,13 @@
<h2 class="section-header">{{localize "VAGABOND.Saves"}}</h2> <h2 class="section-header">{{localize "VAGABOND.Saves"}}</h2>
<div class="saves-list"> <div class="saves-list">
{{#each saves}} {{#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-label">{{localize this.label}}</span>
<span class="save-stats">({{this.stats}})</span> <span class="save-stats">({{this.stats}})</span>
<span class="save-difficulty">{{this.difficulty}}</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> </div>
{{/each}} {{/each}}
</div> </div>
@ -73,13 +76,15 @@
<h2 class="section-header">{{localize "VAGABOND.Attacks"}}</h2> <h2 class="section-header">{{localize "VAGABOND.Attacks"}}</h2>
<div class="attack-skills-grid"> <div class="attack-skills-grid">
{{#each attackSkills}} {{#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-name">{{localize this.label}}</span>
<span class="attack-stat">({{this.statAbbr}})</span> <span class="attack-stat">({{this.statAbbr}})</span>
<span class="attack-difficulty">{{this.difficulty}}</span> <span class="attack-difficulty">{{this.difficulty}}</span>
{{#if this.hasCritBonus}} {{#if this.hasCritBonus}}
<span class="attack-crit"> <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> </span>
{{/if}} {{/if}}
</div> </div>