feat: Announce usage hints to screenreaders on initial workspace focus (#9871)

* feat: Announce usage hints to screenreaders on initial workspace focus

* fix: Improve robustness of tests

* fix: Fix merge

* fix: Don't announce screenreader usage on first focus of every workspace

* fix: Fix test
This commit is contained in:
Aaron Dodson
2026-05-21 10:01:20 -07:00
committed by GitHub
parent e66dac5ffd
commit 715e119d2d
5 changed files with 661 additions and 616 deletions
+17
View File
@@ -70,6 +70,7 @@ import * as registry from './registry.js';
import * as blockRendering from './renderers/common/block_rendering.js';
import type {Renderer} from './renderers/common/renderer.js';
import type {ScrollbarPair} from './scrollbar_pair.js';
import {names as ShortcutNames} from './shortcut_items.js';
import type {Theme} from './theme.js';
import {Classic} from './theme/classic.js';
import {ThemeManager} from './theme_manager.js';
@@ -82,6 +83,7 @@ import * as dom from './utils/dom.js';
import * as drag from './utils/drag.js';
import type {Metrics} from './utils/metrics.js';
import {Rect} from './utils/rect.js';
import {getShortcutKeysShort} from './utils/shortcut_formatting.js';
import {Size} from './utils/size.js';
import {Svg} from './utils/svg.js';
import * as svgMath from './utils/svg_math.js';
@@ -349,6 +351,12 @@ export class WorkspaceSvg
*/
private navigator = new Navigator();
/**
* Whether this workspace has ever been focused. Used to announce usage hints
* to screenreaders on initial focus only.
*/
private static everFocused = false;
/**
* @param options Dictionary of options.
*/
@@ -2742,6 +2750,15 @@ export class WorkspaceSvg
if (!this.isFlyout && !this.isMutator) {
this.updateAriaLabel();
}
if (!WorkspaceSvg.everFocused && !this.options.parentWorkspace) {
aria.announceDynamicAriaState(
Msg['SCREENREADER_HINT'].replace(
'%1',
getShortcutKeysShort(ShortcutNames.TOGGLE_SCREENREADER),
),
);
WorkspaceSvg.everFocused = true;
}
}
/** See IFocusableNode.onNodeBlur. */
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -607,5 +607,6 @@
"SCREENREADER_MODE_DISABLED": "Message announced when screenreader optimization mode is turned off.",
"CURRENT_BLOCK_ANNOUNCEMENT": "Screenreader announcement providing context about the currently focused block.",
"PARENT_BLOCKS_ANNOUNCEMENT": "Screenreader announcement providing context about the currently focused block's parents.",
"NO_PARENT_ANNOUNCEMENT": "Screenreader announcement informing users that the currently focused block has no parent blocks."
"NO_PARENT_ANNOUNCEMENT": "Screenreader announcement informing users that the currently focused block has no parent blocks.",
"SCREENREADER_HINT": "Message announced when screenreader optimization mode is turned off."
}
+3
View File
@@ -2350,3 +2350,6 @@ Blockly.Msg.PARENT_BLOCKS_ANNOUNCEMENT = 'Parent blocks: %1';
/** @type {string} */
/// Screenreader announcement informing users that the currently focused block has no parent blocks.
Blockly.Msg.NO_PARENT_ANNOUNCEMENT = 'Current block has no parent';
/** @type {string} */
/// Message announced when screenreader optimization mode is turned off.
Blockly.Msg.SCREENREADER_HINT = 'Use the arrow keys to navigate. Press %1 to toggle screenreader accessibility mode.';
@@ -126,6 +126,29 @@ suite('WorkspaceSvg', function () {
assert.isNotNull(gesture);
});
test('Announces a screenreader hint on first focus', function () {
document.getElementById('blocklyAriaAnnounce').textContent = '';
Blockly.WorkspaceSvg.everFocused = false;
Blockly.getFocusManager().focusNode(this.workspace);
this.clock.runAll();
assert.include(
document.getElementById('blocklyAriaAnnounce').textContent,
'Use the arrow keys to navigate',
);
});
test('Nested workspaces do not announce screenreader hints', function () {
document.getElementById('blocklyAriaAnnounce').textContent = '';
Blockly.getFocusManager().focusNode(
this.workspace.getFlyout().getWorkspace(),
);
this.clock.runAll();
assert.notInclude(
document.getElementById('blocklyAriaAnnounce').textContent,
'Use the arrow keys to navigate',
);
});
suite('updateToolbox', function () {
test('Passes in null when toolbox exists', function () {
assert.throws(