fix(a11y): add accessible names to hud sliders and inputs
Add explicit aria-label attributes to unlabelled interactive controls in index.html and dynamic parameter sliders in src/ui.js, fulfilling WCAG 2.1 Success Criterion 4.1.2 (Name, Role, Value). - Add aria-label to #scope-feather-slider, #bloom-intensity-slider, #sharpen-intensity-slider, #location-search, #scene-import-file, and first-run suppression checkbox. - Set aria-label to uniform label on dynamic style parameter sliders in ui.js. - Add unit test suite (src/hudA11y.test.mjs) verifying accessible names across all inputs in index.html and dynamic slider construction. Signed-off-by: Kushagra Kumar <kkushagra86@gmail.com>
This commit is contained in:
parent
759652207f
commit
9e92ab6f9b
12
index.html
12
index.html
|
|
@ -440,7 +440,7 @@
|
|||
mirrors SCOPE_FEATHER_RATIO_DEFAULT in src/scopeMask.js. Feather
|
||||
softens the black mask edge; label fading is the separate Fade
|
||||
slider. -->
|
||||
<input type="range" class="pp-slider" id="scope-feather-slider" min="0" max="100" value="11" title="Scope edge feather as a percentage of the keyhole radius" />
|
||||
<input type="range" class="pp-slider" id="scope-feather-slider" min="0" max="100" value="11" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" />
|
||||
<span class="pp-slider-value" id="scope-feather-value">11%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -460,7 +460,7 @@
|
|||
<span class="pp-label">Bloom</span>
|
||||
</button>
|
||||
<div class="pp-slider-row" id="bloom-slider-row">
|
||||
<input type="range" class="pp-slider" id="bloom-intensity-slider" min="0" max="200" value="0" />
|
||||
<input type="range" class="pp-slider" id="bloom-intensity-slider" min="0" max="200" value="0" aria-label="Bloom intensity" />
|
||||
<span class="pp-slider-value" id="bloom-intensity-value">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -470,7 +470,7 @@
|
|||
<span class="pp-label">Sharpen</span>
|
||||
</button>
|
||||
<div class="pp-slider-row" id="sharpen-slider-row">
|
||||
<input type="range" class="pp-slider" id="sharpen-intensity-slider" min="0" max="100" value="49" />
|
||||
<input type="range" class="pp-slider" id="sharpen-intensity-slider" min="0" max="100" value="49" aria-label="Sharpen intensity" />
|
||||
<span class="pp-slider-value" id="sharpen-intensity-value">49%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -565,7 +565,7 @@
|
|||
<button id="search-toggle" class="search-toggle-btn" title="Search any location">
|
||||
<span>🔎</span>
|
||||
</button>
|
||||
<input type="text" id="location-search" placeholder="Search any location..." autocomplete="off" spellcheck="false" />
|
||||
<input type="text" id="location-search" placeholder="Search any location..." autocomplete="off" spellcheck="false" aria-label="Search location by name or coordinates" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -671,7 +671,7 @@
|
|||
<button id="scene-export-btn" class="scene-btn">EXPORT PRESETS</button>
|
||||
<button id="scene-import-btn" class="scene-btn">IMPORT</button>
|
||||
<button id="scene-download-btn" class="scene-btn">RUN LOG</button>
|
||||
<input type="file" id="scene-import-file" accept="application/json,.json" hidden />
|
||||
<input type="file" id="scene-import-file" accept="application/json,.json" hidden aria-label="Import scene JSON file" />
|
||||
</div>
|
||||
<div class="scene-progress">
|
||||
<div id="scene-progress-fill">0%</div>
|
||||
|
|
@ -872,7 +872,7 @@
|
|||
</div>
|
||||
<div class="first-run-footer">
|
||||
<label class="first-run-suppress">
|
||||
<input type="checkbox" data-first-run-suppress />
|
||||
<input type="checkbox" data-first-run-suppress aria-label="Do not show this message again on startup" />
|
||||
<span>Don't show this again</span>
|
||||
</label>
|
||||
<span>ESC to dismiss</span>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
test('index.html: all interactive input elements have accessible names (WCAG 4.1.2)', () => {
|
||||
const htmlPath = path.resolve(process.cwd(), 'index.html');
|
||||
const html = readFileSync(htmlPath, 'utf8');
|
||||
|
||||
// Match all <input ... /> tags
|
||||
const inputMatches = [...html.matchAll(/<input\b([^>]*)\/?>/g)];
|
||||
assert.ok(inputMatches.length > 5, 'expected to find interactive input elements');
|
||||
|
||||
for (const match of inputMatches) {
|
||||
const attrs = match[1];
|
||||
const hasAriaLabel = /aria-label=["'][^"']+["']/.test(attrs);
|
||||
const hasAriaLabelledby = /aria-labelledby=["'][^"']+["']/.test(attrs);
|
||||
const idMatch = attrs.match(/\bid=["']([^"']+)["']/);
|
||||
const id = idMatch ? idMatch[1] : null;
|
||||
|
||||
let hasAssociatedLabel = false;
|
||||
if (id) {
|
||||
// Check if there is a <label for="id">
|
||||
const labelForRegex = new RegExp(`<label\\b[^>]*\\bfor=["']${id}["']`, 'i');
|
||||
hasAssociatedLabel = labelForRegex.test(html);
|
||||
}
|
||||
|
||||
const hasAccessibleName = hasAriaLabel || hasAriaLabelledby || hasAssociatedLabel;
|
||||
assert.ok(
|
||||
hasAccessibleName,
|
||||
`input tag "${match[0]}" must have an accessible name (aria-label, aria-labelledby, or label for)`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('index.html: HUD sliders and controls have descriptive accessible labels', () => {
|
||||
const htmlPath = path.resolve(process.cwd(), 'index.html');
|
||||
const html = readFileSync(htmlPath, 'utf8');
|
||||
|
||||
assert.match(html, /id="scope-feather-slider"[^>]*aria-label="Scope edge feather"/);
|
||||
assert.match(html, /id="bloom-intensity-slider"[^>]*aria-label="Bloom intensity"/);
|
||||
assert.match(html, /id="sharpen-intensity-slider"[^>]*aria-label="Sharpen intensity"/);
|
||||
assert.match(html, /id="location-search"[^>]*aria-label="Search location by name or coordinates"/);
|
||||
assert.match(html, /data-first-run-suppress[^>]*aria-label="Do not show this message again on startup"/);
|
||||
});
|
||||
|
||||
test('ui.js: dynamic style parameter sliders set aria-label', () => {
|
||||
const uiPath = path.resolve(process.cwd(), 'src/ui.js');
|
||||
const uiJs = readFileSync(uiPath, 'utf8');
|
||||
|
||||
assert.match(uiJs, /slider\.setAttribute\(['"]aria-label['"],\s*uMeta\.label\)/);
|
||||
});
|
||||
|
|
@ -8930,6 +8930,7 @@ export class StyleManager {
|
|||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.className = 'param-slider';
|
||||
slider.setAttribute('aria-label', uMeta.label);
|
||||
slider.min = uMeta.min;
|
||||
slider.max = uMeta.max;
|
||||
slider.step = uMeta.max <= 1 ? '0.01' : '0.1';
|
||||
|
|
|
|||
Loading…
Reference in New Issue