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.
366 lines
9.8 KiB
Markdown
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)
|