Files
ui-essentials/projects/ui-accessibility/src/lib/services/live-announcer/live-announcer.service.ts
skyai_dev 5346d6d0c9 Add comprehensive library expansion with new components and demos
- 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>
2025-09-05 05:37:37 +10:00

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;
}
}