Improve app accessibility affordances
This commit is contained in:
parent
1f4784653c
commit
16c173d227
@ -264,8 +264,8 @@
|
||||
|
||||
</div><!-- /app-shell -->
|
||||
|
||||
<div id="toast"></div>
|
||||
<div id="status-bar">Loading…</div>
|
||||
<div id="toast" aria-live="polite" aria-atomic="true"></div>
|
||||
<div id="status-bar" role="status" aria-live="polite" aria-atomic="true">Loading…</div>
|
||||
|
||||
<!-- WaveSurfer — self-hosted, loaded synchronously (voice waveform requires it) -->
|
||||
<script src="/static/vendor/wavesurfer.min.js"></script>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user