From 91e0858c46812d5fcf0aff65875dc917fb0283ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:46:40 +0000 Subject: [PATCH 1/2] Improve web UI a11y foundations and add jsx-a11y lint tooling (#647) First tranche of the WCAG 2.2 AA audit, following the issue's suggested work order (tooling first, then the small global high-impact fixes): Tooling - Add ESLint flat config (web/eslint.config.js) with eslint-plugin-jsx-a11y recommended ruleset in warn mode and a web `npm run lint` script; register eslint-plugin-react-hooks so pre-existing targeted eslint-disable comments resolve without enabling its rules. - Baseline: 902 jsx-a11y warnings, 0 errors, across 80 files (control-has-associated-label 357, label-has-for 340, mouse-events-have-key-events 68, no-static-element-interactions 44, click-events-have-key-events 39, label-has-associated-control 17, no-noninteractive-element-interactions 17, no-autofocus 10, media-has-caption 6, no-noninteractive-tabindex 2, anchor-is-valid 1, interactive-supports-focus 1). P1-1 (SC 1.4.4/1.4.10): remove maximum-scale/user-scalable=no from index.html and hls.html so page zoom works again. P2-1 (SC 2.4.7/2.4.11/1.4.11): extend the global focus-visible outline fallback to summary/[tabindex]; add scroll-padding-top so the header does not cover focused elements; remove the 33 bare focus:outline-none utilities flagged in the issue (they had no replacement indicator and are superseded by the global rule), plus dead tokens paired with focus:ring in the same files. P2-2 (SC 2.4.1): keyboard-reachable "Skip to main content" link injected from Header.jsx with a localized label (a11y.skipToMainContent added to all 18 locales); tabindex="-1" on every page's #main-content so skip focus lands in the main landmark. --- web/css/base.css | 45 +- web/eslint.config.js | 48 + web/hls.html | 4 +- web/index.html | 4 +- web/investigation.html | 2 +- web/js/components/preact/Header.jsx | 5 +- web/js/components/preact/LiveView.jsx | 32 +- .../components/preact/StreamConfigModal.jsx | 72 +- web/js/components/preact/StreamsView.jsx | 20 +- web/js/components/preact/WebRTCView.jsx | 30 +- .../preact/recordings/ActiveFilters.jsx | 2 +- .../preact/recordings/RecordingsGrid.jsx | 10 +- .../preact/recordings/RecordingsTable.jsx | 10 +- .../preact/recordings/TagsOverlay.jsx | 6 +- web/js/components/preact/system/LogsView.jsx | 10 +- .../preact/timeline/SpeedControls.jsx | 2 +- .../preact/timeline/TimelineControls.jsx | 16 +- web/login.html | 2 +- web/package-lock.json | 2401 ++++++++++++++++- web/package.json | 4 + web/public/locales/ar.json | 1 + web/public/locales/de.json | 1 + web/public/locales/en.json | 1 + web/public/locales/es.json | 1 + web/public/locales/fil.json | 1 + web/public/locales/fr.json | 1 + web/public/locales/it.json | 1 + web/public/locales/ja.json | 1 + web/public/locales/ko.json | 1 + web/public/locales/nl.json | 1 + web/public/locales/pl.json | 1 + web/public/locales/pt-BR.json | 1 + web/public/locales/pt-PT.json | 1 + web/public/locales/ru.json | 1 + web/public/locales/tr.json | 1 + web/public/locales/uk.json | 1 + web/public/locales/zh-TW.json | 1 + web/public/locales/zh.json | 1 + web/recordings.html | 2 +- web/settings.html | 2 +- web/streams.html | 2 +- web/system.html | 2 +- web/timeline.html | 2 +- web/users.html | 2 +- 44 files changed, 2537 insertions(+), 218 deletions(-) create mode 100644 web/eslint.config.js diff --git a/web/css/base.css b/web/css/base.css index 35ff89ab1..967a1d7aa 100644 --- a/web/css/base.css +++ b/web/css/base.css @@ -127,6 +127,12 @@ /* Base Styles */ @layer base { + html { + /* Keep focused elements clear of the fixed page header so focus is not + obscured (WCAG 2.4.11). Roughly the header height plus its margin. */ + scroll-padding-top: 5rem; + } + body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; overflow-y: scroll; @@ -147,11 +153,13 @@ focus-visible treatment. Deliberately use an outline so it remains visible even when a component clips box shadows. */ :where( - button, a[href], + button, input, select, textarea, + summary, + [tabindex], [role="button"] ):focus-visible { outline: 2px solid hsl(var(--ring)) !important; @@ -180,6 +188,41 @@ } } +/* "Skip to main content" link (WCAG 2.4.1): visually hidden until focused. */ +.skip-link { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + +.skip-link:focus-visible { + position: fixed; + top: 1rem; + left: 1rem; + width: auto; + height: auto; + padding: 0.5rem 1rem; + margin: 0; + overflow: visible; + clip: auto; + clip-path: none; + white-space: normal; + z-index: 9999; + background-color: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border-radius: var(--radius); + text-decoration: none; + outline: 2px solid hsl(var(--primary-foreground)); + outline-offset: 2px; +} + /* Component Styles */ @layer components { /* Status Message */ diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 000000000..e9479db0d --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,48 @@ +/** + * ESLint configuration for the LightNVR web UI. + * + * Adds eslint-plugin-jsx-a11y (recommended ruleset) as part of the WCAG 2.2 AA + * audit in https://github.com/opensensor/lightNVR/issues/647. All rules start + * in warn mode so CI reports violations without failing the build; switch rules + * to error as each finding category in the issue is closed. + */ +import jsxA11y from 'eslint-plugin-jsx-a11y'; +import reactHooks from 'eslint-plugin-react-hooks'; + +const recommendedRules = jsxA11y.flatConfigs.recommended.rules; + +const a11yWarnRules = Object.fromEntries( + Object.keys(recommendedRules).map((name) => [name, 'warn']), +); + +export default [ + { + ignores: ['dist/**', 'node_modules/**', 'coverage/**'], + }, + { + files: ['js/**/*.js', 'js/**/*.jsx'], + linterOptions: { + // Targeted disable comments (e.g. react-hooks) belong to configs outside + // this a11y lint; don't flag them as unused here. + reportUnusedDisableDirectives: 'off', + }, + plugins: { 'jsx-a11y': jsxA11y, 'react-hooks': reactHooks }, + languageOptions: { + ...jsxA11y.flatConfigs.recommended.languageOptions, + sourceType: 'module', + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, + }, + rules: { + ...a11yWarnRules, + // The plugin is registered only so pre-existing targeted + // eslint-disable-next-line comments referencing react-hooks rules + // resolve; its rules stay off while this config tracks the a11y audit. + 'react-hooks/exhaustive-deps': 'off', + 'react-hooks/rules-of-hooks': 'off', + }, + }, +]; diff --git a/web/hls.html b/web/hls.html index 1eba2afe6..f47790892 100644 --- a/web/hls.html +++ b/web/hls.html @@ -2,7 +2,7 @@ - + HLS View - LightNVR @@ -21,7 +21,7 @@ -
+
diff --git a/web/index.html b/web/index.html index 4c51156e7..45b0dd7e1 100644 --- a/web/index.html +++ b/web/index.html @@ -2,7 +2,7 @@ - + Live View - LightNVR @@ -21,7 +21,7 @@ -
+