Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
0f6454b
release: Blockly v12.1.0 merge master into develop
RoboErikG May 30, 2025
b5dbe6a
chore(deps): bump @blockly/dev-tools from 9.0.0 to 9.0.1 (#9124)
dependabot[bot] Jun 2, 2025
2f7ece8
chore(deps-dev): bump tar-fs
dependabot[bot] Jun 3, 2025
74dbed2
Merge pull request #9127 from google/dependabot/npm_and_yarn/npm_and_…
RoboErikG Jun 6, 2025
1008569
chore(deps): bump eslint-plugin-jsdoc from 50.6.9 to 50.7.1
dependabot[bot] Jun 9, 2025
02f89d6
Merge pull request #9132 from google/dependabot/npm_and_yarn/develop/…
RoboErikG Jun 9, 2025
9685498
Add isCopyable and isCuttable as optional methods on ICopyable
RoboErikG Jun 9, 2025
4607836
Fix build errors
RoboErikG Jun 9, 2025
e1441d5
Remove isCuttable api
RoboErikG Jun 10, 2025
1d4e531
Don't allow things in a flyout to be deleted or moved.
RoboErikG Jun 10, 2025
428e447
Simplify cut/copy logic
RoboErikG Jun 10, 2025
f1b44db
Add missing bang
RoboErikG Jun 10, 2025
32bb84e
Allow copying from readonly workspace and add cut tests
RoboErikG Jun 13, 2025
fd5a7f4
refactor: Make the cursor use the focus manager for tracking the curr…
gonfunko Jun 13, 2025
a888362
Add tests for workspace comments
RoboErikG Jun 13, 2025
93a9b6b
fix: Fix navigation for blocks with multiple statement inputs. (#9143)
gonfunko Jun 13, 2025
3e09a70
chore(deps): bump @hyperjump/json-schema from 1.11.0 to 1.15.1 (#9147)
dependabot[bot] Jun 16, 2025
f117bba
Simplify check for existence of isCopyable
RoboErikG Jun 16, 2025
2bae8eb
Update isCopyable comment
RoboErikG Jun 16, 2025
7df501d
fix: Add isCopyable to the ICopyable interface and use it for cut/cop…
RoboErikG Jun 16, 2025
afe53c5
fix: Dispatch keyboard events with the workspace they occurred on. (#…
gonfunko Jun 16, 2025
cf3fccc
fix: caret position when editing block comments (#9153)
maribethb Jun 18, 2025
97ffea7
chore(deps): bump @hyperjump/browser from 1.1.6 to 1.3.1 (#9148)
dependabot[bot] Jun 18, 2025
acdb27e
chore(deps): bump globals from 16.1.0 to 16.2.0
dependabot[bot] Jun 23, 2025
1e5b4e9
feat: Add support for keyboard navigation into mutator workspaces. (#…
gonfunko Jun 23, 2025
253ea15
chore(deps): bump eslint-plugin-prettier from 5.4.0 to 5.5.0 (#9157)
dependabot[bot] Jun 23, 2025
21216e8
chore(deps): bump prettier from 3.3.3 to 3.6.0
dependabot[bot] Jun 23, 2025
28d6ff7
chore: Update messages for keyboard-experiment. (#9152)
gonfunko Jun 23, 2025
ba90efe
Merge pull request #9160 from google/dependabot/npm_and_yarn/develop/…
RoboErikG Jun 23, 2025
4977b4b
Merge pull request #9158 from google/dependabot/npm_and_yarn/develop/…
RoboErikG Jun 23, 2025
af4a4b4
feat: Run keyboard plugin tests in CI (#9135)
BenHenning Jun 23, 2025
5427c3d
chore(deps): bump mocha from 11.3.0 to 11.7.0 (#9159)
dependabot[bot] Jun 23, 2025
eaf5eea
feat: make comment editor separately focusable from comment itself (#…
maribethb Jun 24, 2025
f4dbea0
refactor(interfaces): Make type predicates more robust (#9150)
cpcallen Jun 25, 2025
9cc3e11
fix: tweak redo shortcut order to match convention (#9169)
microbit-matt-hillsdon Jun 26, 2025
0d6da6c
fix: clear touch identifier on comment text area pointerdown (#9172)
riknoll Jun 26, 2025
e94b350
release: merge develop into rc/v12.2.0
maribethb Jun 26, 2025
8015956
release: Update version number to 12.2.0
maribethb Jun 26, 2025
9b18a9b
Work on fixing more browser tests
RoboErikG Jun 23, 2025
51bfadb
Remove .only
RoboErikG Jun 27, 2025
77543d3
Fix tests for opening categories
RoboErikG Jun 27, 2025
3d6ac54
Fix procedure tests
RoboErikG Jun 27, 2025
ce3e251
Disable test to drag all blocks out and fix comment resize test
RoboErikG Jun 27, 2025
53b6362
chore(deps): bump eslint from 9.26.0 to 9.30.0 (#9186)
dependabot[bot] Jun 30, 2025
6a04d0e
chore(deps): bump eslint-plugin-jsdoc from 50.7.1 to 51.3.1 (#9191)
dependabot[bot] Jun 30, 2025
9424deb
build: Refactor gulpfiles from CJS to ESM (#9149)
cpcallen Jun 30, 2025
fa93ba2
chore(deps): bump glob from 11.0.2 to 11.0.3 (#9189)
dependabot[bot] Jun 30, 2025
460c8c8
chore(deps): bump @blockly/block-test from 6.0.11 to 7.0.1 (#9192)
dependabot[bot] Jun 30, 2025
fd3a756
fix: Fix loss of focus when un/redoing block deletions or moves. (#9195)
gonfunko Jul 1, 2025
0f73bd5
chore(deps): bump mocha from 11.7.0 to 11.7.1 (#9193)
dependabot[bot] Jul 1, 2025
19da66c
chore(deps): bump gulp from 5.0.0 to 5.0.1 (#9188)
dependabot[bot] Jul 1, 2025
c426c6d
fix: Short-circuit node lookups for missing IDs (#9174)
BenHenning Jul 1, 2025
e5804e7
feat: Add support for keyboard navigation in/to workspace comments. (…
gonfunko Jul 1, 2025
5acd072
chore(deps): bump prettier from 3.6.0 to 3.6.2 (#9185)
dependabot[bot] Jul 2, 2025
1e37d21
fix: Ensure focus changes when tabbing fields (#9173)
BenHenning Jul 2, 2025
4c78c1d
fix: Auto close drop-down divs on lost focus (#9175)
BenHenning Jul 2, 2025
7ad18f7
Revert "fix: Auto close drop-down divs on lost focus (#9175)" (#9204)
cpcallen Jul 7, 2025
efb5a2e
fix: check for a drag specifically rather than a gesture for shortcut…
maribethb Jul 7, 2025
b741d78
refactor(CSS): move box-sizing to core/css.ts (#9201)
cpcallen Jul 7, 2025
7184cb2
chore(deps): bump eslint-config-prettier from 10.1.1 to 10.1.5 (#9209)
dependabot[bot] Jul 7, 2025
9828cfa
Merge branch 'google:develop' into fix-browser-tests-2025-06
RoboErikG Jul 7, 2025
b890e32
Re-enable undo/redo tests now that focus is working
RoboErikG Jul 7, 2025
97d0e45
chore(deps): bump eslint-plugin-prettier from 5.5.0 to 5.5.1 (#9206)
dependabot[bot] Jul 7, 2025
dfd5659
refactor: Ensure that the workspace cursor is never null. (#9210)
gonfunko Jul 7, 2025
e3d17be
fix: Improve workspace comment keyboard navigation behavior. (#9211)
gonfunko Jul 7, 2025
0e16b04
fix: Auto close drop-down divs on lost focus (reapply) (#9213)
BenHenning Jul 7, 2025
dfcdcc1
chore(deps): bump @microsoft/api-extractor from 7.48.1 to 7.52.8 (#9208)
dependabot[bot] Jul 7, 2025
8580d76
chore(deps): bump google-closure-compiler from 20240317.0.0 to 202506…
dependabot[bot] Jul 8, 2025
fc9164d
fix: Prevent loss of focus when deleting a workspace comment. (#9200)
gonfunko Jul 8, 2025
274891d
Responses to comments
RoboErikG Jul 8, 2025
1e40641
Fix formatting
RoboErikG Jul 8, 2025
2fba036
Add a todo for enabling the toolbox categories tests
RoboErikG Jul 8, 2025
89af298
Merge pull request #9183 from RoboErikG/fix-browser-tests-2025-06
RoboErikG Jul 8, 2025
c0489b4
feat: add copy api and paste into correct workspace (#9215)
maribethb Jul 8, 2025
bea183d
fix: Auto-close widget divs on lost focus (#9216)
BenHenning Jul 8, 2025
5747fee
fix: Revert drop down and widget div PRs (#9222)
BenHenning Jul 9, 2025
fae8b7f
release: merge develop into rv/v12.2.0
maribethb Jul 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/keyboard_plugin_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Workflow for running the keyboard navigation plugin's automated tests.

name: Keyboard Navigation Automated Tests

on:
workflow_dispatch:
pull_request:
push:
branches:
- develop

permissions:
contents: read

jobs:
webdriverio_tests:
name: WebdriverIO tests
timeout-minutes: 10
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]

steps:
- name: Checkout core Blockly
uses: actions/checkout@v4
with:
path: core-blockly

- name: Checkout keyboard navigation plugin
uses: actions/checkout@v4
with:
repository: 'google/blockly-keyboard-experimentation'
ref: 'main'
path: blockly-keyboard-experimentation

- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x

- name: NPM install
run: |
cd core-blockly
npm install
cd ..
cd blockly-keyboard-experimentation
npm install
cd ..

- name: Link latest core develop with plugin
run: |
cd core-blockly
npm run package
cd dist
npm link
cd ../../blockly-keyboard-experimentation
npm link blockly
cd ..

- name: Run keyboard navigation plugin tests
run: |
cd blockly-keyboard-experimentation
npm run test
2 changes: 2 additions & 0 deletions core/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ export class Block {
isDeletable(): boolean {
return (
this.deletable &&
!this.isInFlyout &&
!this.shadow &&
!this.isDeadOrDying() &&
!this.workspace.isReadOnly()
Expand Down Expand Up @@ -824,6 +825,7 @@ export class Block {
isMovable(): boolean {
return (
this.movable &&
!this.isInFlyout &&
!this.shadow &&
!this.isDeadOrDying() &&
!this.workspace.isReadOnly()
Expand Down
49 changes: 46 additions & 3 deletions core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,19 @@ export class BlockSvg
}

const oldXY = this.getRelativeToSurfaceXY();
const focusedNode = getFocusManager().getFocusedNode();
const restoreFocus = this.getSvgRoot().contains(
focusedNode?.getFocusableElement() ?? null,
);
if (newParent) {
(newParent as BlockSvg).getSvgRoot().appendChild(svgRoot);
// appendChild() clears focus state, so re-focus the previously focused
// node in case it was this block and would otherwise lose its focus. Once
// Element.moveBefore() has better browser support, it should be used
// instead.
if (restoreFocus && focusedNode) {
getFocusManager().focusNode(focusedNode);
}
} else if (oldParent) {
// If we are losing a parent, we want to move our DOM element to the
// root of the workspace. Try to insert it before any top-level
Expand All @@ -319,6 +330,13 @@ export class BlockSvg
canvas.insertBefore(svgRoot, draggingBlockElement);
} else {
canvas.appendChild(svgRoot);
// appendChild() clears focus state, so re-focus the previously focused
// node in case it was this block and would otherwise lose its focus. Once
// Element.moveBefore() has better browser support, it should be used
// instead.
if (restoreFocus && focusedNode) {
getFocusManager().focusNode(focusedNode);
}
}
this.translate(oldXY.x, oldXY.y);
}
Expand Down Expand Up @@ -849,10 +867,30 @@ export class BlockSvg
Tooltip.dispose();
ContextMenu.hide();

// If this block was focused, focus its parent or workspace instead.
// If this block (or a descendant) was focused, focus its parent or
// workspace instead.
const focusManager = getFocusManager();
if (focusManager.getFocusedNode() === this) {
const parent = this.getParent();
if (
this.getSvgRoot().contains(
focusManager.getFocusedNode()?.getFocusableElement() ?? null,
)
) {
let parent: BlockSvg | undefined | null = this.getParent();
if (!parent) {
// In some cases, blocks are disconnected from their parents before
// being deleted. Attempt to infer if there was a parent by checking
// for a connection within a radius of 0. Even if this wasn't a parent,
// it must be adjacent to this block and so is as good an option as any
// to focus after deleting.
const connection = this.outputConnection ?? this.previousConnection;
if (connection) {
const targetConnection = connection.closest(
0,
new Coordinate(0, 0),
).connection;
parent = targetConnection?.getSourceBlock();
}
}
if (parent) {
focusManager.focusNode(parent);
} else {
Expand Down Expand Up @@ -1721,6 +1759,11 @@ export class BlockSvg
this.dragStrategy = dragStrategy;
}

/** Returns whether this block is copyable or not. */
isCopyable(): boolean {
return this.isOwnDeletable() && this.isOwnMovable();
}

/** Returns whether this block is movable or not. */
override isMovable(): boolean {
return this.dragStrategy.isMovable();
Expand Down
16 changes: 14 additions & 2 deletions core/bubbles/mini_workspace_bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ export class MiniWorkspaceBubble extends Bubble {
* are dealt with by resizing the workspace to show them.
*/
private bumpBlocksIntoBounds() {
if (this.miniWorkspace.isDragging()) return;
if (
this.miniWorkspace.isDragging() &&
!this.miniWorkspace.keyboardMoveInProgress
)
return;

const MARGIN = 20;

Expand Down Expand Up @@ -185,7 +189,15 @@ export class MiniWorkspaceBubble extends Bubble {
* mini workspace.
*/
private updateBubbleSize() {
if (this.miniWorkspace.isDragging()) return;
if (
this.miniWorkspace.isDragging() &&
!this.miniWorkspace.keyboardMoveInProgress
)
return;

// Disable autolayout if a keyboard move is in progress to prevent the
// mutator bubble from jumping around.
this.autoLayout &&= !this.miniWorkspace.keyboardMoveInProgress;

const currSize = this.getSize();
const newSize = this.calculateWorkspaceSize();
Expand Down
5 changes: 5 additions & 0 deletions core/bubbles/textinput_bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ export class TextInputBubble extends Bubble {
browserEvents.conditionalBind(textArea, 'wheel', this, (e: Event) => {
e.stopPropagation();
});
// Don't let the pointerdown event get to the workspace.
browserEvents.conditionalBind(textArea, 'pointerdown', this, (e: Event) => {
e.stopPropagation();
touch.clearTouchIdentifier();
});

browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange);
}
Expand Down
138 changes: 110 additions & 28 deletions core/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js';
import * as registry from './clipboard/registry.js';
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
import {isSelectable} from './interfaces/i_selectable.js';
import * as globalRegistry from './registry.js';
import {Coordinate} from './utils/coordinate.js';
import {WorkspaceSvg} from './workspace_svg.js';
Expand All @@ -18,18 +19,119 @@ let stashedCopyData: ICopyData | null = null;

let stashedWorkspace: WorkspaceSvg | null = null;

let stashedCoordinates: Coordinate | undefined = undefined;

/**
* Private version of copy for stubbing in tests.
* Copy a copyable item, and record its data and the workspace it was
* copied from.
*
* This function does not perform any checks to ensure the copy
* should be allowed, e.g. to ensure the block is deletable. Such
* checks should be done before calling this function.
*
* Note that if the copyable item is not an `ISelectable` or its
* `workspace` property is not a `WorkspaceSvg`, the copy will be
* successful, but there will be no saved workspace data. This will
* impact the ability to paste the data unless you explictily pass
* a workspace into the paste method.
*
* @param toCopy item to copy.
* @param location location to save as a potential paste location.
* @returns the copied data if copy was successful, otherwise null.
*/
function copyInternal<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
export function copy<T extends ICopyData>(
toCopy: ICopyable<T>,
location?: Coordinate,
): T | null {
const data = toCopy.toCopyData();
stashedCopyData = data;
stashedWorkspace = (toCopy as any).workspace ?? null;
if (isSelectable(toCopy) && toCopy.workspace instanceof WorkspaceSvg) {
stashedWorkspace = toCopy.workspace;
} else {
stashedWorkspace = null;
}

stashedCoordinates = location;
return data;
}

/**
* Paste a pasteable element into the workspace.
* Gets the copy data for the last item copied. This is useful if you
* are implementing custom copy/paste behavior. If you want the default
* behavior, just use the copy and paste methods directly.
*
* @returns copy data for the last item copied, or null if none set.
*/
export function getLastCopiedData() {
return stashedCopyData;
}

/**
* Sets the last copied item. You should call this method if you implement
* custom copy behavior, so that other callers are working with the correct
* data. This method is called automatically if you use the built-in copy
* method.
*
* @param copyData copy data for the last item copied.
*/
export function setLastCopiedData(copyData: ICopyData) {
stashedCopyData = copyData;
}

/**
* Gets the workspace that was last copied from. This is useful if you
* are implementing custom copy/paste behavior and want to paste on the
* same workspace that was copied from. If you want the default behavior,
* just use the copy and paste methods directly.
*
* @returns workspace that was last copied from, or null if none set.
*/
export function getLastCopiedWorkspace() {
return stashedWorkspace;
}

/**
* Sets the workspace that was last copied from. You should call this method
* if you implement custom copy behavior, so that other callers are working
* with the correct data. This method is called automatically if you use the
* built-in copy method.
*
* @param workspace workspace that was last copied from.
*/
export function setLastCopiedWorkspace(workspace: WorkspaceSvg) {
stashedWorkspace = workspace;
}

/**
* Gets the location that was last copied from. This is useful if you
* are implementing custom copy/paste behavior. If you want the
* default behavior, just use the copy and paste methods directly.
*
* @returns last saved location, or null if none set.
*/
export function getLastCopiedLocation() {
return stashedCoordinates;
}

/**
* Sets the location that was last copied from. You should call this method
* if you implement custom copy behavior, so that other callers are working
* with the correct data. This method is called automatically if you use the
* built-in copy method.
*
* @param location last saved location, which can be used to paste at.
*/
export function setLastCopiedLocation(location: Coordinate) {
stashedCoordinates = location;
}

/**
* Paste a pasteable element into the given workspace.
*
* This function does not perform any checks to ensure the paste
* is allowed, e.g. that the workspace is rendered or the block
* is pasteable. Such checks should be done before calling this
* function.
*
* @param copyData The data to paste into the workspace.
* @param workspace The workspace to paste the data into.
Expand All @@ -43,7 +145,7 @@ export function paste<T extends ICopyData>(
): ICopyable<T> | null;

/**
* Pastes the last copied ICopyable into the workspace.
* Pastes the last copied ICopyable into the last copied-from workspace.
*
* @returns the pasted thing if the paste was successful, null otherwise.
*/
Expand All @@ -65,7 +167,7 @@ export function paste<T extends ICopyData>(
): ICopyable<ICopyData> | null {
if (!copyData || !workspace) {
if (!stashedCopyData || !stashedWorkspace) return null;
return pasteFromData(stashedCopyData, stashedWorkspace);
return pasteFromData(stashedCopyData, stashedWorkspace, stashedCoordinates);
}
return pasteFromData(copyData, workspace, coordinate);
}
Expand All @@ -85,31 +187,11 @@ function pasteFromData<T extends ICopyData>(
): ICopyable<T> | null {
workspace = workspace.isMutator
? workspace
: (workspace.getRootWorkspace() ?? workspace);
: // Use the parent workspace if it exists (e.g. for pasting into flyouts)
(workspace.options.parentWorkspace ?? workspace);
return (globalRegistry
.getObject(globalRegistry.Type.PASTER, copyData.paster, false)
?.paste(copyData, workspace, coordinate) ?? null) as ICopyable<T> | null;
}

/**
* Private version of duplicate for stubbing in tests.
*/
function duplicateInternal<
U extends ICopyData,
T extends ICopyable<U> & IHasWorkspace,
>(toDuplicate: T): T | null {
const data = toDuplicate.toCopyData();
if (!data) return null;
return paste(data, toDuplicate.workspace) as T;
}

interface IHasWorkspace {
workspace: WorkspaceSvg;
}

export const TEST_ONLY = {
duplicateInternal,
copyInternal,
};

export {BlockCopyData, BlockPaster, registry};
4 changes: 4 additions & 0 deletions core/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/

export {CollapseCommentBarButton} from './comments/collapse_comment_bar_button.js';
export {CommentBarButton} from './comments/comment_bar_button.js';
export {CommentEditor} from './comments/comment_editor.js';
export {CommentView} from './comments/comment_view.js';
export {DeleteCommentBarButton} from './comments/delete_comment_bar_button.js';
export {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
export {WorkspaceComment} from './comments/workspace_comment.js';
Loading