From 5db32269f3a66eab7a4cf809db9abae7c8259afd Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Thu, 14 May 2026 13:09:05 -0400 Subject: [PATCH 1/2] feat!: add shortcuts to jump between headings in the flyout --- packages/blockly/core/shortcut_items.ts | 128 +++++++++++ packages/blockly/msg/json/en.json | 2 + packages/blockly/msg/json/qqq.json | 2 + packages/blockly/msg/messages.js | 6 + .../tests/mocha/keyboard_navigation_test.js | 205 ++++++++++++++++++ 5 files changed, 343 insertions(+) diff --git a/packages/blockly/core/shortcut_items.ts b/packages/blockly/core/shortcut_items.ts index 87085e79e9b..9428e39768e 100644 --- a/packages/blockly/core/shortcut_items.ts +++ b/packages/blockly/core/shortcut_items.ts @@ -13,12 +13,14 @@ import {RenderedWorkspaceComment} from './comments.js'; import * as contextmenu from './contextmenu.js'; import * as dropDownDiv from './dropdowndiv.js'; import * as eventUtils from './events/utils.js'; +import {FlyoutButton} from './flyout_button.js'; import {getFocusManager} from './focus_manager.js'; import {clearPasteHints, showCopiedHint, showCutHint} from './hints.js'; import {hasContextMenu} from './interfaces/i_contextmenu.js'; import {isCopyable as isICopyable} from './interfaces/i_copyable.js'; import {isDeletable as isIDeletable} from './interfaces/i_deletable.js'; import {type IDraggable, isDraggable} from './interfaces/i_draggable.js'; +import {type IFlyout} from './interfaces/i_flyout.js'; import {type IFocusableNode} from './interfaces/i_focusable_node.js'; import {isSelectable} from './interfaces/i_selectable.js'; import {Direction, KeyboardMover} from './keyboard_nav/keyboard_mover.js'; @@ -69,6 +71,8 @@ export enum names { DUPLICATE = 'duplicate', CLEANUP = 'cleanup', SHOW_TOOLTIP = 'show_tooltip', + NEXT_HEADING = 'next_heading', + PREVIOUS_HEADING = 'previous_heading', } /** @@ -1003,6 +1007,129 @@ export function registerStackNavigation() { ShortcutRegistry.registry.register(previousStackShortcut); } +/** + * Registers keyboard shortcuts to jump between headings (labels) in a flyout. + * + * Pressing H moves focus to the next heading; Shift+H moves focus to the + * previous heading. The shortcut only activates when focus is already inside + * the flyout; otherwise it returns false so other handlers may take over. + */ +export function registerHeadingNavigation() { + /** + * Returns the flyout the user is currently focused in, or null if focus is + * not inside any flyout's workspace. + */ + const getActiveFlyout = (): IFlyout | null => { + const focusedTree = getFocusManager().getFocusedTree(); + if ( + focusedTree instanceof WorkspaceSvg && + focusedTree.isFlyout && + focusedTree.targetWorkspace + ) { + return focusedTree.targetWorkspace.getFlyout() ?? null; + } + return null; + }; + + /** + * Walks up from the focused node to find the top-level flyout item it + * belongs to (e.g. the block whose field is focused). Returns null if no + * flyout item is focused. + */ + const getCurrentFlyoutItem = (flyout: IFlyout): IFocusableNode | null => { + const navigator = flyout.getWorkspace().getNavigator(); + const items: IFocusableNode[] = flyout + .getContents() + .map((item) => item.getElement()); + let node: IFocusableNode | null = + getFocusManager().getFocusedNode() ?? null; + while (node && !items.includes(node)) { + node = navigator.getParent(node); + } + return node; + }; + + /** + * Finds the next or previous heading in the flyout relative to the + * currently focused item, or the first/last heading if no flyout item is + * focused. Returns null if there is no heading to navigate to. + */ + const findHeading = ( + flyout: IFlyout, + direction: 1 | -1, + ): FlyoutButton | null => { + const items: IFocusableNode[] = flyout + .getContents() + .map((item) => item.getElement()); + const current = getCurrentFlyoutItem(flyout); + // When nothing in the flyout is focused, start from before the first item + // (for next) or after the last item (for previous) so the loop finds the + // first or last heading respectively. + const startIndex = current + ? items.indexOf(current) + : direction === 1 + ? -1 + : items.length; + + for (let offset = 1; offset <= items.length; offset++) { + const index = startIndex + direction * offset; + if (index < 0 || index >= items.length) break; + const item = items[index]; + if (item instanceof FlyoutButton && item.isLabel()) { + return item; + } + } + return null; + }; + + const shiftH = ShortcutRegistry.registry.createSerializedKey(KeyCodes.H, [ + KeyCodes.SHIFT, + ]); + + const nextHeadingShortcut: KeyboardShortcut = { + name: names.NEXT_HEADING, + preconditionFn: (workspace) => + !workspace.isDragging() && + !dropDownDiv.isVisible() && + !widgetDiv.isVisible() && + !!getActiveFlyout(), + callback: () => { + const flyout = getActiveFlyout(); + if (!flyout) return false; + const target = findHeading(flyout, 1); + if (!target) return false; + keyboardNavigationController.setIsActive(true); + getFocusManager().focusNode(target); + return true; + }, + keyCodes: [KeyCodes.H], + displayText: () => Msg['SHORTCUTS_NEXT_HEADING'], + }; + + const previousHeadingShortcut: KeyboardShortcut = { + name: names.PREVIOUS_HEADING, + preconditionFn: (workspace) => + !workspace.isDragging() && + !dropDownDiv.isVisible() && + !widgetDiv.isVisible() && + !!getActiveFlyout(), + callback: () => { + const flyout = getActiveFlyout(); + if (!flyout) return false; + const target = findHeading(flyout, -1); + if (!target) return false; + keyboardNavigationController.setIsActive(true); + getFocusManager().focusNode(target); + return true; + }, + keyCodes: [shiftH], + displayText: () => Msg['SHORTCUTS_PREVIOUS_HEADING'], + }; + + ShortcutRegistry.registry.register(nextHeadingShortcut); + ShortcutRegistry.registry.register(previousHeadingShortcut); +} + /** * Registers keyboard shortcut to perform an action on the focused element. */ @@ -1135,6 +1262,7 @@ export function registerKeyboardNavigationShortcuts() { registerArrowNavigation(); registerDisconnectBlock(); registerStackNavigation(); + registerHeadingNavigation(); registerPerformAction(); registerDuplicate(); registerCleanup(); diff --git a/packages/blockly/msg/json/en.json b/packages/blockly/msg/json/en.json index 22cafc3f392..c22692c683c 100644 --- a/packages/blockly/msg/json/en.json +++ b/packages/blockly/msg/json/en.json @@ -448,6 +448,8 @@ "SHORTCUTS_DISCONNECT": "Disconnect block", "SHORTCUTS_NEXT_STACK": "Next stack", "SHORTCUTS_PREVIOUS_STACK": "Previous stack", + "SHORTCUTS_NEXT_HEADING": "Next heading", + "SHORTCUTS_PREVIOUS_HEADING": "Previous heading", "SHORTCUTS_PERFORM_ACTION": "Edit or confirm", "SHORTCUTS_DUPLICATE": "Duplicate", "SHORTCUTS_CLEANUP": "Clean up workspace", diff --git a/packages/blockly/msg/json/qqq.json b/packages/blockly/msg/json/qqq.json index 1867b58f159..06c2a371a0a 100644 --- a/packages/blockly/msg/json/qqq.json +++ b/packages/blockly/msg/json/qqq.json @@ -456,6 +456,8 @@ "SHORTCUTS_DISCONNECT": "shortcut display text for the disconnect shortcut, which disconnects a block from its neighbor.", "SHORTCUTS_NEXT_STACK": "shortcut display text for the next stack shortcut, which navigates to the next block stack.", "SHORTCUTS_PREVIOUS_STACK": "shortcut display text for the previous stack shortcut, which navigates to the previous block stack.", + "SHORTCUTS_NEXT_HEADING": "shortcut display text for the next heading shortcut, which moves focus to the next heading (label) in the flyout.", + "SHORTCUTS_PREVIOUS_HEADING": "shortcut display text for the previous heading shortcut, which moves focus to the previous heading (label) in the flyout.", "SHORTCUTS_PERFORM_ACTION": "shortcut display text for the perform action shortcut, which triggers an action on the focused element.", "SHORTCUTS_DUPLICATE": "shortcut display text for the duplicate shortcut, which duplicates the focused block or comment.", "SHORTCUTS_CLEANUP": "shortcut display text for the cleanup shortcut, which organizes blocks on the workspace.", diff --git a/packages/blockly/msg/messages.js b/packages/blockly/msg/messages.js index 4a71e140ae0..c061478b9b1 100644 --- a/packages/blockly/msg/messages.js +++ b/packages/blockly/msg/messages.js @@ -1775,6 +1775,12 @@ Blockly.Msg.SHORTCUTS_NEXT_STACK = 'Next stack'; /// shortcut display text for the previous stack shortcut, which navigates to the previous block stack. Blockly.Msg.SHORTCUTS_PREVIOUS_STACK = 'Previous stack'; /** @type {string} */ +/// shortcut display text for the next heading shortcut, which moves focus to the next heading (label) in the flyout. +Blockly.Msg.SHORTCUTS_NEXT_HEADING = 'Next heading'; +/** @type {string} */ +/// shortcut display text for the previous heading shortcut, which moves focus to the previous heading (label) in the flyout. +Blockly.Msg.SHORTCUTS_PREVIOUS_HEADING = 'Previous heading'; +/** @type {string} */ /// shortcut display text for the perform action shortcut, which triggers an action on the focused element. Blockly.Msg.SHORTCUTS_PERFORM_ACTION = 'Edit or confirm'; /** @type {string} */ diff --git a/packages/blockly/tests/mocha/keyboard_navigation_test.js b/packages/blockly/tests/mocha/keyboard_navigation_test.js index 21cee04628f..14851aaba70 100644 --- a/packages/blockly/tests/mocha/keyboard_navigation_test.js +++ b/packages/blockly/tests/mocha/keyboard_navigation_test.js @@ -867,3 +867,208 @@ suite('Toolbox and flyout arrow navigation by layout', function () { }); } }); + +suite('Flyout heading navigation (H / Shift+H)', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + type: 'basic_block', + message0: '%1', + args0: [ + { + type: 'field_input', + name: 'TEXT', + text: 'default', + }, + ], + }, + ]); + // Build a flyout toolbox that mixes blocks and headings (labels) so we + // can verify that the H shortcut jumps over non-heading items. + this.workspace = Blockly.inject('blocklyDiv', { + toolbox: { + kind: 'flyoutToolbox', + contents: [ + {kind: 'label', text: 'First heading'}, + {kind: 'block', type: 'basic_block'}, + {kind: 'block', type: 'basic_block'}, + {kind: 'label', text: 'Second heading'}, + {kind: 'block', type: 'basic_block'}, + {kind: 'label', text: 'Third heading'}, + {kind: 'block', type: 'basic_block'}, + ], + }, + }); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + /** + * Returns all FlyoutButton labels (headings) currently in the flyout. + * + * @param {!Blockly.WorkspaceSvg} workspace The main workspace owning the + * flyout. + * @returns {!Array} The labels in flyout order. + */ + function getHeadings(workspace) { + return workspace + .getFlyout() + .getContents() + .map((item) => item.getElement()) + .filter( + (element) => + element instanceof Blockly.FlyoutButton && element.isLabel(), + ); + } + + test('Shortcut is a no-op when focus is on the main workspace', function () { + Blockly.getFocusManager().focusTree(this.workspace); + const before = Blockly.getFocusManager().getFocusedNode(); + pressKey(this.workspace, Blockly.utils.KeyCodes.H); + assert.equal(Blockly.getFocusManager().getFocusedNode(), before); + }); + + test('Shortcut is a no-op when focus is on a workspace block', function () { + const block = this.workspace.newBlock('basic_block'); + block.initSvg(); + block.render(); + Blockly.getFocusManager().focusNode(block); + pressKey(this.workspace, Blockly.utils.KeyCodes.H); + assert.equal(Blockly.getFocusManager().getFocusedNode(), block); + }); + + test('H from flyout workspace focuses the first heading', function () { + Blockly.getFocusManager().focusNode( + this.workspace.getFlyout().getWorkspace(), + ); + pressKey(this.workspace, Blockly.utils.KeyCodes.H); + const headings = getHeadings(this.workspace); + assert.equal(Blockly.getFocusManager().getFocusedNode(), headings[0]); + }); + + test('H from a block in the flyout focuses the next heading', function () { + Blockly.getFocusManager().focusNode( + this.workspace.getFlyout().getWorkspace().getTopBlocks()[0], + ); + pressKey(this.workspace, Blockly.utils.KeyCodes.H); + const headings = getHeadings(this.workspace); + assert.equal(Blockly.getFocusManager().getFocusedNode(), headings[1]); + }); + + test('H from a heading focuses the next heading', function () { + const headings = getHeadings(this.workspace); + Blockly.getFocusManager().focusNode(headings[0]); + pressKey(this.workspace, Blockly.utils.KeyCodes.H); + assert.equal(Blockly.getFocusManager().getFocusedNode(), headings[1]); + }); + + test('H from the last heading does nothing', function () { + const headings = getHeadings(this.workspace); + Blockly.getFocusManager().focusNode(headings[headings.length - 1]); + pressKey(this.workspace, Blockly.utils.KeyCodes.H); + assert.equal( + Blockly.getFocusManager().getFocusedNode(), + headings[headings.length - 1], + ); + }); + + test('Shift+H from flyout workspace focuses the last heading', function () { + Blockly.getFocusManager().focusNode( + this.workspace.getFlyout().getWorkspace(), + ); + pressKey(this.workspace, Blockly.utils.KeyCodes.H, [ + Blockly.utils.KeyCodes.SHIFT, + ]); + const headings = getHeadings(this.workspace); + assert.equal( + Blockly.getFocusManager().getFocusedNode(), + headings[headings.length - 1], + ); + }); + + test('Shift+H from a heading focuses the previous heading', function () { + const headings = getHeadings(this.workspace); + Blockly.getFocusManager().focusNode(headings[2]); + pressKey(this.workspace, Blockly.utils.KeyCodes.H, [ + Blockly.utils.KeyCodes.SHIFT, + ]); + assert.equal(Blockly.getFocusManager().getFocusedNode(), headings[1]); + }); + + test('Shift+H from a block focuses the previous heading', function () { + Blockly.getFocusManager().focusNode( + this.workspace.getFlyout().getWorkspace().getTopBlocks()[2], + ); + pressKey(this.workspace, Blockly.utils.KeyCodes.H, [ + Blockly.utils.KeyCodes.SHIFT, + ]); + const headings = getHeadings(this.workspace); + assert.equal(Blockly.getFocusManager().getFocusedNode(), headings[1]); + }); + + test('Shift+H from the first heading does nothing', function () { + const headings = getHeadings(this.workspace); + Blockly.getFocusManager().focusNode(headings[0]); + pressKey(this.workspace, Blockly.utils.KeyCodes.H, [ + Blockly.utils.KeyCodes.SHIFT, + ]); + assert.equal(Blockly.getFocusManager().getFocusedNode(), headings[0]); + }); +}); + +suite('Flyout heading navigation with no headings', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + type: 'basic_block', + message0: '%1', + args0: [ + { + type: 'field_input', + name: 'TEXT', + text: 'default', + }, + ], + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', { + toolbox: { + kind: 'flyoutToolbox', + contents: [ + {kind: 'block', type: 'basic_block'}, + {kind: 'block', type: 'basic_block'}, + ], + }, + }); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + test('H does nothing when the flyout has no headings', function () { + const firstBlock = this.workspace + .getFlyout() + .getWorkspace() + .getTopBlocks()[0]; + Blockly.getFocusManager().focusNode(firstBlock); + pressKey(this.workspace, Blockly.utils.KeyCodes.H); + assert.equal(Blockly.getFocusManager().getFocusedNode(), firstBlock); + }); + + test('Shift+H does nothing when the flyout has no headings', function () { + const firstBlock = this.workspace + .getFlyout() + .getWorkspace() + .getTopBlocks()[0]; + Blockly.getFocusManager().focusNode(firstBlock); + pressKey(this.workspace, Blockly.utils.KeyCodes.H, [ + Blockly.utils.KeyCodes.SHIFT, + ]); + assert.equal(Blockly.getFocusManager().getFocusedNode(), firstBlock); + }); +}); From 0bccfaaafc0f59d60e6b0dc8209b2f0e42173b0f Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Thu, 14 May 2026 15:23:45 -0400 Subject: [PATCH 2/2] feat: show a hint if user presses enter on flyout label --- packages/blockly/core/flyout_button.ts | 17 ++++++--- packages/blockly/core/hints.ts | 16 ++++++++ packages/blockly/msg/json/en.json | 1 + packages/blockly/msg/json/qqq.json | 1 + packages/blockly/msg/messages.js | 3 ++ .../tests/mocha/shortcut_items_test.js | 37 +++++++++++++++++++ 6 files changed, 69 insertions(+), 6 deletions(-) diff --git a/packages/blockly/core/flyout_button.ts b/packages/blockly/core/flyout_button.ts index 00782a19a00..3083fa8e19e 100644 --- a/packages/blockly/core/flyout_button.ts +++ b/packages/blockly/core/flyout_button.ts @@ -13,6 +13,7 @@ import * as browserEvents from './browser_events.js'; import * as Css from './css.js'; +import * as hints from './hints.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; @@ -434,14 +435,18 @@ export class FlyoutButton /** * Handles the user acting on this button via keyboard navigation. - * Invokes the click handler callback. + * Invokes the click handler callback for buttons. For labels, which are not + * interactive, shows a toast directing the user to navigate using the arrow + * keys or the next-heading shortcut. */ performAction(): void { - if (!this.isFlyoutLabel) { - const callback = this.targetWorkspace.getButtonCallback(this.callbackKey); - if (callback) { - callback(this); - } + if (this.isFlyoutLabel) { + hints.showFlyoutLabelActionHint(this.targetWorkspace); + return; + } + const callback = this.targetWorkspace.getButtonCallback(this.callbackKey); + if (callback) { + callback(this); } } } diff --git a/packages/blockly/core/hints.ts b/packages/blockly/core/hints.ts index d6b12cca030..b6a24ac2339 100644 --- a/packages/blockly/core/hints.ts +++ b/packages/blockly/core/hints.ts @@ -16,6 +16,7 @@ const constrainedMoveHintId = 'constrainedMoveHint'; const helpHintId = 'helpHint'; const blockNavigationHintId = 'blockNavigationHint'; const workspaceNavigationHintId = 'workspaceNavigationHint'; +const flyoutLabelHintId = 'flyoutLabelHint'; const copiedHintId = 'copiedHint'; const cutHintId = 'cutHint'; @@ -112,6 +113,21 @@ export function showWorkspaceNavigationHint(workspace: WorkspaceSvg) { Toast.show(workspace, {message, id}); } +/** + * Tell the user how to navigate away from a flyout label (heading) when they + * try to act on it. Labels are not interactive, so direct them to use the + * arrow keys to reach a block or the next-heading shortcut to skip ahead. + * + * @param workspace The workspace. + */ +export function showFlyoutLabelActionHint(workspace: WorkspaceSvg) { + const message = Msg['KEYBOARD_NAV_FLYOUT_LABEL_HINT'].replace( + '%1', + getShortcutKeysShort(names.NEXT_HEADING), + ); + Toast.show(workspace, {message, id: flyoutLabelHintId}); +} + /** * Nudge the user to paste after a copy. * diff --git a/packages/blockly/msg/json/en.json b/packages/blockly/msg/json/en.json index c22692c683c..8614494917d 100644 --- a/packages/blockly/msg/json/en.json +++ b/packages/blockly/msg/json/en.json @@ -469,6 +469,7 @@ "WORKSPACE_CONTENTS_COMMENTS_ONE": " and one comment", "KEYBOARD_NAV_BLOCK_NAVIGATION_HINT": "Use %1 to navigate inside of blocks.", "KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT": "Use the arrow keys to navigate.", + "KEYBOARD_NAV_FLYOUT_LABEL_HINT": "Use the arrow keys to navigate to a block, or press %1 to go to the next heading.", "BLOCK_LABEL_BEGIN_STACK": "Begin stack", "BLOCK_LABEL_BEGIN_PREFIX": "Begin %1", "BLOCK_LABEL_TOOLBOX_CATEGORY": "%1 category", diff --git a/packages/blockly/msg/json/qqq.json b/packages/blockly/msg/json/qqq.json index 06c2a371a0a..e71d24b7f4f 100644 --- a/packages/blockly/msg/json/qqq.json +++ b/packages/blockly/msg/json/qqq.json @@ -477,6 +477,7 @@ "WORKSPACE_CONTENTS_COMMENTS_ONE": "ARIA live region phrase appended when there is exactly one workspace comment.", "KEYBOARD_NAV_BLOCK_NAVIGATION_HINT": "Message shown when a user presses Enter with a navigable block focused.", "KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT": "Message shown when a user presses Enter with the workspace focused.", + "KEYBOARD_NAV_FLYOUT_LABEL_HINT": "Message shown when a user presses Enter with a flyout label (heading) focused. Placeholder %1 is the keyboard shortcut for navigating to the next heading.", "BLOCK_LABEL_BEGIN_STACK": "Part of an accessibility label for a block that indicates it is the first block in the stack.", "BLOCK_LABEL_BEGIN_PREFIX": "Part of an accessibility label for a block that indicates it is the first block inside of a statement input. Placeholder corresponds to the parent statement input's accessibility label.", "BLOCK_LABEL_TOOLBOX_CATEGORY": "Part of an accessibility label for a block that indicates its parent toolbox category. Placeholder corresponds to a category name, e.g. 'Logic' or 'Math'.", diff --git a/packages/blockly/msg/messages.js b/packages/blockly/msg/messages.js index c061478b9b1..656177ee887 100644 --- a/packages/blockly/msg/messages.js +++ b/packages/blockly/msg/messages.js @@ -1853,6 +1853,9 @@ Blockly.Msg.KEYBOARD_NAV_BLOCK_NAVIGATION_HINT = 'Use %1 to navigate inside of b /// Message shown when a user presses Enter with the workspace focused. Blockly.Msg.KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT = 'Use the arrow keys to navigate.'; /** @type {string} */ +/// Message shown when a user presses Enter with a flyout label (heading) focused. +Blockly.Msg.KEYBOARD_NAV_FLYOUT_LABEL_HINT = 'Use the arrow keys to navigate to a block, or press %1 to go to the next heading.'; +/** @type {string} */ /// Part of an accessibility label for a block that indicates it is the first /// block in the stack. Blockly.Msg.BLOCK_LABEL_BEGIN_STACK = 'Begin stack'; diff --git a/packages/blockly/tests/mocha/shortcut_items_test.js b/packages/blockly/tests/mocha/shortcut_items_test.js index 239b9524fc5..a194e32eb0f 100644 --- a/packages/blockly/tests/mocha/shortcut_items_test.js +++ b/packages/blockly/tests/mocha/shortcut_items_test.js @@ -1499,6 +1499,43 @@ suite('Keyboard Shortcut Items', function () { ws.dispose(); }); + test('Shows a toast with navigation hints for flyout labels', function () { + const ws = Blockly.inject('blocklyDiv', { + toolbox: { + kind: 'flyoutToolbox', + contents: [ + {kind: 'label', text: 'A heading'}, + {kind: 'block', type: 'stack_block'}, + ], + }, + }); + const toastSpy = sinon.spy(Blockly.Toast, 'show'); + + const label = ws + .getFlyout() + .getContents() + .map((item) => item.getElement()) + .find( + (element) => + element instanceof Blockly.FlyoutButton && element.isLabel(), + ); + assert.exists(label, 'Expected a flyout label in the test fixture'); + Blockly.getFocusManager().focusNode(label); + + const event = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + ws.getInjectionDiv().dispatchEvent(event); + + sinon.assert.calledWith(toastSpy, ws, { + id: 'flyoutLabelHint', + message: Blockly.Msg['KEYBOARD_NAV_FLYOUT_LABEL_HINT'].replace( + '%1', + 'H', + ), + }); + toastSpy.restore(); + ws.dispose(); + }); + // Reenable this tests once the shortcut listing shortcut has been added. test.skip('Shows a toast with instructions to view help for non-navigable blocks', function () { const toastSpy = sinon.spy(Blockly.Toast, 'show');