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",
|
"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",
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user