Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a53db40
release: Merge branch 'develop' into rc/v12.3.0
gonfunko Aug 18, 2025
86da7dc
release: Update version number to 12.3.0-beta.0
gonfunko Aug 21, 2025
84933b9
chore: lint error on only in mocha tests (#9300)
maribethb Aug 19, 2025
29d5b43
chore(deps): bump actions/checkout from 4 to 5 (#9320)
dependabot[bot] Aug 19, 2025
10b1d1e
fix: Fix positioning of pasted blocks and comments in RTL. (#9302)
gonfunko Aug 19, 2025
9e1db9e
chore: Fix documentation generation warnings. (#9325)
gonfunko Aug 19, 2025
4a0b710
fix: Show the delete cursor when dragging a block by an editable fiel…
gonfunko Aug 20, 2025
cf93f07
fix: Correct the alignment of narrow text in input fields. (#9327)
gonfunko Aug 21, 2025
4891659
fix: Fix bug that caused inadvertent scrolling when the `WidgetDiv` w…
gonfunko Aug 21, 2025
c32f6db
chore(deps): bump eslint-plugin-prettier from 5.5.1 to 5.5.4 (#9319)
dependabot[bot] Aug 21, 2025
be5f5a2
fix: pointercancel event is not handled (#9250)
nianxy Aug 21, 2025
e358f4e
chore(deps): bump eslint-config-prettier from 10.1.5 to 10.1.8 (#9321)
dependabot[bot] Aug 22, 2025
cb69892
fix: Allow reregistering fields. (#9290)
gonfunko Aug 22, 2025
aeb3e5e
chore(deps): bump chai from 5.2.1 to 6.0.1 (#9330)
dependabot[bot] Aug 26, 2025
90580a8
chore(deps): bump eslint from 9.30.0 to 9.34.0 (#9329)
dependabot[bot] Aug 27, 2025
f10454c
chore: Add node.js v24 to CI build matrix (#9219)
cpcallen Aug 27, 2025
b0569c4
fix: Prevent mocha tests failures when window does not have focus. (#…
gonfunko Aug 27, 2025
e51efe4
fix: Fix bug that could cause errant line when rendering. (#9333)
gonfunko Aug 28, 2025
5afc0d6
refactor: Make focusable elements responsible for scrolling themselve…
gonfunko Aug 28, 2025
5f21e9b
release: Update version number to 12.3.0
gonfunko Aug 28, 2025
2bd8b63
Merge pull request #9335 from google/rc/v12.3.0
gonfunko Aug 28, 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
2 changes: 1 addition & 1 deletion .github/workflows/appengine_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
steps:
# Checks-out the repository under $GITHUB_WORKSPACE.
# When running manually this checks out the master branch.
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Prepare demo files
# Install all dependencies, then copy all the files needed for demos.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/browser_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
# https://nodejs.org/en/about/releases/

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ jobs:
# TODO (#2114): re-enable osx build.
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
node-version: [18.x, 20.x, 22.x]
node-version: [18.x, 20.x, 22.x, 24.x]
# See supported Node.js release schedule at
# https://nodejs.org/en/about/releases/

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

Expand Down Expand Up @@ -54,7 +54,7 @@ jobs:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Use Node.js 20.x
uses: actions/setup-node@v4
Expand All @@ -71,7 +71,7 @@ jobs:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Use Node.js 20.x
uses: actions/setup-node@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/keyboard_plugin_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ jobs:

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

- name: Checkout keyboard navigation plugin
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
repository: 'google/blockly-keyboard-experimentation'
ref: 'main'
Expand Down
2 changes: 1 addition & 1 deletion core/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,7 @@ export class Block {
/**
* Returns a generator that provides every field on the block.
*
* @yields A generator that can be used to iterate the fields on the block.
* @returns A generator that can be used to iterate the fields on the block.
*/
*getFields(): Generator<Field, undefined, void> {
for (const input of this.inputList) {
Expand Down
3 changes: 3 additions & 0 deletions core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1844,6 +1844,9 @@ export class BlockSvg
/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {
this.select();
this.workspace.scrollBoundsIntoView(
this.getBoundingRectangleWithoutChildren(),
);
}

/** See IFocusableNode.onNodeBlur. */
Expand Down
4 changes: 4 additions & 0 deletions core/bubbles/bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,10 @@ export abstract class Bubble implements IBubble, ISelectable, IFocusableNode {
onNodeFocus(): void {
this.select();
this.bringToFront();
const xy = this.getRelativeToSurfaceXY();
const size = this.getSize();
const bounds = new Rect(xy.y, xy.y + size.height, xy.x, xy.x + size.width);
this.workspace.scrollBoundsIntoView(bounds);
}

/** See IFocusableNode.onNodeBlur. */
Expand Down
3 changes: 3 additions & 0 deletions core/clipboard/block_paster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ export function moveBlockToNotConflict(
block: BlockSvg,
originalPosition: Coordinate,
) {
if (block.workspace.RTL) {
originalPosition.x = block.workspace.getWidth() - originalPosition.x;
}
const workspace = block.workspace;
const snapRadius = config.snapRadius;
const bumpOffset = Coordinate.difference(
Expand Down
8 changes: 7 additions & 1 deletion core/comments/comment_bar_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,13 @@ export abstract class CommentBarButton implements IFocusableNode {
}

/** Called when this button's focusable DOM element gains focus. */
onNodeFocus() {}
onNodeFocus() {
const commentView = this.getCommentView();
const xy = commentView.getRelativeToSurfaceXY();
const size = commentView.getSize();
const bounds = new Rect(xy.y, xy.y + size.height, xy.x, xy.x + size.width);
commentView.workspace.scrollBoundsIntoView(bounds);
}

/** Called when this button's focusable DOM element loses focus. */
onNodeBlur() {}
Expand Down
13 changes: 12 additions & 1 deletion core/comments/comment_editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {IFocusableNode} from '../interfaces/i_focusable_node.js';
import {IFocusableTree} from '../interfaces/i_focusable_tree.js';
import * as touch from '../touch.js';
import * as dom from '../utils/dom.js';
import {Rect} from '../utils/rect.js';
import {Size} from '../utils/size.js';
import {Svg} from '../utils/svg.js';
import * as svgMath from '../utils/svg_math.js';
import {WorkspaceSvg} from '../workspace_svg.js';

/**
Expand Down Expand Up @@ -188,7 +190,16 @@ export class CommentEditor implements IFocusableNode {
getFocusableTree(): IFocusableTree {
return this.workspace;
}
onNodeFocus(): void {}
onNodeFocus(): void {
const bbox = Rect.from(this.foreignObject.getBoundingClientRect());
this.workspace.scrollBoundsIntoView(
Rect.createFromPoint(
svgMath.screenToWsCoordinates(this.workspace, bbox.getOrigin()),
bbox.getWidth(),
bbox.getHeight(),
),
);
}
onNodeBlur(): void {}
canBeFocused(): boolean {
if (this.id) return true;
Expand Down
5 changes: 4 additions & 1 deletion core/comments/comment_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,10 @@ export class CommentView implements IRenderedElement {

const textPreviewWidth =
size.width - foldoutSize.getWidth() - deleteSize.getWidth();
this.textPreview.setAttribute('x', `${foldoutSize.getWidth()}`);
this.textPreview.setAttribute(
'x',
`${(this.workspace.RTL ? -1 : 1) * foldoutSize.getWidth()}`,
);
this.textPreview.setAttribute(
'y',
`${textPreviewMargin + textPreviewSize.height / 2}`,
Expand Down
1 change: 1 addition & 0 deletions core/comments/rendered_workspace_comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ export class RenderedWorkspaceComment
this.select();
// Ensure that the comment is always at the top when focused.
this.workspace.getLayerManager()?.append(this, layers.BLOCK);
this.workspace.scrollBoundsIntoView(this.getBoundingRectangle());
}

/** See IFocusableNode.onNodeBlur. */
Expand Down
3 changes: 2 additions & 1 deletion core/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ let content = `
cursor: -webkit-grabbing;
}

.blocklyDragging.blocklyDraggingDelete {
.blocklyDragging.blocklyDraggingDelete,
.blocklyDragging.blocklyDraggingDelete .blocklyField {
cursor: url("<<<PATH>>>/handdelete.cur"), auto;
}

Expand Down
13 changes: 7 additions & 6 deletions core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,6 @@ export abstract class Field<T = any>
return this.size;
}

/**
* Sets the size of this field.
*/
protected set size_(newValue: Size) {
this.size = newValue;
}
Expand Down Expand Up @@ -852,8 +849,7 @@ export abstract class Field<T = any>
totalHeight = Math.max(totalHeight, constants!.FIELD_BORDER_RECT_HEIGHT);
}

this.size_.height = totalHeight;
this.size_.width = totalWidth;
this.size_ = new Size(totalWidth, totalHeight);

this.positionTextElement_(xOffset, contentWidth);
this.positionBorderRect_();
Expand Down Expand Up @@ -1384,7 +1380,12 @@ export abstract class Field<T = any>
}

/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {}
onNodeFocus(): void {
const block = this.getSourceBlock() as BlockSvg;
block.workspace.scrollBoundsIntoView(
block.getBoundingRectangleWithoutChildren(),
);
}

/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {}
Expand Down
7 changes: 3 additions & 4 deletions core/field_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import {Size} from './utils/size.js';
import * as utilsString from './utils/string.js';
import {Svg} from './utils/svg.js';

Expand Down Expand Up @@ -553,8 +554,7 @@ export class FieldDropdown extends Field<string> {
} else {
arrowWidth = dom.getTextWidth(this.arrow as SVGTSpanElement);
}
this.size_.width = imageWidth + arrowWidth + xPadding * 2;
this.size_.height = height;
this.size_ = new Size(imageWidth + arrowWidth + xPadding * 2, height);

let arrowX = 0;
if (block.RTL) {
Expand Down Expand Up @@ -595,8 +595,7 @@ export class FieldDropdown extends Field<string> {
height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2,
);
}
this.size_.width = textWidth + arrowWidth + xPadding * 2;
this.size_.height = height;
this.size_ = new Size(textWidth + arrowWidth + xPadding * 2, height);

this.positionTextElement_(xPadding, textWidth);
}
Expand Down
32 changes: 26 additions & 6 deletions core/field_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ import type {WorkspaceSvg} from './workspace_svg.js';
*/
type InputTypes = string | number;

/**
* The minimum width of an input field.
*/
const MINIMUM_WIDTH = 14;

/**
* Abstract class for an editable input field.
*
Expand Down Expand Up @@ -102,11 +107,9 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
*/
override SERIALIZABLE = true;

/**
* Sets the size of this field. Although this appears to be a no-op, it must
* exist since the getter is overridden below.
*/
protected override set size_(newValue: Size) {
// Although this appears to be a no-op, it must exist since the getter is
// overridden below.
super.size_ = newValue;
}

Expand All @@ -115,8 +118,8 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
*/
protected override get size_() {
const s = super.size_;
if (s.width < 14) {
s.width = 14;
if (s.width < MINIMUM_WIDTH) {
s.width = MINIMUM_WIDTH;
}

return s;
Expand Down Expand Up @@ -732,6 +735,23 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
return true;
}

/**
* Position a field's text element after a size change. This handles both LTR
* and RTL positioning.
*
* @param xMargin x offset to use when positioning the text element.
* @param contentWidth The content width.
*/
protected override positionTextElement_(
xMargin: number,
contentWidth: number,
) {
const effectiveWidth = xMargin * 2 + contentWidth;
const delta =
effectiveWidth < MINIMUM_WIDTH ? (MINIMUM_WIDTH - effectiveWidth) / 2 : 0;
super.positionTextElement_(xMargin + delta, contentWidth);
}

/**
* Use the `getText_` developer hook to override the field's text
* representation. When we're currently editing, return the current HTML value
Expand Down
6 changes: 3 additions & 3 deletions core/field_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ export interface RegistrableField {
* @param type The field type name as used in the JSON definition.
* @param fieldClass The field class containing a fromJson function that can
* construct an instance of the field.
* @throws {Error} if the type name is empty, the field is already registered,
* or the fieldClass is not an object containing a fromJson function.
* @throws {Error} if the type name is empty or the fieldClass is not an object
* containing a fromJson function.
*/
export function register(type: string, fieldClass: RegistrableField) {
registry.register(registry.Type.FIELD, type, fieldClass);
registry.register(registry.Type.FIELD, type, fieldClass, true);
}

/**
Expand Down
6 changes: 5 additions & 1 deletion core/flyout_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,11 @@ export class FlyoutButton
}

/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {}
onNodeFocus(): void {
const xy = this.getPosition();
const bounds = new Rect(xy.y, xy.y + this.height, xy.x, xy.x + this.width);
this.workspace.scrollBoundsIntoView(bounds);
}

/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {}
Expand Down
8 changes: 6 additions & 2 deletions core/focus_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ export class FocusManager {
* Note that this may update the specified node's element's tabindex to ensure
* that it can be properly read out by screenreaders while focused.
*
* The focused node will not be automatically scrolled into view.
*
* @param focusableNode The node that should receive active focus.
*/
focusNode(focusableNode: IFocusableNode): void {
Expand Down Expand Up @@ -423,6 +425,8 @@ export class FocusManager {
* the returned lambda is called. Additionally, only 1 ephemeral focus context
* can be active at any given time (attempting to activate more than one
* simultaneously will result in an error being thrown).
*
* This method does not scroll the ephemerally focused element into view.
*/
takeEphemeralFocus(
focusableElement: HTMLElement | SVGElement,
Expand All @@ -439,7 +443,7 @@ export class FocusManager {
if (this.focusedNode) {
this.passivelyFocusNode(this.focusedNode, null);
}
focusableElement.focus();
focusableElement.focus({preventScroll: true});

let hasFinishedEphemeralFocus = false;
return () => {
Expand Down Expand Up @@ -574,7 +578,7 @@ export class FocusManager {
}

this.setNodeToVisualActiveFocus(node);
elem.focus();
elem.focus({preventScroll: true});
}

/**
Expand Down
12 changes: 11 additions & 1 deletion core/icons/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as tooltip from '../tooltip.js';
import {Coordinate} from '../utils/coordinate.js';
import * as dom from '../utils/dom.js';
import * as idGenerator from '../utils/idgenerator.js';
import {Rect} from '../utils/rect.js';
import {Size} from '../utils/size.js';
import {Svg} from '../utils/svg.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
Expand Down Expand Up @@ -168,7 +169,16 @@ export abstract class Icon implements IIcon {
}

/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {}
onNodeFocus(): void {
const blockBounds = (this.sourceBlock as BlockSvg).getBoundingRectangle();
const bounds = new Rect(
blockBounds.top + this.offsetInBlock.y,
blockBounds.top + this.offsetInBlock.y + this.getSize().height,
blockBounds.left + this.offsetInBlock.x,
blockBounds.left + this.offsetInBlock.x + this.getSize().width,
);
(this.sourceBlock as BlockSvg).workspace.scrollBoundsIntoView(bounds);
}

/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {}
Expand Down
Loading
Loading