feat!: announce toasts via shared ARIA live region (#9672)

* feat: announce toasts via shared ARIA live region

* chore: add extra space
This commit is contained in:
Michael Harvey
2026-04-03 09:06:11 -04:00
committed by GitHub
parent 3389f87cee
commit 34c265fcf8
2 changed files with 34 additions and 26 deletions
+7 -15
View File
@@ -45,7 +45,7 @@ export interface ToastOptions {
* How prominently/interrupting the readout of the toast should be for
* screenreaders. Corresponds to aria-live and defaults to polite.
*/
assertiveness?: Toast.Assertiveness;
assertiveness?: aria.LiveRegionAssertiveness;
}
/**
@@ -89,15 +89,13 @@ export class Toast {
const {
message,
duration = 5,
assertiveness = Toast.Assertiveness.POLITE,
assertiveness = aria.LiveRegionAssertiveness.POLITE,
} = options;
const toast = document.createElement('div');
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;
@@ -157,6 +155,11 @@ export class Toast {
toast.addEventListener('mouseleave', setToastTimeout);
setToastTimeout();
aria.announceDynamicAriaState(message, {
assertiveness,
role: aria.Role.STATUS,
});
return toast;
}
@@ -174,17 +177,6 @@ export class Toast {
}
}
/**
* Options for how aggressively toasts should be read out by screenreaders.
* Values correspond to those for aria-live.
*/
export namespace Toast {
export enum Assertiveness {
ASSERTIVE = 'assertive',
POLITE = 'polite',
}
}
Css.register(`
.${CLASS_NAME} {
font-size: 1.2rem;
+27 -11
View File
@@ -14,6 +14,7 @@ suite('Toasts', function () {
setup(function () {
sharedTestSetup.call(this);
this.workspace = Blockly.inject('blocklyDiv', {});
this.liveRegion = document.getElementById('blocklyAriaAnnounce');
this.toastIsVisible = (message) => {
const toast = this.workspace
.getInjectionDiv()
@@ -97,16 +98,20 @@ suite('Toasts', function () {
clock.restore();
});
test('default to polite assertiveness', function () {
test('toast announces message with status role and polite assertiveness', function () {
const message = 'texas toast';
Blockly.Toast.show(this.workspace, {message, id: 'test'});
const toast = this.workspace
.getInjectionDiv()
.querySelector('.blocklyToast');
this.clock.tick(11);
assert.include(this.liveRegion.textContent, message);
assert.equal(
toast.getAttribute('aria-live'),
Blockly.Toast.Assertiveness.POLITE,
this.liveRegion.getAttribute('role'),
Blockly.utils.aria.Role.STATUS,
);
assert.equal(
this.liveRegion.getAttribute('aria-live'),
Blockly.utils.aria.LiveRegionAssertiveness.POLITE,
);
});
@@ -115,15 +120,26 @@ suite('Toasts', function () {
Blockly.Toast.show(this.workspace, {
message,
id: 'test',
assertiveness: Blockly.Toast.Assertiveness.ASSERTIVE,
assertiveness: Blockly.utils.aria.LiveRegionAssertiveness.ASSERTIVE,
});
this.clock.tick(11);
assert.equal(
this.liveRegion.getAttribute('aria-live'),
Blockly.utils.aria.LiveRegionAssertiveness.ASSERTIVE,
);
});
test('toast is not itself a live region', function () {
const message = 'texas toast';
Blockly.Toast.show(this.workspace, {message, id: 'test'});
const toast = this.workspace
.getInjectionDiv()
.querySelector('.blocklyToast');
assert.equal(
toast.getAttribute('aria-live'),
Blockly.Toast.Assertiveness.ASSERTIVE,
);
assert.isNull(toast.getAttribute('aria-live'));
assert.notEqual(toast.getAttribute('role'), Blockly.utils.aria.Role.STATUS);
});
});