fix: Make live region coalesce messages (#9778)

* fix: Make live region coalesce messages

* chore: Remove dead code

* fix: Fix test
This commit is contained in:
Aaron Dodson
2026-04-30 13:16:57 -07:00
committed by GitHub
parent 771f9eabe1
commit b714ef7fd6
3 changed files with 61 additions and 7 deletions
+35 -2
View File
@@ -33,6 +33,9 @@ export enum LiveRegionAssertiveness {
POLITE = 'polite',
}
let nextAnnouncementAssertiveness = LiveRegionAssertiveness.OFF;
const queuedAnnouncements: string[] = [];
/**
* Customization options that can be passed when using `announceDynamicAriaState`.
*/
@@ -386,6 +389,12 @@ export function announceDynamicAriaState(
role = DEFAULT_LIVE_REGION_ROLE,
} = options || {};
queuedAnnouncements.push(text);
nextAnnouncementAssertiveness = mostAssertive(
assertiveness,
nextAnnouncementAssertiveness,
);
// We use a short delay so rapid successive calls collapse into a single
// announcement, and to ensure assistive technologies reliably detect the
// DOM change.
@@ -393,14 +402,38 @@ export function announceDynamicAriaState(
ariaAnnounceTimeout = setTimeout(() => {
// Clear previous content.
ariaAnnouncementContainer.replaceChildren();
setState(ariaAnnouncementContainer, State.LIVE, assertiveness);
setState(
ariaAnnouncementContainer,
State.LIVE,
nextAnnouncementAssertiveness,
);
setRole(ariaAnnouncementContainer, role);
const span = document.createElement('span');
// The non-breaking space toggle ensures otherwise identical consecutive
// messages are still announced.
span.textContent = text + (addBreakingSpace ? '\u00A0' : '');
span.textContent =
queuedAnnouncements.join('\n') + (addBreakingSpace ? '\u00A0' : '');
addBreakingSpace = !addBreakingSpace;
ariaAnnouncementContainer.appendChild(span);
queuedAnnouncements.length = 0;
nextAnnouncementAssertiveness = LiveRegionAssertiveness.OFF;
}, 10);
}
/** Returns the maximally assertive of the given assertiveness levels. */
function mostAssertive(a: LiveRegionAssertiveness, b: LiveRegionAssertiveness) {
if (
a === LiveRegionAssertiveness.ASSERTIVE ||
b === LiveRegionAssertiveness.ASSERTIVE
) {
return LiveRegionAssertiveness.ASSERTIVE;
} else if (
a === LiveRegionAssertiveness.POLITE ||
b === LiveRegionAssertiveness.POLITE
) {
return LiveRegionAssertiveness.POLITE;
}
return LiveRegionAssertiveness.OFF;
}
+21 -2
View File
@@ -103,14 +103,33 @@ suite('ARIA', function () {
assert.notEqual(first, second);
});
test('last write wins when called rapidly', function () {
test('Coalesces messages when called rapidly', function () {
Blockly.utils.aria.announceDynamicAriaState('First message');
Blockly.utils.aria.announceDynamicAriaState('Second message');
Blockly.utils.aria.announceDynamicAriaState('Final message');
this.clock.tick(11);
assert.include(this.liveRegion.textContent, 'Final message');
assert.include(
this.liveRegion.textContent,
'First message\nSecond message\nFinal message',
);
});
test('Uses maximal assertiveness when coalescing', function () {
Blockly.utils.aria.announceDynamicAriaState('First message', {
assertiveness: Blockly.utils.aria.LiveRegionAssertiveness.OFF,
});
Blockly.utils.aria.announceDynamicAriaState('Second message', {
assertiveness: Blockly.utils.aria.LiveRegionAssertiveness.ASSERTIVE,
});
Blockly.utils.aria.announceDynamicAriaState('Final message', {
assertiveness: Blockly.utils.aria.LiveRegionAssertiveness.POLITE,
});
this.clock.tick(11);
assert.equal(this.liveRegion.getAttribute('aria-live'), 'assertive');
});
test('assertive option sets aria-live assertive', function () {
@@ -1040,7 +1040,7 @@ suite('Keyboard-driven movement', function () {
Blockly.getFocusManager().focusNode(valueBlock);
startMove(this.workspace);
this.clock.tick(10);
this.moveAndAssert(
moveRight,
['moving', 'inside', this.getBlockLabel(parent)],
@@ -1057,6 +1057,7 @@ suite('Keyboard-driven movement', function () {
Blockly.getFocusManager().focusNode(loop);
startMove(this.workspace);
moveRight(this.workspace);
this.clock.tick(10);
this.moveAndAssert(
moveRight,
@@ -1081,6 +1082,7 @@ suite('Keyboard-driven movement', function () {
Blockly.getFocusManager().focusNode(ifBlock);
startMove(this.workspace); // on workspace
moveRight(this.workspace); // before block1
this.clock.tick(10);
this.moveAndAssert(
moveRight,
[
@@ -1114,7 +1116,7 @@ suite('Keyboard-driven movement', function () {
Blockly.getFocusManager().focusNode(boolean);
startMove(this.workspace);
this.clock.tick(10);
this.moveAndAssert(
moveRight,
[
@@ -1151,7 +1153,7 @@ suite('Keyboard-driven movement', function () {
Blockly.getFocusManager().focusNode(text);
startMove(this.workspace);
moveRight(this.workspace); // First labeled input
this.clock.tick(10);
this.moveAndAssert(
moveRight,
['moving', 'inside', this.getBlockLabel(textJoin), 'input 2'],