Skip to content

Commit fc401d3

Browse files
authored
Merge branch 'v13' into lights-camera-action
2 parents 3c83dd7 + b49fe1e commit fc401d3

8 files changed

Lines changed: 222 additions & 38 deletions

File tree

packages/blockly/core/shortcut_items.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import {type IFocusableNode} from './interfaces/i_focusable_node.js';
2020
import {isSelectable} from './interfaces/i_selectable.js';
2121
import {Direction, KeyboardMover} from './keyboard_nav/keyboard_mover.js';
2222
import {keyboardNavigationController} from './keyboard_navigation_controller.js';
23+
import {Msg} from './msg.js';
2324
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
25+
import {aria} from './utils.js';
2426
import {Coordinate} from './utils/coordinate.js';
2527
import {KeyCodes} from './utils/keycodes.js';
2628
import {Rect} from './utils/rect.js';
@@ -57,6 +59,7 @@ export enum names {
5759
PERFORM_ACTION = 'perform_action',
5860
NEXT_STACK = 'next_stack',
5961
PREVIOUS_STACK = 'previous_stack',
62+
INFORMATION = 'information',
6063
}
6164

6265
/**
@@ -639,20 +642,20 @@ export function registerArrowNavigation() {
639642
}
640643
}
641644

645+
const resolveWorkspace = (workspace: WorkspaceSvg) => {
646+
if (workspace.isFlyout) {
647+
const target = workspace.targetWorkspace;
648+
if (target) {
649+
return resolveWorkspace(target);
650+
}
651+
}
652+
return workspace.getRootWorkspace() ?? workspace;
653+
};
654+
642655
/**
643656
* Registers keyboard shortcut to focus the workspace.
644657
*/
645658
export function registerFocusWorkspace() {
646-
const resolveWorkspace = (workspace: WorkspaceSvg) => {
647-
if (workspace.isFlyout) {
648-
const target = workspace.targetWorkspace;
649-
if (target) {
650-
return resolveWorkspace(target);
651-
}
652-
}
653-
return workspace.getRootWorkspace() ?? workspace;
654-
};
655-
656659
const focusWorkspaceShortcut: KeyboardShortcut = {
657660
name: names.FOCUS_WORKSPACE,
658661
preconditionFn: (workspace) => !workspace.isDragging(),
@@ -693,6 +696,55 @@ export function registerFocusToolbox() {
693696
ShortcutRegistry.registry.register(focusToolboxShortcut);
694697
}
695698

699+
/**
700+
* Registers keyboard shortcut to get count of block stacks and comments.
701+
*/
702+
export function registerWorkspaceOverview() {
703+
const shortcut: KeyboardShortcut = {
704+
name: names.INFORMATION,
705+
preconditionFn: (workspace, scope) => {
706+
const focused = scope.focusedNode;
707+
return focused === workspace;
708+
},
709+
callback: (_workspace) => {
710+
const workspace = resolveWorkspace(_workspace);
711+
const stackCount = workspace.getTopBlocks().length;
712+
const commentCount = workspace.getTopComments().length;
713+
714+
// Build base string with block stack count.
715+
let baseMsgKey;
716+
if (stackCount === 0) {
717+
baseMsgKey = 'WORKSPACE_CONTENTS_BLOCKS_ZERO';
718+
} else if (stackCount === 1) {
719+
baseMsgKey = 'WORKSPACE_CONTENTS_BLOCKS_ONE';
720+
} else {
721+
baseMsgKey = 'WORKSPACE_CONTENTS_BLOCKS_MANY';
722+
}
723+
724+
// Build comment suffix.
725+
let suffix = '';
726+
if (commentCount > 0) {
727+
suffix = Msg[
728+
commentCount === 1
729+
? 'WORKSPACE_CONTENTS_COMMENTS_ONE'
730+
: 'WORKSPACE_CONTENTS_COMMENTS_MANY'
731+
].replace('%1', String(commentCount));
732+
}
733+
734+
// Build final message.
735+
const msg = Msg[baseMsgKey]
736+
.replace('%1', String(stackCount))
737+
.replace('%2', suffix);
738+
739+
aria.announceDynamicAriaState(msg);
740+
741+
return true;
742+
},
743+
keyCodes: [KeyCodes.I],
744+
};
745+
ShortcutRegistry.registry.register(shortcut);
746+
}
747+
696748
/**
697749
* Registers keyboard shortcut to disconnect the focused block.
698750
*/
@@ -843,5 +895,13 @@ export function registerKeyboardNavigationShortcuts() {
843895
registerStackNavigation();
844896
}
845897

898+
/**
899+
* Registers keyboard shortcuts used to announce screen reader information.
900+
*/
901+
export function registerScreenReaderShortcuts() {
902+
registerWorkspaceOverview();
903+
}
904+
846905
registerDefaultShortcuts();
847906
registerKeyboardNavigationShortcuts();
907+
registerScreenReaderShortcuts();

packages/blockly/core/toast.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export interface ToastOptions {
4545
* How prominently/interrupting the readout of the toast should be for
4646
* screenreaders. Corresponds to aria-live and defaults to polite.
4747
*/
48-
assertiveness?: Toast.Assertiveness;
48+
assertiveness?: aria.LiveRegionAssertiveness;
4949
}
5050

5151
/**
@@ -89,15 +89,13 @@ export class Toast {
8989
const {
9090
message,
9191
duration = 5,
92-
assertiveness = Toast.Assertiveness.POLITE,
92+
assertiveness = aria.LiveRegionAssertiveness.POLITE,
9393
} = options;
9494

9595
const toast = document.createElement('div');
9696
workspace.getInjectionDiv().appendChild(toast);
9797
toast.dataset.toastId = options.id;
9898
toast.className = CLASS_NAME;
99-
aria.setRole(toast, aria.Role.STATUS);
100-
aria.setState(toast, aria.State.LIVE, assertiveness);
10199

102100
const messageElement = toast.appendChild(document.createElement('div'));
103101
messageElement.className = MESSAGE_CLASS_NAME;
@@ -157,6 +155,11 @@ export class Toast {
157155
toast.addEventListener('mouseleave', setToastTimeout);
158156
setToastTimeout();
159157

158+
aria.announceDynamicAriaState(message, {
159+
assertiveness,
160+
role: aria.Role.STATUS,
161+
});
162+
160163
return toast;
161164
}
162165

@@ -174,17 +177,6 @@ export class Toast {
174177
}
175178
}
176179

177-
/**
178-
* Options for how aggressively toasts should be read out by screenreaders.
179-
* Values correspond to those for aria-live.
180-
*/
181-
export namespace Toast {
182-
export enum Assertiveness {
183-
ASSERTIVE = 'assertive',
184-
POLITE = 'polite',
185-
}
186-
}
187-
188180
Css.register(`
189181
.${CLASS_NAME} {
190182
font-size: 1.2rem;

packages/blockly/core/utils/aria.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ export function removeRole(element: Element) {
188188
*/
189189
export function setRole(element: Element, roleName: Role | null) {
190190
if (!roleName) {
191-
console.log('Removing role from element', element, roleName);
192191
removeRole(element);
193192
} else {
194193
element.setAttribute(ROLE_ATTRIBUTE, roleName);

packages/blockly/msg/json/en.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{
22
"@metadata": {
33
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
4+
<<<<<<< lights-camera-action
45
"lastupdated": "2026-03-18 15:02:18.076379",
6+
=======
7+
"lastupdated": "2026-04-03 10:36:19.846436",
8+
>>>>>>> v13
59
"locale": "en",
610
"messagedocumentation" : "qqq"
711
},
@@ -421,6 +425,14 @@
421425
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Use the arrow keys to move, then %1 to accept the position",
422426
"KEYBOARD_NAV_COPIED_HINT": "Copied. Press %1 to paste.",
423427
"KEYBOARD_NAV_CUT_HINT": "Cut. Press %1 to paste.",
428+
<<<<<<< lights-camera-action
424429
"KEYBOARD_NAV_BLOCK_NAVIGATION_HINT": "Use the right arrow key to navigate inside of blocks",
425430
"KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT": "Use the arrow keys to navigate"
431+
=======
432+
"WORKSPACE_CONTENTS_BLOCKS_MANY": "%1 stacks of blocks%2 in workspace.",
433+
"WORKSPACE_CONTENTS_BLOCKS_ONE": "One stack of blocks%2 in workspace.",
434+
"WORKSPACE_CONTENTS_BLOCKS_ZERO": "No blocks%2 in workspace.",
435+
"WORKSPACE_CONTENTS_COMMENTS_MANY": " and %1 comments",
436+
"WORKSPACE_CONTENTS_COMMENTS_ONE": " and one comment"
437+
>>>>>>> v13
426438
}

packages/blockly/msg/json/qqq.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,14 @@
428428
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks with the keyboard.",
429429
"KEYBOARD_NAV_COPIED_HINT": "Message shown when an item is copied in keyboard navigation mode.",
430430
"KEYBOARD_NAV_CUT_HINT": "Message shown when an item is cut in keyboard navigation mode.",
431+
<<<<<<< lights-camera-action
431432
"KEYBOARD_NAV_BLOCK_NAVIGATION_HINT": "Message shown when a user presses Enter with a navigable block focused.",
432433
"KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT": "Message shown when a user presses Enter with the workspace focused."
434+
=======
435+
"WORKSPACE_CONTENTS_BLOCKS_MANY": "ARIA live region message announcing the number of stacks of blocks in the workspace, optionally including comments. \n\nParameters:\n* %1 - the number of stacks (integer greater than 1)\n* %2 - optional phrase announcing comments, including leading space \n\nExamples:\n* '5 stacks of blocks in workspace.'\n* '5 stacks of blocks and 2 comments in workspace.'",
436+
"WORKSPACE_CONTENTS_BLOCKS_ONE": "ARIA live region message announcing there is one stack of blocks in the workspace, optionally including a count of comments. \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space \n\nExamples:\n* 'One stack of blocks in workspace.'\n* 'One stack of blocks and 1 comment in workspace.'",
437+
"WORKSPACE_CONTENTS_BLOCKS_ZERO": "ARIA live region message announcing there are no blocks in the workspace, optionally including a count of comments. \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space \n\nExamples:\n* 'No blocks in workspace.'\n* 'No blocks and 3 comments in workspace.'",
438+
"WORKSPACE_CONTENTS_COMMENTS_MANY": "ARIA live region phrase appended when there are multiple workspace comments. \n\nParameters:\n* %1 - the number of comments (integer greater than 1)",
439+
"WORKSPACE_CONTENTS_COMMENTS_ONE": "ARIA live region phrase appended when there is exactly one workspace comment."
440+
>>>>>>> v13
433441
}

packages/blockly/msg/messages.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1701,4 +1701,26 @@ Blockly.Msg.KEYBOARD_NAV_CUT_HINT = 'Cut. Press %1 to paste.';
17011701
Blockly.Msg.KEYBOARD_NAV_BLOCK_NAVIGATION_HINT = 'Use the right arrow key to navigate inside of blocks';
17021702
/** @type {string} */
17031703
/// Message shown when a user presses Enter with the workspace focused.
1704-
Blockly.Msg.KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT = 'Use the arrow keys to navigate';
1704+
Blockly.Msg.KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT = 'Use the arrow keys to navigate';
1705+
/** @type {string} */
1706+
/// ARIA live region message announcing the number of stacks of blocks in the workspace, optionally including comments.
1707+
/// \n\nParameters:\n* %1 - the number of stacks (integer greater than 1)\n* %2 - optional phrase announcing comments, including leading space
1708+
/// \n\nExamples:\n* "5 stacks of blocks in workspace."\n* "5 stacks of blocks and 2 comments in workspace."
1709+
Blockly.Msg.WORKSPACE_CONTENTS_BLOCKS_MANY = '%1 stacks of blocks%2 in workspace.';
1710+
/** @type {string} */
1711+
/// ARIA live region message announcing there is one stack of blocks in the workspace, optionally including a count of comments.
1712+
/// \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space
1713+
/// \n\nExamples:\n* "One stack of blocks in workspace."\n* "One stack of blocks and 1 comment in workspace."
1714+
Blockly.Msg.WORKSPACE_CONTENTS_BLOCKS_ONE = 'One stack of blocks%2 in workspace.';
1715+
/** @type {string} */
1716+
/// ARIA live region message announcing there are no blocks in the workspace, optionally including a count of comments.
1717+
/// \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space
1718+
/// \n\nExamples:\n* "No blocks in workspace."\n* "No blocks and 3 comments in workspace."
1719+
Blockly.Msg.WORKSPACE_CONTENTS_BLOCKS_ZERO = 'No blocks%2 in workspace.';
1720+
/** @type {string} */
1721+
/// ARIA live region phrase appended when there are multiple workspace comments.
1722+
/// \n\nParameters:\n* %1 - the number of comments (integer greater than 1)
1723+
Blockly.Msg.WORKSPACE_CONTENTS_COMMENTS_MANY = ' and %1 comments';
1724+
/** @type {string} */
1725+
/// ARIA live region phrase appended when there is exactly one workspace comment.
1726+
Blockly.Msg.WORKSPACE_CONTENTS_COMMENTS_ONE = ' and one comment';

packages/blockly/tests/mocha/shortcut_items_test.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,81 @@ suite('Keyboard Shortcut Items', function () {
552552
});
553553
});
554554

555+
suite('Workspace Information (I)', function () {
556+
setup(function () {
557+
const keyEvent = createKeyDownEvent(Blockly.utils.KeyCodes.I);
558+
// Helper to trigger the shortcut and assert the live region text.
559+
this.assertAnnouncement = (expected) => {
560+
this.injectionDiv.dispatchEvent(keyEvent);
561+
// Wait for the live region to update after the event.
562+
this.clock.tick(11);
563+
// The announcement may include an additional non-breaking space.
564+
assert.include(this.liveRegion.textContent, expected);
565+
};
566+
this.liveRegion = document.getElementById('blocklyAriaAnnounce');
567+
});
568+
569+
test('Empty workspace', function () {
570+
// Start with empty workspace.
571+
Blockly.getFocusManager().focusNode(this.workspace);
572+
this.assertAnnouncement('No blocks in workspace.');
573+
});
574+
575+
test('One block', function () {
576+
this.workspace.newBlock('stack_block');
577+
Blockly.getFocusManager().focusNode(this.workspace);
578+
this.assertAnnouncement('One stack of blocks in workspace.');
579+
});
580+
581+
test('Two blocks', function () {
582+
this.workspace.newBlock('stack_block');
583+
this.workspace.newBlock('stack_block');
584+
Blockly.getFocusManager().focusNode(this.workspace);
585+
this.assertAnnouncement('2 stacks of blocks in workspace.');
586+
});
587+
588+
test('One comment', function () {
589+
this.workspace.newComment();
590+
Blockly.getFocusManager().focusNode(this.workspace);
591+
this.assertAnnouncement('No blocks and one comment in workspace.');
592+
});
593+
594+
test('Two comments', function () {
595+
this.workspace.newComment();
596+
this.workspace.newComment();
597+
Blockly.getFocusManager().focusNode(this.workspace);
598+
this.assertAnnouncement('No blocks and 2 comments in workspace.');
599+
});
600+
601+
test('One block, one comment', function () {
602+
this.workspace.newBlock('stack_block');
603+
this.workspace.newComment();
604+
Blockly.getFocusManager().focusNode(this.workspace);
605+
this.assertAnnouncement(
606+
'One stack of blocks and one comment in workspace.',
607+
);
608+
});
609+
610+
test('Two blocks, two comments', function () {
611+
this.workspace.newBlock('stack_block');
612+
this.workspace.newBlock('stack_block');
613+
this.workspace.newComment();
614+
this.workspace.newComment();
615+
Blockly.getFocusManager().focusNode(this.workspace);
616+
this.assertAnnouncement(
617+
'2 stacks of blocks and 2 comments in workspace.',
618+
);
619+
});
620+
621+
suite('Preconditions', function () {
622+
test('Not called when focus is not on workspace', function () {
623+
this.block = this.workspace.newBlock('stack_block');
624+
Blockly.getFocusManager().focusNode(this.block);
625+
this.assertAnnouncement('');
626+
});
627+
});
628+
});
629+
555630
suite('Focus Toolbox (T)', function () {
556631
test('Does not change focus when toolbox item is already focused', function () {
557632
const item = this.workspace.getToolbox().getToolboxItems()[1];

0 commit comments

Comments
 (0)