mirror of
https://github.com/google/blockly.git
synced 2026-05-29 23:40:08 +02:00
feat!: add shortcuts to navigate between headings in the flyout (#9874)
* feat!: add shortcuts to jump between headings in the flyout * feat: show a hint if user presses enter on flyout label
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
const screenreaderHintId = 'screenreaderHint';
|
||||
@@ -113,6 +114,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.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,7 @@ 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,
|
||||
@@ -24,6 +25,7 @@ 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';
|
||||
@@ -74,6 +76,8 @@ export enum names {
|
||||
DUPLICATE = 'duplicate',
|
||||
CLEANUP = 'cleanup',
|
||||
SHOW_TOOLTIP = 'show_tooltip',
|
||||
NEXT_HEADING = 'next_heading',
|
||||
PREVIOUS_HEADING = 'previous_heading',
|
||||
TOGGLE_SCREENREADER = 'toggle_screenreader',
|
||||
}
|
||||
|
||||
@@ -1023,6 +1027,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.
|
||||
*/
|
||||
@@ -1190,6 +1317,7 @@ export function registerKeyboardNavigationShortcuts() {
|
||||
registerArrowNavigation();
|
||||
registerDisconnectBlock();
|
||||
registerStackNavigation();
|
||||
registerHeadingNavigation();
|
||||
registerPerformAction();
|
||||
registerDuplicate();
|
||||
registerCleanup();
|
||||
|
||||
@@ -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",
|
||||
@@ -468,6 +470,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",
|
||||
|
||||
@@ -442,6 +442,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.",
|
||||
@@ -462,6 +464,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'.",
|
||||
|
||||
@@ -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} */
|
||||
@@ -1850,6 +1856,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';
|
||||
|
||||
@@ -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<!Blockly.FlyoutButton>} 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user