diff --git a/packages/blockly/core/workspace_svg.ts b/packages/blockly/core/workspace_svg.ts index 4f227d617c0..60919e6a3a5 100644 --- a/packages/blockly/core/workspace_svg.ts +++ b/packages/blockly/core/workspace_svg.ts @@ -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 everFocused = false; + /** * @param options Dictionary of options. */ @@ -2742,6 +2750,15 @@ export class WorkspaceSvg if (!this.isFlyout && !this.isMutator) { this.updateAriaLabel(); } + if (!this.everFocused && !this.options.parentWorkspace) { + aria.announceDynamicAriaState( + Msg['SCREENREADER_HINT'].replace( + '%1', + getShortcutKeysShort(ShortcutNames.TOGGLE_SCREENREADER), + ), + ); + this.everFocused = true; + } } /** See IFocusableNode.onNodeBlur. */ diff --git a/packages/blockly/msg/json/en.json b/packages/blockly/msg/json/en.json index 7c24c37a7db..60738187da3 100644 --- a/packages/blockly/msg/json/en.json +++ b/packages/blockly/msg/json/en.json @@ -2,6 +2,7 @@ "@metadata": { "author": "Ellen Spertus ", "lastupdated": "2026-05-14 08:47:43.920300", + "lastupdated": "2026-05-13 14:56:12.075780", "locale": "en", "messagedocumentation" : "qqq" }, @@ -529,5 +530,6 @@ "SCREENREADER_MODE_DISABLED": "Screenreader mode is off, press %1 to turn it on", "CURRENT_BLOCK_ANNOUNCEMENT": "Current block: %1", "PARENT_BLOCKS_ANNOUNCEMENT": "Parent blocks: %1", - "NO_PARENT_ANNOUNCEMENT": "Current block has no parent" + "NO_PARENT_ANNOUNCEMENT": "Current block has no parent", + "SCREENREADER_HINT": "Use the arrow keys to navigate. Press %1 to toggle screenreader accessibility mode." } diff --git a/packages/blockly/msg/json/qqq.json b/packages/blockly/msg/json/qqq.json index 1d1dca07940..e4acdbe0cc3 100644 --- a/packages/blockly/msg/json/qqq.json +++ b/packages/blockly/msg/json/qqq.json @@ -523,5 +523,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." } diff --git a/packages/blockly/msg/messages.js b/packages/blockly/msg/messages.js index 103e533d91e..aa30d27563c 100644 --- a/packages/blockly/msg/messages.js +++ b/packages/blockly/msg/messages.js @@ -2090,3 +2090,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.'; diff --git a/packages/blockly/tests/mocha/workspace_svg_test.js b/packages/blockly/tests/mocha/workspace_svg_test.js index b40a46941bd..4c435349839 100644 --- a/packages/blockly/tests/mocha/workspace_svg_test.js +++ b/packages/blockly/tests/mocha/workspace_svg_test.js @@ -126,6 +126,28 @@ suite('WorkspaceSvg', function () { assert.isNotNull(gesture); }); + test('Announces a screenreader hint on first focus', function () { + document.getElementById('blocklyAriaAnnounce').textContent = ''; + 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(