();
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
{{ statusMessage }}
Loading findings...
{{ errorMessage }}
...
```
### 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
Enter CVE ID like CVE-2024-1234
```
### Error Messages
```html
{{ getErrorMessage(control) }}
```
## 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)