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:
parent
8b9daa1f36
commit
26cac676f0
@ -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",
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user