1
1
import { MinHeap } from './MinHeap'
2
2
import { Ordering , type Order } from './order'
3
- import { throttle , type Throttle } from './throttle'
4
3
5
4
type Politeness = 'polite' | 'assertive'
6
5
@@ -26,7 +25,7 @@ type AnnounceOptions = {
26
25
type Message = {
27
26
contents : string
28
27
politeness : Politeness
29
- scheduled : number | 'immediate'
28
+ scheduled : number
30
29
}
31
30
32
31
/**
@@ -40,11 +39,25 @@ type Cancel = () => void
40
39
const DEFAULT_THROTTLE_DELAY_MS = 500
41
40
42
41
class LiveRegionElement extends HTMLElement {
42
+ /**
43
+ * A flag to indicate if a message has been announced and we are currently
44
+ * waiting for the delay to pass before announcing the next message.
45
+ */
46
+ #pending: boolean
47
+
48
+ /**
49
+ * A priority queue to store messages to be announced by the live region.
50
+ */
43
51
#queue: MinHeap < Message >
52
+
53
+ /**
54
+ * The id for a timeout being used by the live region to either wait until the
55
+ * next message or wait until the delay has passed before announcing the next
56
+ * message
57
+ */
44
58
#timeoutId: number | null
45
- updateContainerWithMessage : Throttle < ( message : Message ) => void >
46
59
47
- constructor ( { waitMs } : { waitMs ?: number } = { } ) {
60
+ constructor ( ) {
48
61
super ( )
49
62
50
63
if ( ! this . shadowRoot ) {
@@ -53,24 +66,27 @@ class LiveRegionElement extends HTMLElement {
53
66
shadowRoot . appendChild ( template . content . cloneNode ( true ) )
54
67
}
55
68
69
+ this . #pending = false
56
70
this . #timeoutId = null
57
71
this . #queue = new MinHeap ( {
58
72
compareFn : compareMessages ,
59
73
} )
74
+ }
60
75
61
- this . updateContainerWithMessage = throttle ( ( message : Message ) => {
62
- const { contents, politeness} = message
63
- const container = this . shadowRoot ?. getElementById ( politeness )
64
- if ( ! container ) {
65
- throw new Error ( `Unable to find container for message. Expected a container with id="${ politeness } "` )
66
- }
76
+ /**
77
+ * The delay in milliseconds to wait between announcements. This helps to
78
+ * prevent announcements getting dropped if multiple are made at the same time.
79
+ */
80
+ get delay ( ) : number {
81
+ const value = this . getAttribute ( 'delay' )
82
+ if ( value ) {
83
+ return parseInt ( value , 10 )
84
+ }
85
+ return DEFAULT_THROTTLE_DELAY_MS
86
+ }
67
87
68
- if ( container . textContent === contents ) {
69
- container . textContent = `${ contents } \u00A0`
70
- } else {
71
- container . textContent = contents
72
- }
73
- } , waitMs ?? DEFAULT_THROTTLE_DELAY_MS )
88
+ set delay ( value : number ) {
89
+ this . setAttribute ( 'delay' , `${ value } ` )
74
90
}
75
91
76
92
/**
@@ -79,13 +95,14 @@ class LiveRegionElement extends HTMLElement {
79
95
*/
80
96
public announce ( message : string , options : AnnounceOptions = { } ) : Cancel {
81
97
const { delayMs, politeness = 'polite' } = options
98
+ const now = Date . now ( )
82
99
const item : Message = {
83
100
politeness,
84
101
contents : message ,
85
- scheduled : delayMs !== undefined ? Date . now ( ) + delayMs : 'immediate' ,
102
+ scheduled : delayMs !== undefined ? now + delayMs : now ,
86
103
}
87
104
this . #queue. insert ( item )
88
- this . performWork ( )
105
+ this . # performWork( )
89
106
return ( ) => {
90
107
this . #queue. delete ( item )
91
108
}
@@ -103,7 +120,11 @@ class LiveRegionElement extends HTMLElement {
103
120
return noop
104
121
}
105
122
106
- performWork ( ) {
123
+ #performWork( ) {
124
+ if ( this . #pending) {
125
+ return
126
+ }
127
+
107
128
let message = this . #queue. peek ( )
108
129
if ( ! message ) {
109
130
return
@@ -114,29 +135,20 @@ class LiveRegionElement extends HTMLElement {
114
135
this . #timeoutId = null
115
136
}
116
137
117
- if ( message . scheduled === 'immediate' ) {
118
- message = this . #queue. pop ( )
119
- if ( message ) {
120
- this . updateContainerWithMessage ( message )
121
- }
122
- this . performWork ( )
123
- return
124
- }
125
-
126
138
const now = Date . now ( )
127
139
if ( message . scheduled <= now ) {
128
140
message = this . #queue. pop ( )
129
141
if ( message ) {
130
- this . updateContainerWithMessage ( message )
142
+ this . # updateContainerWithMessage( message )
131
143
}
132
- this . performWork ( )
144
+ this . # performWork( )
133
145
return
134
146
}
135
147
136
148
const timeout = message . scheduled > now ? message . scheduled - now : 0
137
149
this . #timeoutId = window . setTimeout ( ( ) => {
138
150
this . #timeoutId = null
139
- this . performWork ( )
151
+ this . # performWork( )
140
152
} , timeout )
141
153
}
142
154
@@ -148,16 +160,46 @@ class LiveRegionElement extends HTMLElement {
148
160
return container . textContent
149
161
}
150
162
163
+ #updateContainerWithMessage( message : Message ) {
164
+ // Prevent any other announcements from being made while we are updating the
165
+ // contents to trigger an announcement
166
+ this . #pending = true
167
+
168
+ const { contents, politeness} = message
169
+ const container = this . shadowRoot ?. getElementById ( politeness )
170
+ if ( ! container ) {
171
+ this . #pending = false
172
+ throw new Error ( `Unable to find container for message. Expected a container with id="${ politeness } "` )
173
+ }
174
+
175
+ if ( container . textContent === contents ) {
176
+ container . textContent = `${ contents } \u00A0`
177
+ } else {
178
+ container . textContent = contents
179
+ }
180
+
181
+ if ( this . #timeoutId !== null ) {
182
+ clearTimeout ( this . #timeoutId)
183
+ }
184
+
185
+ // Wait the set delay amount before announcing the next message. This should
186
+ // help to make sure that announcements are only made once every delay
187
+ // amount.
188
+ this . #timeoutId = window . setTimeout ( ( ) => {
189
+ this . #timeoutId = null
190
+ this . #pending = false
191
+ this . #performWork( )
192
+ } , this . delay )
193
+ }
194
+
151
195
/**
152
- * Stop any pending messages from being announced by the live region
196
+ * Prevent pending messages from being announced by the live region.
153
197
*/
154
198
public clear ( ) : void {
155
199
if ( this . #timeoutId !== null ) {
156
200
clearTimeout ( this . #timeoutId)
157
201
this . #timeoutId = null
158
202
}
159
-
160
- this . updateContainerWithMessage . cancel ( )
161
203
this . #queue. clear ( )
162
204
}
163
205
}
@@ -210,16 +252,6 @@ function compareMessages(a: Message, b: Message): Order {
210
252
return Ordering . Equal
211
253
}
212
254
213
- // Schedule a before b
214
- if ( a . scheduled === 'immediate' && b . scheduled !== 'immediate' ) {
215
- return Ordering . Less
216
- }
217
-
218
- // Schedule a after b
219
- if ( a . scheduled !== 'immediate' && b . scheduled === 'immediate' ) {
220
- return Ordering . Greater
221
- }
222
-
223
255
// Schedule a before b
224
256
if ( a . scheduled < b . scheduled ) {
225
257
return Ordering . Less
0 commit comments