Improve app accessibility affordances
This commit is contained in:
parent
1f4784653c
commit
16c173d227
@ -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>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user