// Screen Reader Announcements Module // Handles ARIA live region announcements for accessibility (function() { 'use strict'; AppModules.require('ui-controls'); AppModules.register('screen-reader'); // Announcement regions let regions = {}; // State tracking to avoid announcement spam let lastViewerCount = 0; let lastStreamStatus = 'unknown'; let lastConnectionStatus = 'unknown'; // Announcement queue to prevent rapid-fire announcements let announcementQueue = []; let isProcessingQueue = false; // Configuration const ANNOUNCEMENT_THRESHOLDS = { viewerCount: 5, // Only announce when count changes by 5+ users messageGroup: 3, // Minimum messages for group announcements activityDelay: 500 // Delay for activity announcements to batch them }; // Initialize announcement regions function initializeAnnouncements() { // Get all announcement region elements regions = { stream: document.getElementById('sr-stream-announcer'), connection: document.getElementById('sr-connection-announcer'), system: document.getElementById('sr-system-announcer'), messageGroup: document.getElementById('sr-message-group-announcer'), viewer: document.getElementById('sr-viewer-announcer'), activity: document.getElementById('sr-activity-announcer'), form: document.getElementById('sr-form-announcer') }; // Verify all regions exist const missingRegions = Object.entries(regions).filter(([key, element]) => !element); if (missingRegions.length > 0) { AppLogger.error('Missing screen reader announcement regions:', missingRegions.map(([key]) => key)); } else { AppLogger.log('Screen reader announcement regions initialized'); } } // Queue announcement to prevent spam function queueAnnouncement(region, text, options = {}) { if (!text || !text.trim()) return; announcementQueue.push({ region, text: text.trim(), options: { priority: options.priority || 'normal', // 'low', 'normal', 'high' delay: options.delay || 0, ...options } }); // Start processing queue if not already running if (!isProcessingQueue) { processAnnouncementQueue(); } } // Process the announcement queue async function processAnnouncementQueue() { if (isProcessingQueue || announcementQueue.length === 0) return; isProcessingQueue = true; while (announcementQueue.length > 0) { const announcement = announcementQueue.shift(); try { await announce(announcement.region, announcement.text, announcement.options); } catch (error) { AppLogger.error('Error processing announcement:', error); } } isProcessingQueue = false; } // Actually announce to a region async function announce(regionName, text, options = {}) { const regionElement = regions[regionName]; if (!regionElement) { AppLogger.error(`Unknown announcement region: ${regionName}`); return; } // Apply delay if specified if (options.delay > 0) { await new Promise(resolve => setTimeout(resolve, options.delay)); } // Clear region first for assertive updates regionElement.textContent = ''; // Small delay to ensure clear is processed await new Promise(resolve => setTimeout(resolve, 10)); // Set the announcement text regionElement.textContent = text; // Log for debugging AppLogger.log(`Screen reader announcement (${regionName}): ${text}`); } // Public announcement functions const announcements = { // Stream status changes streamStatus(status, details = '') { if (status === lastStreamStatus) return; // Avoid duplicate announcements lastStreamStatus = status; let message = ''; switch (status) { case 'online': message = 'Stream is now online and available'; break; case 'offline': message = 'Stream is currently offline'; break; case 'reconnecting': message = 'Attempting to reconnect to stream'; break; case 'quality-changed': message = `Stream quality changed to ${details}`; break; case 'buffering': message = 'Stream is buffering'; break; } if (message) { queueAnnouncement('stream', message, { priority: 'high' }); } }, // Connection status changes connectionStatus(status, details = '') { if (status === lastConnectionStatus) return; lastConnectionStatus = status; let message = ''; switch (status) { case 'connected': message = 'Chat connection established'; break; case 'disconnected': message = 'Chat connection lost - attempting to reconnect'; break; case 'reconnecting': message = 'Reconnecting to chat'; break; case 'error': message = `Connection error: ${details}`; break; } if (message) { queueAnnouncement('connection', message, { priority: 'high' }); } }, // Viewer count changes (with threshold) viewerCount(count) { const oldCount = lastViewerCount; const diff = Math.abs(count - oldCount); // Only announce if significant change (threshold) or crossing boundaries if (diff >= ANNOUNCEMENT_THRESHOLDS.viewerCount || (oldCount === 0 && count > 0) || (oldCount > 0 && count === 0)) { lastViewerCount = count; let message; if (count === 0) { message = 'No viewers currently online'; } else if (count === 1) { message = '1 viewer online'; } else { message = `${count} viewers online`; } queueAnnouncement('viewer', message, { priority: 'low', delay: 500 }); } }, // System announcements (admin actions, etc.) systemMessage(message, priority = 'normal') { queueAnnouncement('system', message, { priority: priority, delay: priority === 'high' ? 0 : 200 }); }, // Message group announcements messageGroup(messages, action = 'added') { if (!Array.isArray(messages) || messages.length === 0) return; const count = messages.length; let message = ''; if (count >= ANNOUNCEMENT_THRESHOLDS.messageGroup) { if (action === 'added') { message = `${count} new messages loaded`; } else if (action === 'filtered') { message = `Showing ${count} messages matching filter`; } } if (message) { queueAnnouncement('messageGroup', message, { priority: 'low', delay: 300 }); } }, // Individual message arrival (for existing messages) messageReceived(message, isUserMessage = false) { // Don't announce user's own messages to avoid echo feedback if (isUserMessage) return; // For individual messages, let the aria-live="polite" on chat handle it // Only use this for special announcements }, // Activity announcements activity(userAction, details = '') { let message = ''; switch (userAction) { case 'joined': message = `User ${details} joined the chat`; break; case 'left': message = `User ${details} left the chat`; break; case 'admin-action': message = details; // Admin actions passed through break; default: if (details) { message = details; } } if (message) { queueAnnouncement('activity', message, { priority: 'low', delay: ANNOUNCEMENT_THRESHOLDS.activityDelay }); } }, // Form validation messages formValidation(field, isValid, message) { if (!message) return; queueAnnouncement('form', message, { priority: isValid ? 'low' : 'high', delay: 100 }); }, // Form success messages formSuccess(message) { queueAnnouncement('form', message, { priority: 'normal', delay: 100 }); }, // Generic high-priority announcements (errors, critical state changes) critical(message) { queueAnnouncement('system', message, { priority: 'high' }); }, // Status updates (dashboard stats changes) statusUpdate(type, value, unit = '') { let message = ''; switch (type) { case 'uptime': message = `Stream uptime: ${value}`; break; case 'quality': message = `Stream quality: ${value}`; break; case 'buffer': const bufferPercent = Math.round(value * 100); message = `Buffer ${bufferPercent}%`; break; default: if (unit) { message = `${type}: ${value} ${unit}`; } else { message = `${type}: ${value}`; } } if (message) { // Use activity region for status updates queueAnnouncement('activity', message, { priority: 'low', delay: 500 }); } } }; // Public API window.ScreenReader = { ...announcements, initialize: initializeAnnouncements, queueAnnouncement: queueAnnouncement, announce: announce // Direct announce function for special cases }; // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', function() { // Small delay to ensure DOM is fully ready setTimeout(initializeAnnouncements, 100); }); AppLogger.log('Screen reader announcements module loaded'); })();