Improve accessibility across core flows (#86)
First incremental pass at issue #86, focused on the universal entry points and primary navigation. All changes verified in-browser with the axe-core engine (0 violations on the surfaces below) plus manual keyboard testing, on both desktop (1280px) and mobile (390px). Login / first-run setup (static/login.html) - Add a real <h1>, wrap content in <main> + <footer> landmarks. - Mark the decorative boat SVG aria-hidden. - Errors now use role="alert" so screen readers announce them. - "Remember me" checkbox is keyboard-focusable (was display:none) with an accessible name and a focus ring; dynamic 2FA field gets a linked label. - Darken the brand-red submit button so white text clears WCAG AA 4.5:1 (was ~3.2:1); add visible :focus-visible rings. App shell (static/index.html, static/style.css) - Remove invalid role="region" from the <main> chat container (it was overriding the implicit main landmark). - Add a persistent, visually-hidden <h1> inside <main> so the page always exposes one logical level-1 heading — works even on mobile where the sidebar (with the visible brand) is hidden off-canvas. - Add a reusable .a11y-visually-hidden utility. - Raise chat-title, model-picker, settings-helper and notes text contrast above 4.5:1 (were 2.8-3.9:1). Keyboard nav + dialogs (static/js/a11y.js - new) - Make the click-only <div> sidebar navigation (New Chat, Search, Brain, Calendar, Compare, Cookbook, Deep Research, Gallery, Library, Notes, Tasks, Theme, account) focusable and Enter/Space-activatable, announced as buttons (skipping role=button where a nested control would create a nested-interactive violation). Visible focus ring reused from existing .list-item:focus-visible. - Upgrade modals (.modal-content and the docked .notes-pane) to labelled role="dialog" + aria-modal, and normalise their title to heading level 2 so heading order stays valid. A MutationObserver covers runtime-rendered rows and modals. Decorative background canvases (static/js/theme.js) - Mark all 7 bg-effect canvases aria-hidden. Notes & Tasks (static/js/notes.js, static/js/tasks.js) - Label the icon-only Note/To-do toggle pills (fixes a critical button-name issue) and track aria-pressed state. - Improve Notes header-button + empty-state contrast. - Give the Tasks sort <select> an accessible name (fixes a critical select-name issue). Remaining data-dense tool modals (Tasks cards, Calendar, Gallery, Email, Cookbook, Compare, Deep Research) still have muted-text contrast to polish and are the next incremental step, per the issue's own guidance.
This commit is contained in:
@@ -1495,6 +1495,9 @@ function _initSynapse() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'synapse-canvas';
|
||||
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
|
||||
// Decorative background effect — hide from assistive tech so screen readers
|
||||
// don't announce an empty canvas and axe's "region" rule doesn't flag it.
|
||||
canvas.setAttribute('aria-hidden', 'true');
|
||||
document.body.prepend(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
@@ -1588,6 +1591,9 @@ function _initRain() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'rain-canvas';
|
||||
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
|
||||
// Decorative background effect — hide from assistive tech so screen readers
|
||||
// don't announce an empty canvas and axe's "region" rule doesn't flag it.
|
||||
canvas.setAttribute('aria-hidden', 'true');
|
||||
document.body.prepend(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
@@ -1660,6 +1666,9 @@ function _initConstellations() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'constellations-canvas';
|
||||
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
|
||||
// Decorative background effect — hide from assistive tech so screen readers
|
||||
// don't announce an empty canvas and axe's "region" rule doesn't flag it.
|
||||
canvas.setAttribute('aria-hidden', 'true');
|
||||
document.body.prepend(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
@@ -1763,6 +1772,9 @@ function _initPerlinFlow() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'perlin-flow-canvas';
|
||||
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
|
||||
// Decorative background effect — hide from assistive tech so screen readers
|
||||
// don't announce an empty canvas and axe's "region" rule doesn't flag it.
|
||||
canvas.setAttribute('aria-hidden', 'true');
|
||||
document.body.prepend(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
@@ -1818,6 +1830,9 @@ function _initPetals() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'petals-canvas';
|
||||
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
|
||||
// Decorative background effect — hide from assistive tech so screen readers
|
||||
// don't announce an empty canvas and axe's "region" rule doesn't flag it.
|
||||
canvas.setAttribute('aria-hidden', 'true');
|
||||
document.body.prepend(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
@@ -1872,6 +1887,9 @@ function _initSparkles() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'sparkles-canvas';
|
||||
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
|
||||
// Decorative background effect — hide from assistive tech so screen readers
|
||||
// don't announce an empty canvas and axe's "region" rule doesn't flag it.
|
||||
canvas.setAttribute('aria-hidden', 'true');
|
||||
document.body.prepend(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
@@ -1927,6 +1945,9 @@ function _initEmbers() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'embers-canvas';
|
||||
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;';
|
||||
// Decorative background effect — hide from assistive tech so screen readers
|
||||
// don't announce an empty canvas and axe's "region" rule doesn't flag it.
|
||||
canvas.setAttribute('aria-hidden', 'true');
|
||||
document.body.prepend(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
|
||||
Reference in New Issue
Block a user