Files
git.stella-ops.org/docs/modules/ui/accessibility.md
master a4badc275e UI work to fill SBOM sourcing management gap. UI planning remaining functionality exposure. Work on CI/Tests stabilization
Introduces CGS determinism test runs to CI workflows for Windows, macOS, Linux, Alpine, and Debian, fulfilling CGS-008 cross-platform requirements. Updates local-ci scripts to support new smoke steps, test timeouts, progress intervals, and project slicing for improved test isolation and diagnostics.
2025-12-29 19:12:38 +02:00

366 lines
9.8 KiB
Markdown

# StellaOps UI Accessibility Guide
## Overview
StellaOps web interface aims for WCAG 2.1 AA compliance. This document outlines accessibility requirements, testing procedures, and implementation patterns.
## WCAG 2.1 AA Compliance Checklist
### Perceivable
| Criterion | Requirement | Implementation |
|-----------|-------------|----------------|
| **1.1.1 Non-text Content** | Alt text for images | All `<img>` elements have `alt` attribute |
| **1.3.1 Info and Relationships** | Semantic HTML | Use proper heading hierarchy, lists, tables |
| **1.3.2 Meaningful Sequence** | Logical reading order | DOM order matches visual order |
| **1.4.1 Use of Color** | Color not sole indicator | Use icons + color, patterns + color |
| **1.4.3 Contrast (Minimum)** | 4.5:1 for text | All text meets contrast ratio |
| **1.4.4 Resize Text** | 200% zoom works | No horizontal scroll at 200% zoom |
| **1.4.11 Non-text Contrast** | 3:1 for UI components | Buttons, inputs, focus indicators |
### Operable
| Criterion | Requirement | Implementation |
|-----------|-------------|----------------|
| **2.1.1 Keyboard** | All functionality keyboard accessible | Tab navigation, keyboard shortcuts |
| **2.1.2 No Keyboard Trap** | Users can navigate away | Focus trap only in modals |
| **2.4.1 Bypass Blocks** | Skip navigation link | Skip link at top of page |
| **2.4.3 Focus Order** | Logical focus order | Tab order matches visual layout |
| **2.4.6 Headings and Labels** | Descriptive headings | Clear, unique page titles |
| **2.4.7 Focus Visible** | Focus indicator visible | Custom focus styles |
### Understandable
| Criterion | Requirement | Implementation |
|-----------|-------------|----------------|
| **3.1.1 Language of Page** | `lang` attribute set | `<html lang="en">` |
| **3.2.1 On Focus** | No unexpected changes | No auto-submit on focus |
| **3.3.1 Error Identification** | Errors identified | Error messages with field reference |
| **3.3.2 Labels or Instructions** | Labels provided | All inputs have labels |
### Robust
| Criterion | Requirement | Implementation |
|-----------|-------------|----------------|
| **4.1.1 Parsing** | Valid HTML | No duplicate IDs, proper nesting |
| **4.1.2 Name, Role, Value** | ARIA attributes | Proper ARIA for custom widgets |
## Color Contrast Requirements
### Severity Colors (with sufficient contrast)
```css
/* All severity colors meet 4.5:1 contrast on white background */
:root {
--color-critical: #991b1b; /* 7.8:1 on white */
--color-critical-bg: #fef2f2;
--color-high: #9a3412; /* 7.5:1 on white */
--color-high-bg: #fff7ed;
--color-medium: #854d0e; /* 7.2:1 on white */
--color-medium-bg: #fefce8;
--color-low: #166534; /* 6.8:1 on white */
--color-low-bg: #f0fdf4;
--color-info: #1e40af; /* 8.2:1 on white */
--color-info-bg: #eff6ff;
}
```
### Dark Mode Contrast
```css
/* Dark mode maintains contrast ratios */
[data-theme="dark"] {
--color-critical: #fca5a5; /* 4.5:1 on dark background */
--color-high: #fdba74;
--color-medium: #fde047;
--color-low: #86efac;
--color-info: #93c5fd;
}
```
## Keyboard Navigation
### Global Shortcuts
| Shortcut | Action | Scope |
|----------|--------|-------|
| `Cmd/Ctrl+K` | Open command palette | Global |
| `?` | Show keyboard help | Global |
| `Esc` | Close modal/Clear selection | Global |
| `g h` | Go to Home | Navigation |
| `g f` | Go to Findings | Navigation |
| `g p` | Go to Policy | Navigation |
| `g o` | Go to Ops | Navigation |
### List Navigation
| Shortcut | Action |
|----------|--------|
| `j` / `↓` | Move down |
| `k` / `↑` | Move up |
| `Enter` | Select/Open |
| `Space` | Toggle checkbox |
| `x` | Select current |
| `/` | Focus search |
### Implementation Example
```typescript
@Injectable({ providedIn: 'root' })
export class KeyboardShortcutService {
private shortcuts = new Map<string, ShortcutHandler>();
register(key: string, handler: ShortcutHandler): void {
this.shortcuts.set(key.toLowerCase(), handler);
}
@HostListener('window:keydown', ['$event'])
handleKeydown(event: KeyboardEvent): void {
// Ignore when typing in inputs
if (this.isTyping(event)) return;
const key = this.normalizeKey(event);
const handler = this.shortcuts.get(key);
if (handler) {
event.preventDefault();
handler.execute();
}
}
}
```
## Focus Management
### Focus Trap for Modals
```typescript
@Directive({ selector: '[appFocusTrap]' })
export class FocusTrapDirective implements AfterViewInit, OnDestroy {
private focusableElements: HTMLElement[] = [];
private previousActiveElement: HTMLElement | null = null;
ngAfterViewInit(): void {
this.previousActiveElement = document.activeElement as HTMLElement;
this.focusableElements = this.getFocusableElements();
this.focusFirst();
}
ngOnDestroy(): void {
this.previousActiveElement?.focus();
}
@HostListener('keydown.tab', ['$event'])
handleTab(event: KeyboardEvent): void {
const first = this.focusableElements[0];
const last = this.focusableElements[this.focusableElements.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
}
```
### Focus Visible Styles
```css
/* Custom focus indicator */
*:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* Remove default outline for mouse users */
*:focus:not(:focus-visible) {
outline: none;
}
/* High contrast mode focus */
@media (forced-colors: active) {
*:focus-visible {
outline: 3px solid CanvasText;
}
}
```
## Screen Reader Support
### ARIA Attributes
```html
<!-- Live regions for dynamic updates -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
{{ statusMessage }}
</div>
<!-- Loading states -->
<div aria-busy="true" aria-describedby="loading-message">
<span id="loading-message" class="sr-only">Loading findings...</span>
</div>
<!-- Error announcements -->
<div role="alert" aria-live="assertive">
{{ errorMessage }}
</div>
<!-- Navigation landmarks -->
<nav aria-label="Main navigation">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<main aria-labelledby="page-title">...</main>
```
### Screen Reader Announcements
```typescript
@Injectable({ providedIn: 'root' })
export class AnnouncerService {
private liveRegion: HTMLElement;
constructor() {
this.createLiveRegion();
}
announce(message: string, politeness: 'polite' | 'assertive' = 'polite'): void {
this.liveRegion.setAttribute('aria-live', politeness);
this.liveRegion.textContent = '';
// Force reflow
void this.liveRegion.offsetWidth;
this.liveRegion.textContent = message;
}
private createLiveRegion(): void {
this.liveRegion = document.createElement('div');
this.liveRegion.setAttribute('aria-live', 'polite');
this.liveRegion.setAttribute('aria-atomic', 'true');
this.liveRegion.className = 'sr-only';
document.body.appendChild(this.liveRegion);
}
}
```
## Form Accessibility
### Input Labels
```html
<!-- Visible label -->
<label for="cve-search">Search CVEs</label>
<input id="cve-search" type="text" aria-describedby="cve-search-hint">
<span id="cve-search-hint" class="hint">Enter CVE ID like CVE-2024-1234</span>
<!-- Hidden label for icon buttons -->
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<!-- Group labels -->
<fieldset>
<legend>Severity Filter</legend>
<label><input type="checkbox" name="severity" value="critical"> Critical</label>
<label><input type="checkbox" name="severity" value="high"> High</label>
</fieldset>
```
### Error Messages
```html
<div class="form-group" [class.has-error]="control.invalid && control.touched">
<label for="policy-name">Policy Name</label>
<input
id="policy-name"
[attr.aria-invalid]="control.invalid && control.touched"
[attr.aria-describedby]="control.invalid ? 'policy-name-error' : null">
<span id="policy-name-error" role="alert" *ngIf="control.invalid && control.touched">
{{ getErrorMessage(control) }}
</span>
</div>
```
## Motion and Animation
### Reduced Motion Support
```css
/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Safe animations (opacity, transform only) */
.fade-in {
animation: fadeIn 200ms ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Alternative for reduced motion */
@media (prefers-reduced-motion: reduce) {
.fade-in {
animation: none;
opacity: 1;
}
}
```
## Testing Procedures
### Automated Testing
```bash
# axe-core CLI
npm run test:a11y
# Lighthouse accessibility audit
lighthouse --only-categories=accessibility https://localhost:4200
# Pa11y for CI
pa11y https://localhost:4200 --standard WCAG2AA
```
### Manual Testing Checklist
- [ ] Tab through entire page - all interactive elements focusable
- [ ] Keyboard shortcuts work without mouse
- [ ] Focus indicator visible on all focusable elements
- [ ] Screen reader announces page changes
- [ ] Zoom to 200% - no horizontal scroll
- [ ] High contrast mode renders correctly
- [ ] Reduced motion preference respected
### Screen Reader Testing
| Platform | Screen Reader | Browser |
|----------|---------------|---------|
| Windows | NVDA | Firefox |
| Windows | JAWS | Chrome |
| macOS | VoiceOver | Safari |
| iOS | VoiceOver | Safari |
| Android | TalkBack | Chrome |
## Related Documentation
- [Information Architecture](./information-architecture.md)
- [Component Library](./components.md)
- [SPRINT_040 - Keyboard Accessibility](../../implplan/SPRINT_20251229_040_FE_keyboard_accessibility.md)