diff --git a/core/block_svg.ts b/core/block_svg.ts index 00b3b816d44..55e9c505454 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -2014,7 +2014,7 @@ export class BlockSvg newLoc?: Coordinate, ) { if (isCanceled) { - aria.announceDynamicAriaState('Canceled movement'); + aria.announceDynamicAriaState('Canceled movement.'); return; } if (!isMoving) return; @@ -2036,7 +2036,7 @@ export class BlockSvg // If the block is currently being moved, announce the new block label so that the user understands where it is now. // TODO: Figure out how much recomputeAriaTreeItemDetailsRecursively needs to anticipate position if it won't be reannounced, and how much of that context should be included in the liveannouncement. - aria.announceDynamicAriaState(announcementContext.join(' ')); + aria.announceDynamicAriaState(announcementContext.join(' ') + '.'); } else if (newLoc) { // The block is being freely dragged. aria.announceDynamicAriaState( diff --git a/core/inject.ts b/core/inject.ts index f5f04b5c387..1956c45930b 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -83,10 +83,10 @@ export function inject( ); // See: https://stackoverflow.com/a/48590836 for a reference. - const ariaAnnouncementSpan = document.createElement('span'); + const ariaAnnouncementSpan = document.createElement('div'); ariaAnnouncementSpan.id = 'blocklyAriaAnnounce'; dom.addClass(ariaAnnouncementSpan, 'hiddenForAria'); - aria.setState(ariaAnnouncementSpan, aria.State.LIVE, 'assertive'); + aria.setState(ariaAnnouncementSpan, aria.State.LIVE, 'polite'); subContainer.appendChild(ariaAnnouncementSpan); return workspace; diff --git a/core/toast.ts b/core/toast.ts index 72559279f57..48d25df66da 100644 --- a/core/toast.ts +++ b/core/toast.ts @@ -96,9 +96,6 @@ export class Toast { workspace.getInjectionDiv().appendChild(toast); toast.dataset.toastId = options.id; toast.className = CLASS_NAME; - aria.setRole(toast, aria.Role.STATUS); - aria.setState(toast, aria.State.LIVE, assertiveness); - const messageElement = toast.appendChild(document.createElement('div')); messageElement.className = MESSAGE_CLASS_NAME; messageElement.innerText = message; @@ -157,6 +154,11 @@ export class Toast { toast.addEventListener('mouseleave', setToastTimeout); setToastTimeout(); + aria.announceDynamicAriaState(message, { + assertiveness, + role: aria.Role.STATUS, + }); + return toast; } diff --git a/core/utils/aria.ts b/core/utils/aria.ts index 84d4e2312c4..ce329133a52 100644 --- a/core/utils/aria.ts +++ b/core/utils/aria.ts @@ -201,6 +201,10 @@ export function getState(element: Element, stateName: State): string | null { return element.getAttribute(attrStateName); } +let ariaAnnounceTimeout: ReturnType; +let ariaAnnounceHtml = ''; +let addBreakingSpace = false; + /** * Assertively requests that the specified text be read to the user if a screen * reader is currently active. @@ -217,10 +221,31 @@ export function getState(element: Element, stateName: State): string | null { * * @param text The text to read to the user. */ -export function announceDynamicAriaState(text: string) { - const ariaAnnouncementSpan = document.getElementById('blocklyAriaAnnounce'); - if (!ariaAnnouncementSpan) { +export function announceDynamicAriaState( + text: string, + options: { + assertiveness: string; + role: Role | null; + } = { + assertiveness: 'polite', + role: null, + }, +) { + const ariaAnnouncementContainer = document.getElementById( + 'blocklyAriaAnnounce', + ); + if (!ariaAnnouncementContainer) { throw new Error('Expected element with id blocklyAriaAnnounce to exist.'); } - ariaAnnouncementSpan.innerHTML = text; + const {assertiveness, role} = options; + ariaAnnouncementContainer.innerHTML = ''; + setState(ariaAnnouncementContainer, State.LIVE, assertiveness); + ariaAnnounceHtml += `

${text}${addBreakingSpace ? ' ' : ''}

`; + addBreakingSpace = !addBreakingSpace; + clearTimeout(ariaAnnounceTimeout); + ariaAnnounceTimeout = setTimeout(() => { + setRole(ariaAnnouncementContainer, role); + ariaAnnouncementContainer.innerHTML = ariaAnnounceHtml; + ariaAnnounceHtml = ''; + }, 10); }