Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions packages/blockly/core/utils/aria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*/
Expand Down Expand Up @@ -379,21 +382,51 @@ 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.
clearTimeout(ariaAnnounceTimeout);
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;
}
23 changes: 21 additions & 2 deletions packages/blockly/tests/mocha/aria_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
8 changes: 5 additions & 3 deletions packages/blockly/tests/mocha/keyboard_movement_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)],
Expand All @@ -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,
Expand All @@ -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,
[
Expand Down Expand Up @@ -1114,7 +1116,7 @@ suite('Keyboard-driven movement', function () {

Blockly.getFocusManager().focusNode(boolean);
startMove(this.workspace);

this.clock.tick(10);
this.moveAndAssert(
moveRight,
[
Expand Down Expand Up @@ -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'],
Expand Down