- Add new libraries: ui-accessibility, ui-animations, ui-backgrounds, ui-code-display, ui-data-utils, ui-font-manager, hcl-studio - Add extensive layout components: gallery-grid, infinite-scroll-container, kanban-board, masonry, split-view, sticky-layout - Add comprehensive demo components for all new features - Update project configuration and dependencies - Expand component exports and routing structure - Add UI landing pages planning document 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
152 lines
3.7 KiB
TypeScript
152 lines
3.7 KiB
TypeScript
import { Injectable, OnDestroy } from '@angular/core';
|
|
|
|
export type AriaLivePoliteness = 'off' | 'polite' | 'assertive';
|
|
|
|
export interface LiveAnnouncementConfig {
|
|
politeness?: AriaLivePoliteness;
|
|
duration?: number;
|
|
clearPrevious?: boolean;
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class LiveAnnouncerService implements OnDestroy {
|
|
private liveElement?: HTMLElement;
|
|
private timeouts: Map<AriaLivePoliteness, any> = new Map();
|
|
|
|
constructor() {
|
|
this.createLiveElement();
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.timeouts.forEach(timeout => clearTimeout(timeout));
|
|
this.removeLiveElement();
|
|
}
|
|
|
|
/**
|
|
* Announces a message to screen readers
|
|
*/
|
|
announce(
|
|
message: string,
|
|
politeness: AriaLivePoliteness = 'polite',
|
|
duration?: number
|
|
): void {
|
|
this.announceWithConfig(message, {
|
|
politeness,
|
|
duration,
|
|
clearPrevious: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Announces a message with full configuration options
|
|
*/
|
|
announceWithConfig(message: string, config: LiveAnnouncementConfig = {}): void {
|
|
const {
|
|
politeness = 'polite',
|
|
duration = 4000,
|
|
clearPrevious = true
|
|
} = config;
|
|
|
|
if (!this.liveElement) {
|
|
this.createLiveElement();
|
|
}
|
|
|
|
if (!this.liveElement) return;
|
|
|
|
// Clear any existing timeout for this politeness level
|
|
if (this.timeouts.has(politeness)) {
|
|
clearTimeout(this.timeouts.get(politeness));
|
|
}
|
|
|
|
// Clear previous message if requested
|
|
if (clearPrevious) {
|
|
this.liveElement.textContent = '';
|
|
}
|
|
|
|
// Set the politeness level
|
|
this.liveElement.setAttribute('aria-live', politeness);
|
|
|
|
// Announce the message
|
|
setTimeout(() => {
|
|
if (this.liveElement) {
|
|
this.liveElement.textContent = message;
|
|
}
|
|
}, 100);
|
|
|
|
// Clear the message after the specified duration
|
|
if (duration > 0) {
|
|
const timeoutId = setTimeout(() => {
|
|
if (this.liveElement) {
|
|
this.liveElement.textContent = '';
|
|
}
|
|
this.timeouts.delete(politeness);
|
|
}, duration);
|
|
|
|
this.timeouts.set(politeness, timeoutId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears all current announcements
|
|
*/
|
|
clear(): void {
|
|
if (this.liveElement) {
|
|
this.liveElement.textContent = '';
|
|
}
|
|
this.timeouts.forEach(timeout => clearTimeout(timeout));
|
|
this.timeouts.clear();
|
|
}
|
|
|
|
/**
|
|
* Announces multiple messages with delays between them
|
|
*/
|
|
announceSequence(
|
|
messages: string[],
|
|
politeness: AriaLivePoliteness = 'polite',
|
|
delayBetween = 1000
|
|
): void {
|
|
messages.forEach((message, index) => {
|
|
setTimeout(() => {
|
|
this.announce(message, politeness);
|
|
}, index * delayBetween);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates the live region element
|
|
*/
|
|
private createLiveElement(): void {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
// Remove existing element if it exists
|
|
this.removeLiveElement();
|
|
|
|
// Create new live region
|
|
this.liveElement = document.createElement('div');
|
|
this.liveElement.setAttribute('aria-live', 'polite');
|
|
this.liveElement.setAttribute('aria-atomic', 'true');
|
|
this.liveElement.setAttribute('id', 'a11y-live-announcer');
|
|
|
|
// Make it invisible but still accessible to screen readers
|
|
this.liveElement.style.position = 'absolute';
|
|
this.liveElement.style.left = '-10000px';
|
|
this.liveElement.style.width = '1px';
|
|
this.liveElement.style.height = '1px';
|
|
this.liveElement.style.overflow = 'hidden';
|
|
|
|
document.body.appendChild(this.liveElement);
|
|
}
|
|
|
|
/**
|
|
* Removes the live region element
|
|
*/
|
|
private removeLiveElement(): void {
|
|
const existing = document.getElementById('a11y-live-announcer');
|
|
if (existing) {
|
|
existing.remove();
|
|
}
|
|
this.liveElement = undefined;
|
|
}
|
|
} |