66
77// Former goog.module ID: Blockly.utils.aria
88
9+ import * as dom from './dom.js' ;
10+
911/** ARIA states/properties prefix. */
1012const ARIA_PREFIX = 'aria-' ;
1113
1214/** ARIA role attribute. */
1315const ROLE_ATTRIBUTE = 'role' ;
1416
17+ /**
18+ * ARIA state values for LivePriority.
19+ * Copied from Closure's goog.a11y.aria.LivePriority
20+ */
21+ export enum LivePriority {
22+ // This information has the highest priority and assistive technologies
23+ // SHOULD notify the user immediately. Because an interruption may disorient
24+ // users or cause them to not complete their current task, authors SHOULD NOT
25+ // use the assertive value unless the interruption is imperative.
26+ ASSERTIVE = 'assertive' ,
27+ // Updates to the region will not be presented to the user unless the
28+ // assistive teechnology is currently focused on that region.
29+ OFF = 'off' ,
30+ // (Background change) Assistive technologies SHOULD announce the updates at
31+ // the next graceful opportunity, such as at the end of speaking the current
32+ // sentence or when the users pauses typing.
33+ POLITE = 'polite' ,
34+ }
35+
1536/**
1637 * ARIA role values.
1738 * Copied from Closure's goog.a11y.aria.Role
@@ -131,8 +152,12 @@ export enum State {
131152 * @param element DOM node to set role of.
132153 * @param roleName Role name.
133154 */
134- export function setRole ( element : Element , roleName : Role ) {
135- element . setAttribute ( ROLE_ATTRIBUTE , roleName ) ;
155+ export function setRole ( element : Element , roleName : Role | null ) {
156+ if ( roleName ) {
157+ element . setAttribute ( ROLE_ATTRIBUTE , roleName ) ;
158+ } else {
159+ element . removeAttribute ( ROLE_ATTRIBUTE ) ;
160+ }
136161}
137162
138163/**
@@ -156,3 +181,85 @@ export function setState(
156181 const attrStateName = ARIA_PREFIX + stateName ;
157182 element . setAttribute ( attrStateName , `${ value } ` ) ;
158183}
184+
185+ /**
186+ * Creates the centralized ARIA live region used to announce dynamic state
187+ * changes to screen readers. This live region is visually hidden but exposed to
188+ * assistive technologies.
189+ *
190+ * Only one live region should exist per Blockly injection. This function should
191+ * be called during workspace/injection setup to create the region inside the
192+ * Blockly container.
193+ *
194+ * See: https://stackoverflow.com/a/48590836 for a reference.
195+ *
196+ * @param container The container element to which the live region will be
197+ * appended.
198+ */
199+ export function createLiveRegion ( container : HTMLDivElement ) {
200+ const ariaAnnouncementDiv = document . createElement ( 'div' ) ;
201+ ariaAnnouncementDiv . textContent = '' ;
202+ ariaAnnouncementDiv . id = 'blocklyAriaAnnounce' ;
203+ dom . addClass ( ariaAnnouncementDiv , 'hiddenForAria' ) ;
204+ setState ( ariaAnnouncementDiv , State . LIVE , LivePriority . POLITE ) ;
205+ container . appendChild ( ariaAnnouncementDiv ) ;
206+ }
207+
208+ let ariaAnnounceTimeout : ReturnType < typeof setTimeout > ;
209+ let addBreakingSpace = false ;
210+
211+ /**
212+ * Requests that the specified text be announced to the user via a centrally
213+ * managed ARIA live region, if a screen reader is active.
214+ *
215+ * Announcements are scheduled asynchronously. If this function is called again
216+ * before a pending announcement is inserted into the live region, the pending
217+ * announcement is canceled and replaced with the new one.
218+ *
219+ * The live region element must have id `blocklyAriaAnnounce`. Its `aria-live`
220+ * politeness setting and optional `role` are updated before the message is
221+ * inserted so screen readers announce the content correctly.
222+ *
223+ * A non-breaking space is alternated at the end of the message to ensure that
224+ * repeated messages are still announced by screen readers.
225+ *
226+ * Callers should use this judiciously. Over-announcing can reduce usability,
227+ * so this should primarily be used for dynamic states or information that
228+ * cannot be conveyed through standard ARIA semantics.
229+ *
230+ * @param text The text to announce to the user.
231+ * @param options Configuration options for the announcement.
232+ * @param options.assertiveness The ARIA live region priority
233+ * @param options.role Optional ARIA role to apply to the live region before
234+ */
235+ export function announceDynamicAriaState (
236+ text : string ,
237+ options : {
238+ assertiveness : LivePriority ;
239+ role : Role | null ;
240+ } = {
241+ assertiveness : LivePriority . POLITE ,
242+ role : null ,
243+ } ,
244+ ) {
245+ const ariaAnnouncementContainer = document . getElementById (
246+ 'blocklyAriaAnnounce' ,
247+ ) ;
248+ if ( ! ariaAnnouncementContainer ) {
249+ throw new Error ( 'Expected element with id blocklyAriaAnnounce to exist.' ) ;
250+ }
251+ const { assertiveness, role} = options ;
252+
253+ // Clear previous content.
254+ ariaAnnouncementContainer . replaceChildren ( ) ;
255+ setState ( ariaAnnouncementContainer , State . LIVE , assertiveness ) ;
256+
257+ clearTimeout ( ariaAnnounceTimeout ) ;
258+ ariaAnnounceTimeout = setTimeout ( ( ) => {
259+ setRole ( ariaAnnouncementContainer , role ) ;
260+ const p = document . createElement ( 'p' ) ;
261+ p . textContent = text + ( addBreakingSpace ? '\u00A0' : '' ) ;
262+ addBreakingSpace = ! addBreakingSpace ;
263+ ariaAnnouncementContainer . appendChild ( p ) ;
264+ } , 10 ) ;
265+ }
0 commit comments