ci(a11y): add djlint HTML accessibility lint to CI pipeline
- Add html-lint job to CI that runs djlint on all PRs - Configure djlint in pyproject.toml with Jinja2 profile and accessibility rules - Add djlint to requirements-dev.txt - Expand accessibility section in frontend Copilot instructions (WCAG 2.1 AA) - Wire html-lint into CI dependency chain (test/mypy/build depend on it) Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -86,12 +86,57 @@ These instructions apply to all files in the `frontend/` directory (templates, C
|
||||
</form>
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
- Use semantic HTML elements (`nav`, `main`, `article`, `section`)
|
||||
- Include `alt` text for images
|
||||
- Use proper heading hierarchy (h1 → h2 → h3)
|
||||
- Add ARIA labels when needed
|
||||
- Ensure keyboard navigation works
|
||||
### Accessibility (WCAG 2.1 Level AA Required)
|
||||
|
||||
DocuElevate targets **WCAG 2.1 Level AA** compliance. Every template change **must** follow these rules.
|
||||
For the full guide with examples, see `docs/AccessibilityGuide.md`.
|
||||
|
||||
#### Semantic HTML (WCAG 1.3.1)
|
||||
- Use semantic elements: `<nav>`, `<main>`, `<article>`, `<section>`, `<header>`, `<footer>`
|
||||
- Use proper heading hierarchy: one `<h1>` per page, then `<h2>` → `<h3>` (never skip levels)
|
||||
- Use `<button>` for actions (not `<a>` or `<div>`) and `<a>` for navigation
|
||||
- Use `<table>` with `<caption>` or `aria-label`, `<thead>`/`<tbody>`, and `scope="col"`/`scope="row"` on headers
|
||||
|
||||
#### Images & Icons (WCAG 1.1.1)
|
||||
- All `<img>` elements **must** have an `alt` attribute — descriptive for content images, `alt=""` for purely decorative ones
|
||||
- Decorative Font Awesome `<i>` icons **must** have `aria-hidden="true"` when adjacent text already conveys meaning
|
||||
- Icon-only buttons **must** have `aria-label` describing the action (e.g., `aria-label="Delete file"`)
|
||||
|
||||
#### Keyboard Navigation (WCAG 2.1.1, 2.4.1, 2.4.7)
|
||||
- All interactive elements must be keyboard-reachable (native `<a>`, `<button>`, `<input>`, or add `tabindex="0"` + key handlers)
|
||||
- `base.html` provides a **skip-to-content** link (`<a href="#main-content" class="skip-link">`) — do not remove it
|
||||
- Never suppress focus indicators — the global `focus-visible` outline in `styles.css` is required
|
||||
- Custom interactive widgets (dropdowns, modals) must trap focus correctly
|
||||
|
||||
#### ARIA Attributes
|
||||
- `aria-label` — use on elements whose purpose isn't clear from visible text (icon-only buttons, unlabelled inputs)
|
||||
- `aria-hidden="true"` — use on purely decorative icons and elements that duplicate adjacent text
|
||||
- `aria-live="polite"` — add to any container whose content updates dynamically (status messages, search results, upload progress)
|
||||
- `aria-expanded` — add to buttons that toggle visibility of content (menus, accordions)
|
||||
- `aria-current="page"` — mark the current page's navigation link
|
||||
- `aria-sort` — use on sortable table column headers
|
||||
|
||||
#### Forms (WCAG 1.3.1, 3.3.2)
|
||||
- Every `<input>`, `<select>`, and `<textarea>` **must** have an associated `<label>` (via `for`/`id`) or `aria-label`
|
||||
- Error messages must be linked via `aria-describedby` or announced with `role="alert"`
|
||||
- Use `role="search"` on search form containers
|
||||
|
||||
#### Modals / Dialogs (WCAG 4.1.2)
|
||||
- Add `role="dialog"`, `aria-modal="true"`, and `aria-labelledby` pointing to the dialog title
|
||||
- Focus must move into the dialog when opened and return to the trigger when closed
|
||||
|
||||
#### Color & Contrast (WCAG 1.4.3, 1.4.1)
|
||||
- Text must meet 4.5:1 contrast ratio against its background (3:1 for large text)
|
||||
- Never rely on color alone to convey information — pair color with icons, text labels, or patterns
|
||||
- Dark-mode overrides in `styles.css` are WCAG AA-verified; maintain this when adding new colors
|
||||
|
||||
#### Touch Targets (WCAG 2.5.8)
|
||||
- All clickable/tappable elements must be at least 44×44 CSS pixels (`min-height:44px; min-width:44px`)
|
||||
|
||||
#### Automated Checks
|
||||
- The CI pipeline runs `djlint` on every PR to catch common accessibility regressions
|
||||
- Run locally: `djlint frontend/templates/ --lint`
|
||||
- Configuration is in `pyproject.toml` under `[tool.djlint]`
|
||||
|
||||
### Error Handling
|
||||
- Display user-friendly error messages
|
||||
|
||||
@@ -61,6 +61,28 @@ jobs:
|
||||
# ruff format only supports --check; do not pass --fix here
|
||||
run: ruff format --check app/ tests/
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# Stage 1b: HTML Accessibility Lint (catches a11y regressions early)
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
html-lint:
|
||||
name: HTML Accessibility Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install djLint
|
||||
run: pip install djlint>=1.36.0
|
||||
|
||||
- name: Lint HTML templates for accessibility
|
||||
run: djlint frontend/templates/ --lint
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# Stage 2a: Dependency Vulnerability Scan (runs in parallel with lint)
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
@@ -93,7 +115,7 @@ jobs:
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, dependency-scan] # Wait for lint and dependency scan before running tests
|
||||
needs: [lint, html-lint, dependency-scan] # Wait for lint, HTML a11y lint, and dependency scan before running tests
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
@@ -160,7 +182,7 @@ jobs:
|
||||
mypy:
|
||||
name: Mypy
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, dependency-scan] # Wait for lint and dependency scan before running type checks
|
||||
needs: [lint, html-lint, dependency-scan] # Wait for lint, HTML a11y lint, and dependency scan before running type checks
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
@@ -185,7 +207,7 @@ jobs:
|
||||
build:
|
||||
name: Build & Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, lint, mypy, dependency-scan]
|
||||
needs: [test, lint, html-lint, mypy, dependency-scan]
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
|
||||
@@ -225,3 +225,14 @@ exclude_lines = [
|
||||
"if TYPE_CHECKING:",
|
||||
"@abstractmethod",
|
||||
]
|
||||
|
||||
# djLint configuration — HTML template linter for Jinja2 templates
|
||||
# Enforces accessibility (WCAG 2.1 AA) and HTML best practices in CI.
|
||||
# Run: djlint frontend/templates/ --profile jinja --lint
|
||||
[tool.djlint]
|
||||
profile = "jinja"
|
||||
# Include only accessibility-critical and correctness rules for CI enforcement
|
||||
include = "H005,H013,H016,H025,H026"
|
||||
# Ignore rules that conflict with project patterns or produce excessive noise
|
||||
ignore = "H006,H021,H023,H030,H031,H029,H036,T003,J018,J004,T028,H014,T001"
|
||||
max_line_length = 120
|
||||
|
||||
@@ -16,6 +16,7 @@ boto3>=1.26.0 # For S3 integration tests
|
||||
# Code quality
|
||||
ruff>=0.3.0
|
||||
mypy>=1.8.0
|
||||
djlint>=1.36.0 # HTML template linter for accessibility and best practices
|
||||
|
||||
# Type stubs for mypy
|
||||
types-requests>=2.31.0
|
||||
|
||||
Reference in New Issue
Block a user