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>
|
</form>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Accessibility
|
### Accessibility (WCAG 2.1 Level AA Required)
|
||||||
- Use semantic HTML elements (`nav`, `main`, `article`, `section`)
|
|
||||||
- Include `alt` text for images
|
DocuElevate targets **WCAG 2.1 Level AA** compliance. Every template change **must** follow these rules.
|
||||||
- Use proper heading hierarchy (h1 → h2 → h3)
|
For the full guide with examples, see `docs/AccessibilityGuide.md`.
|
||||||
- Add ARIA labels when needed
|
|
||||||
- Ensure keyboard navigation works
|
#### 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
|
### Error Handling
|
||||||
- Display user-friendly error messages
|
- Display user-friendly error messages
|
||||||
|
|||||||
@@ -61,6 +61,28 @@ jobs:
|
|||||||
# ruff format only supports --check; do not pass --fix here
|
# ruff format only supports --check; do not pass --fix here
|
||||||
run: ruff format --check app/ tests/
|
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)
|
# Stage 2a: Dependency Vulnerability Scan (runs in parallel with lint)
|
||||||
# ══════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -93,7 +115,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: ubuntu-latest
|
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:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:7
|
image: redis:7
|
||||||
@@ -160,7 +182,7 @@ jobs:
|
|||||||
mypy:
|
mypy:
|
||||||
name: Mypy
|
name: Mypy
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -185,7 +207,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build & Push Docker Image
|
name: Build & Push Docker Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [test, lint, mypy, dependency-scan]
|
needs: [test, lint, html-lint, mypy, dependency-scan]
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -225,3 +225,14 @@ exclude_lines = [
|
|||||||
"if TYPE_CHECKING:",
|
"if TYPE_CHECKING:",
|
||||||
"@abstractmethod",
|
"@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
|
# Code quality
|
||||||
ruff>=0.3.0
|
ruff>=0.3.0
|
||||||
mypy>=1.8.0
|
mypy>=1.8.0
|
||||||
|
djlint>=1.36.0 # HTML template linter for accessibility and best practices
|
||||||
|
|
||||||
# Type stubs for mypy
|
# Type stubs for mypy
|
||||||
types-requests>=2.31.0
|
types-requests>=2.31.0
|
||||||
|
|||||||
Reference in New Issue
Block a user