From 16c173d2278811d4b3610400723f9173bad4484c Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 5 Jun 2026 22:08:13 +0200 Subject: [PATCH] Improve app accessibility affordances --- static/index.html | 4 +-- static/js/utils.js | 73 ++++++++++++++++++++++++++++++++++++++++++++++ static/loader.js | 1 + 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/static/index.html b/static/index.html index 404c370..9077425 100644 --- a/static/index.html +++ b/static/index.html @@ -264,8 +264,8 @@ -
-
Loading…
+
+
Loading…
diff --git a/static/js/utils.js b/static/js/utils.js index 431fd7a..954972e 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -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 ──────────────────────────────────────────────────── function applyTheme(t) { diff --git a/static/loader.js b/static/loader.js index e74425a..af80c29 100644 --- a/static/loader.js +++ b/static/loader.js @@ -149,6 +149,7 @@ // ── i18n: add the language picker and translate the (now fully-rendered) UI ── if (window.initLangPicker) window.initLangPicker(); if (window.applyI18n) window.applyI18n(document); + if (window.initAccessibilityEnhancements) window.initAccessibilityEnhancements(); // E — post-init modules load in background AFTER the UI is already visible. // Engines, AI backends, generation, and conversation sections load while