diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 01cfa360..64ea6cbe 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -11,6 +11,24 @@ permissions: contents: read jobs: + web-jsx-a11y: + name: Web JSX accessibility lint + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: web/package-lock.json + - name: Install web dependencies + run: npm ci --prefix web + - name: Report JSX accessibility findings + run: | + npm run lint --prefix web + echo 'The existing WCAG audit findings are warnings. Promote each rule to an error as its finding is fixed.' >> "$GITHUB_STEP_SUMMARY" + clang-tidy: name: clang-tidy runs-on: ubuntu-latest diff --git a/web/css/base.css b/web/css/base.css index 35ff89ab..967a1d7a 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 00000000..e9479db0 --- /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 1eba2afe..f4779089 100644 --- a/web/hls.html +++ b/web/hls.html @@ -2,7 +2,7 @@
- +