Improve app accessibility affordances

This commit is contained in:
mARTin-B78 2026-06-05 22:08:13 +02:00
parent 1f4784653c
commit 16c173d227
3 changed files with 76 additions and 2 deletions

View File

@ -264,8 +264,8 @@
</div><!-- /app-shell --> </div><!-- /app-shell -->
<div id="toast"></div> <div id="toast" aria-live="polite" aria-atomic="true"></div>
<div id="status-bar">Loading…</div> <div id="status-bar" role="status" aria-live="polite" aria-atomic="true">Loading…</div>
<!-- WaveSurfer — self-hosted, loaded synchronously (voice waveform requires it) --> <!-- WaveSurfer — self-hosted, loaded synchronously (voice waveform requires it) -->
<script src="/static/vendor/wavesurfer.min.js"></script> <script src="/static/vendor/wavesurfer.min.js"></script>

View File

@ -139,6 +139,79 @@ async function clientAutoTrimBounds(fid) {
}; };
} }
// ── Accessibility helpers ────────────────────────────────────────────────
function _a11yText(el) {
return String(el?.textContent || '').replace(/\s+/g, ' ').trim();
}
function _a11yLabel(el) {
return el?.getAttribute('aria-label') || el?.getAttribute('title') || _a11yText(el) || '';
}
function enhanceAccessibility(root = document) {
const scope = root && root.querySelectorAll ? root : document;
const statusBar = $('status-bar');
if (statusBar) {
statusBar.setAttribute('role', 'status');
statusBar.setAttribute('aria-live', 'polite');
statusBar.setAttribute('aria-atomic', 'true');
}
const toastEl = $('toast');
if (toastEl) {
toastEl.setAttribute('aria-live', 'polite');
toastEl.setAttribute('aria-atomic', 'true');
}
scope.querySelectorAll('.nav-item, .nav-tree-item, .tab, [onclick]').forEach(el => {
if (/^(A|BUTTON|INPUT|SELECT|TEXTAREA|SUMMARY)$/i.test(el.tagName)) return;
if (!el.hasAttribute('role')) el.setAttribute('role', 'button');
if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0');
const label = _a11yLabel(el);
if (label && !el.getAttribute('aria-label')) el.setAttribute('aria-label', label);
if (label && !el.getAttribute('title')) el.setAttribute('title', label);
});
scope.querySelectorAll('button, [role=button], a, input, select, textarea').forEach(el => {
const label = _a11yLabel(el);
if (label && !el.getAttribute('title') && el.matches('button, [role=button]')) el.setAttribute('title', label);
if (label && !el.getAttribute('aria-label') && !_a11yText(el) && el.matches('button, [role=button]')) el.setAttribute('aria-label', label);
});
scope.querySelectorAll('button .mdi, [role=button] .mdi, .nav-icon .mdi').forEach(icon => {
icon.setAttribute('aria-hidden', 'true');
});
document.querySelectorAll('.nav-item, .nav-tree-item').forEach(el => {
const active = el.classList.contains('active') || el.classList.contains('is-active');
if (active && el.getAttribute('aria-current') !== 'page') el.setAttribute('aria-current', 'page');
else if (!active && el.hasAttribute('aria-current')) el.removeAttribute('aria-current');
});
}
function initAccessibilityEnhancements() {
if (window.__ttsvcA11yReady) { enhanceAccessibility(document); return; }
window.__ttsvcA11yReady = true;
enhanceAccessibility(document);
document.addEventListener('keydown', (e) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
const el = e.target?.closest?.('[role=button]');
if (!el || /^(BUTTON|A|INPUT|SELECT|TEXTAREA)$/i.test(el.tagName)) return;
e.preventDefault();
el.click();
});
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList') {
m.addedNodes.forEach(n => { if (n.nodeType === 1) enhanceAccessibility(n); });
} else if (m.type === 'attributes') {
enhanceAccessibility(document);
}
}
});
obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
}
// ── Light / dark theme ──────────────────────────────────────────────────── // ── Light / dark theme ────────────────────────────────────────────────────
function applyTheme(t) { function applyTheme(t) {

View File

@ -149,6 +149,7 @@
// ── i18n: add the language picker and translate the (now fully-rendered) UI ── // ── i18n: add the language picker and translate the (now fully-rendered) UI ──
if (window.initLangPicker) window.initLangPicker(); if (window.initLangPicker) window.initLangPicker();
if (window.applyI18n) window.applyI18n(document); if (window.applyI18n) window.applyI18n(document);
if (window.initAccessibilityEnhancements) window.initAccessibilityEnhancements();
// E — post-init modules load in background AFTER the UI is already visible. // E — post-init modules load in background AFTER the UI is already visible.
// Engines, AI backends, generation, and conversation sections load while // Engines, AI backends, generation, and conversation sections load while