1- const DEFAULT_CLEAR_DELAY_MS = 300 ;
1+ const DEFAULT_MIN_ANNOUNCE_MS = 700 ;
2+ const DEFAULT_SPEECH_CHARS_PER_SEC = 12 ;
23const INPUT_CLEAR_DELAY_MS = 700 ;
34const ANSI_ESCAPE_PATTERN = / \x1B \[ [ 0 - 9 ; ? ] * [ -\/ ] * [ @ - ~ ] / g;
45const CONTROL_CHAR_PATTERN = / [ \x00 - \x08 \x0B - \x1F \x7F ] / g;
@@ -16,12 +17,16 @@ export class MudScreenReaderAnnouncer {
1617 private inputClearTimer : number | undefined ;
1718 private sessionStartedAt : number ;
1819 private lastAnnouncedBuffer = '' ;
20+ private announceQueue : string [ ] = [ ] ;
21+ private isAnnouncing = false ;
22+ private abortToken = 0 ;
1923
2024 constructor (
2125 private readonly liveRegion : HTMLElement ,
2226 private readonly historyRegion ?: HTMLElement ,
2327 private readonly inputRegion ?: HTMLElement ,
24- private readonly clearDelayMs : number = DEFAULT_CLEAR_DELAY_MS ,
28+ private readonly minAnnouncementMs : number = DEFAULT_MIN_ANNOUNCE_MS ,
29+ private readonly speechCharsPerSecond : number = DEFAULT_SPEECH_CHARS_PER_SEC ,
2530 ) {
2631 this . sessionStartedAt = Date . now ( ) ;
2732 }
@@ -31,7 +36,7 @@ export class MudScreenReaderAnnouncer {
3136 */
3237 public markSessionStart ( timestamp : number = Date . now ( ) ) : void {
3338 this . sessionStartedAt = timestamp ;
34- this . clear ( ) ;
39+ this . stopAnnouncements ( ) ;
3540 this . clearHistory ( ) ;
3641 this . lastAnnouncedBuffer = '' ;
3742 }
@@ -64,12 +69,7 @@ export class MudScreenReaderAnnouncer {
6469 return ;
6570 }
6671
67- this . liveRegion . textContent = normalized ;
68- console . debug (
69- '[ScreenReader] Live region updated:' ,
70- this . liveRegion . textContent ,
71- ) ;
72- this . scheduleClear ( ) ;
72+ this . enqueueAnnouncement ( normalized ) ;
7373 }
7474
7575 /**
@@ -80,11 +80,21 @@ export class MudScreenReaderAnnouncer {
8080 this . cancelClearTimer ( ) ;
8181 }
8282
83+ /**
84+ * Stops any in-flight announcements and drops the queued backlog.
85+ */
86+ public stopAnnouncements ( ) : void {
87+ this . abortToken += 1 ;
88+ this . announceQueue = [ ] ;
89+ this . isAnnouncing = false ;
90+ this . clear ( ) ;
91+ }
92+
8393 /**
8494 * Disposes internal timers.
8595 */
8696 public dispose ( ) : void {
87- this . clear ( ) ;
97+ this . stopAnnouncements ( ) ;
8898 this . cancelInputClearTimer ( ) ;
8999 }
90100
@@ -272,12 +282,44 @@ export class MudScreenReaderAnnouncer {
272282 this . lastAnnouncedBuffer = '' ;
273283 }
274284
275- private scheduleClear ( ) : void {
285+ private enqueueAnnouncement ( normalized : string ) : void {
286+ this . announceQueue . push ( normalized ) ;
287+ this . processQueue ( ) ;
288+ }
289+
290+ private processQueue ( ) : void {
291+ if ( this . isAnnouncing ) {
292+ return ;
293+ }
294+
295+ const next = this . announceQueue . shift ( ) ;
296+ if ( ! next ) {
297+ return ;
298+ }
299+
300+ this . isAnnouncing = true ;
301+ const token = this . abortToken ;
302+
303+ this . liveRegion . textContent = next ;
304+ console . debug ( '[ScreenReader] Live region updated:' , next ) ;
305+
306+ const duration = this . getAnnouncementDuration ( next ) ;
307+ this . scheduleClear ( duration , token ) ;
308+ }
309+
310+ private scheduleClear ( durationMs : number , token : number ) : void {
276311 this . cancelClearTimer ( ) ;
277312
278313 this . clearTimer = window . setTimeout ( ( ) => {
279- this . clear ( ) ;
280- } , this . clearDelayMs ) ;
314+ if ( token !== this . abortToken ) {
315+ return ;
316+ }
317+
318+ this . liveRegion . textContent = '' ;
319+ this . isAnnouncing = false ;
320+ this . clearTimer = undefined ;
321+ this . processQueue ( ) ;
322+ } , durationMs ) ;
281323 }
282324
283325 private cancelClearTimer ( ) : void {
@@ -287,6 +329,13 @@ export class MudScreenReaderAnnouncer {
287329 }
288330 }
289331
332+ private getAnnouncementDuration ( text : string ) : number {
333+ const chars = Math . max ( text . length , 1 ) ;
334+ const estimatedMs = Math . ceil ( ( chars / this . speechCharsPerSecond ) * 1000 ) ;
335+
336+ return Math . max ( estimatedMs , this . minAnnouncementMs ) ;
337+ }
338+
290339 // Input clear helpers are retained for potential future use (currently unused)
291340 private scheduleInputClear ( ) : void {
292341 this . cancelInputClearTimer ( ) ;
0 commit comments