Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a1cbdd9
Merge pull request #9060 from google/master
gonfunko May 15, 2025
3a53af9
fix: Ensure `FieldImage` is clickable when appropriate (#9063)
BenHenning May 16, 2025
64160d1
chore(deps): bump @blockly/dev-tools from 8.0.12 to 9.0.0 (#9065)
dependabot[bot] May 19, 2025
7d0414c
fix: When moving to a field, scroll the field's block into view (#9071)
RoboErikG May 19, 2025
3010cee
fix: Skip hidden fields when navigating (#9070)
RoboErikG May 19, 2025
91632a4
fix: Limit LineCursor<-focus syncing. (#9062)
BenHenning May 19, 2025
8e11337
chore(deps): bump glob from 11.0.1 to 11.0.2 (#9066)
dependabot[bot] May 19, 2025
361b453
fix: Fix browser tests PART 1 (#9064)
RoboErikG May 19, 2025
135da40
fix: focus something after deleting a block (#9073)
maribethb May 20, 2025
53d7876
feat: Add keyboard navigation support for icons. (#9072)
gonfunko May 20, 2025
4f01c99
fix: focus after drag and deleting comments (#9074)
maribethb May 20, 2025
358371c
chore(deps): bump webdriverio from 9.12.5 to 9.14.0 (#9068)
dependabot[bot] May 21, 2025
3c75457
chore(deps): bump mocha from 10.7.3 to 11.3.0 (#9067)
dependabot[bot] May 21, 2025
6dbd7b8
chore(deps-dev): bump undici in the npm_and_yarn group (#8744)
dependabot[bot] May 21, 2025
e4d7245
fix: Visit all nodes in getNextSibling and getPreviousSibling (#9080)
RoboErikG May 21, 2025
4f3eade
fix: Update `focusNode` to self correct focus (#9082)
BenHenning May 22, 2025
056aaf3
feat: Add more ephemeral overrides for drop-downs. (#9086)
BenHenning May 22, 2025
cc9384a
fix: Don't visit collapsed blocks (#9090)
RoboErikG May 23, 2025
ff2ec11
feat: Paste where context menu was opened (#9093)
johnnesky May 27, 2025
ab15372
chore(deps): bump typescript-eslint from 8.31.1 to 8.32.1 (#9095)
dependabot[bot] May 27, 2025
d2c4016
fix: Fix bug that prevented using keyboard shortcuts when the DropDow…
gonfunko May 27, 2025
edf344c
fix: Tweak outline CSS for Safari/Firefox (#9100)
microbit-matt-hillsdon May 27, 2025
d5a4522
fix: Skip invisible inputs in the field navigation policy (#9092)
RoboErikG May 28, 2025
b0b685a
refactor(shortcuts): Factor copy-eligibility out of cut/copy `precond…
cpcallen May 28, 2025
38df7c8
feat: Allow visiting empty input connections. (#9104)
gonfunko May 29, 2025
fd0c08e
fix: Copy shortcuts before returning them (#9109)
gonfunko May 29, 2025
3cbca8e
feat: Automatically manage focus tree tab indexes (#9079)
BenHenning May 29, 2025
0498ed6
feat: add keyboard navigation controller (#8924)
maribethb May 29, 2025
f416875
release: merge branch develop into rc/v12.1.0
maribethb May 29, 2025
3ccfba9
feat: ephemeral focus public getter, use in shortcut precondition (#9…
maribethb May 30, 2025
fdffd65
fix: Make cut/copy/paste work consistently and as expected (#9107)
RoboErikG May 30, 2025
d1b17d1
fix: context menus on flyout (#9116)
maribethb May 30, 2025
cb08247
fix: Fix bug that caused the focus manager to attempt to focus unfocu…
gonfunko May 30, 2025
2ffd3cb
release: merge branch develop into rc/v12.1.0
maribethb May 30, 2025
0d5cc01
release: merge develop into v12.1.0
maribethb May 30, 2025
2ea750f
release: update version number to 12.1.0
maribethb May 30, 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
11 changes: 11 additions & 0 deletions core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,17 @@ export class BlockSvg
Tooltip.dispose();
ContextMenu.hide();

// If this block was focused, focus its parent or workspace instead.
const focusManager = getFocusManager();
if (focusManager.getFocusedNode() === this) {
const parent = this.getParent();
if (parent) {
focusManager.focusNode(parent);
} else {
setTimeout(() => focusManager.focusTree(this.workspace), 0);
}
}

if (animate) {
this.unplug(healStack);
blockAnimations.disposeUiEffect(this);
Expand Down
6 changes: 6 additions & 0 deletions core/blockly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
import * as internalConstants from './internal_constants.js';
import {LineCursor} from './keyboard_nav/line_cursor.js';
import {Marker} from './keyboard_nav/marker.js';
import {
KeyboardNavigationController,
keyboardNavigationController,
} from './keyboard_navigation_controller.js';
import type {LayerManager} from './layer_manager.js';
import * as layers from './layers.js';
import {MarkerManager} from './marker_manager.js';
Expand Down Expand Up @@ -580,6 +584,7 @@ export {
ImageProperties,
Input,
InsertionMarkerPreviewer,
KeyboardNavigationController,
LabelFlyoutInflater,
LayerManager,
Marker,
Expand Down Expand Up @@ -631,6 +636,7 @@ export {
isSelectable,
isSerializable,
isVariableBackedParameterModel,
keyboardNavigationController,
layers,
renderManagement,
serialization,
Expand Down
5 changes: 2 additions & 3 deletions core/bubbles/bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ export abstract class Bubble implements IBubble, ISelectable {
* when automatically positioning.
* @param overriddenFocusableElement An optional replacement to the focusable
* element that's represented by this bubble (as a focusable node). This
* element will have its ID and tabindex overwritten. If not provided, the
* focusable element of this node will default to the bubble's SVG root.
* element will have its ID overwritten. If not provided, the focusable
* element of this node will default to the bubble's SVG root.
*/
constructor(
public readonly workspace: WorkspaceSvg,
Expand Down Expand Up @@ -138,7 +138,6 @@ export abstract class Bubble implements IBubble, ISelectable {

this.focusableElement = overriddenFocusableElement ?? this.svgRoot;
this.focusableElement.setAttribute('id', this.id);
this.focusableElement.setAttribute('tabindex', '-1');

browserEvents.conditionalBind(
this.background,
Expand Down
10 changes: 8 additions & 2 deletions core/comments/rendered_workspace_comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {IContextMenu} from '../interfaces/i_contextmenu.js';
import {ICopyable} from '../interfaces/i_copyable.js';
import {IDeletable} from '../interfaces/i_deletable.js';
import {IDraggable} from '../interfaces/i_draggable.js';
import {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
import {ISelectable} from '../interfaces/i_selectable.js';
Expand All @@ -42,7 +43,8 @@ export class RenderedWorkspaceComment
ISelectable,
IDeletable,
ICopyable<WorkspaceCommentCopyData>,
IContextMenu
IContextMenu,
IFocusableNode
{
/** The class encompassing the svg elements making up the workspace comment. */
private view: CommentView;
Expand All @@ -63,7 +65,6 @@ export class RenderedWorkspaceComment
this.view.setEditable(this.isEditable());
this.view.getSvgRoot().setAttribute('data-id', this.id);
this.view.getSvgRoot().setAttribute('id', this.id);
this.view.getSvgRoot().setAttribute('tabindex', '-1');

this.addModelUpdateBindings();

Expand Down Expand Up @@ -207,7 +208,12 @@ export class RenderedWorkspaceComment
/** Disposes of the view. */
override dispose() {
this.disposing = true;
const focusManager = getFocusManager();
if (focusManager.getFocusedNode() === this) {
setTimeout(() => focusManager.focusTree(this.workspace), 0);
}
if (!this.view.isDeadOrDying()) this.view.dispose();

super.dispose();
}

Expand Down
27 changes: 27 additions & 0 deletions core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

import type {Block} from './block.js';
import {BlockDefinition, Blocks} from './blocks.js';
import * as browserEvents from './browser_events.js';
import type {Connection} from './connection.js';
import {EventType} from './events/type.js';
import * as eventUtils from './events/utils.js';
import {getFocusManager} from './focus_manager.js';
import {ISelectable, isSelectable} from './interfaces/i_selectable.js';
import {ShortcutRegistry} from './shortcut_registry.js';
import type {Workspace} from './workspace.js';
import type {WorkspaceSvg} from './workspace_svg.js';

Expand Down Expand Up @@ -310,4 +312,29 @@ export function defineBlocks(blocks: {[key: string]: BlockDefinition}) {
}
}

/**
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
* is not visible.
*
* @internal
* @param e Key down event.
*/
export function globalShortcutHandler(e: KeyboardEvent) {
const mainWorkspace = getMainWorkspace() as WorkspaceSvg;
if (!mainWorkspace) {
return;
}

if (
browserEvents.isTargetInput(e) ||
(mainWorkspace.rendered && !mainWorkspace.isVisible())
) {
// When focused on an HTML text input widget, don't trap any keys.
// Ignore keypresses on rendered workspaces that have been explicitly
// hidden.
return;
}
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
}

export const TEST_ONLY = {defineBlocksWithJsonArrayInternal};
2 changes: 1 addition & 1 deletion core/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,6 @@ input[type=number] {
.blocklyIconGroup,
.blocklyTextarea
) {
outline-width: 0px;
outline: none;
}
`;
49 changes: 41 additions & 8 deletions core/dropdowndiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// Former goog.module ID: Blockly.dropDownDiv

import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as common from './common.js';
import type {Field} from './field.js';
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
Expand Down Expand Up @@ -86,6 +87,9 @@ let positionToField: boolean | null = null;
/** Callback to FocusManager to return ephemeral focus when the div closes. */
let returnEphemeralFocus: ReturnEphemeralFocus | null = null;

/** Identifier for shortcut keydown listener used to unbind it. */
let keydownListener: browserEvents.Data | null = null;

/**
* Dropdown bounds info object used to encapsulate sizing information about a
* bounding element (bounding box and width/height).
Expand Down Expand Up @@ -122,13 +126,21 @@ export function createDom() {
}
div = document.createElement('div');
div.className = 'blocklyDropDownDiv';
div.tabIndex = -1;
const parentDiv = common.getParentContainer() || document.body;
parentDiv.appendChild(div);

content = document.createElement('div');
content.className = 'blocklyDropDownContent';
div.appendChild(content);

keydownListener = browserEvents.conditionalBind(
content,
'keydown',
null,
common.globalShortcutHandler,
);

arrow = document.createElement('div');
arrow.className = 'blocklyDropDownArrow';
div.appendChild(arrow);
Expand Down Expand Up @@ -167,6 +179,10 @@ export function getContentDiv(): HTMLDivElement {

/** Clear the content of the drop-down. */
export function clearContent() {
if (keydownListener) {
browserEvents.unbind(keydownListener);
keydownListener = null;
}
div.remove();
createDom();
}
Expand All @@ -192,17 +208,24 @@ export function setColour(backgroundColour: string, borderColour: string) {
* @param block Block to position the drop-down around.
* @param opt_onHide Optional callback for when the drop-down is hidden.
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
* @param manageEphemeralFocus Whether ephemeral focus should be managed
* according to the drop-down div's lifetime. Note that if a false value is
* passed in here then callers should manage ephemeral focus directly
* otherwise focus may not properly restore when the widget closes. Defaults
* to true.
* @returns True if the menu rendered below block; false if above.
*/
export function showPositionedByBlock<T>(
field: Field<T>,
block: BlockSvg,
opt_onHide?: () => void,
opt_secondaryYOffset?: number,
manageEphemeralFocus: boolean = true,
): boolean {
return showPositionedByRect(
getScaledBboxOfBlock(block),
field as Field,
manageEphemeralFocus,
opt_onHide,
opt_secondaryYOffset,
);
Expand All @@ -217,17 +240,24 @@ export function showPositionedByBlock<T>(
* @param field The field to position the dropdown against.
* @param opt_onHide Optional callback for when the drop-down is hidden.
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
* @param manageEphemeralFocus Whether ephemeral focus should be managed
* according to the drop-down div's lifetime. Note that if a false value is
* passed in here then callers should manage ephemeral focus directly
* otherwise focus may not properly restore when the widget closes. Defaults
* to true.
* @returns True if the menu rendered below block; false if above.
*/
export function showPositionedByField<T>(
field: Field<T>,
opt_onHide?: () => void,
opt_secondaryYOffset?: number,
manageEphemeralFocus: boolean = true,
): boolean {
positionToField = true;
return showPositionedByRect(
getScaledBboxOfField(field as Field),
field as Field,
manageEphemeralFocus,
opt_onHide,
opt_secondaryYOffset,
);
Expand Down Expand Up @@ -271,16 +301,15 @@ function getScaledBboxOfField(field: Field): Rect {
* @param manageEphemeralFocus Whether ephemeral focus should be managed
* according to the drop-down div's lifetime. Note that if a false value is
* passed in here then callers should manage ephemeral focus directly
* otherwise focus may not properly restore when the widget closes. Defaults
* to true.
* otherwise focus may not properly restore when the widget closes.
* @returns True if the menu rendered below block; false if above.
*/
function showPositionedByRect(
bBox: Rect,
field: Field,
manageEphemeralFocus: boolean,
opt_onHide?: () => void,
opt_secondaryYOffset?: number,
manageEphemeralFocus: boolean = true,
): boolean {
// If we can fit it, render below the block.
const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
Expand Down Expand Up @@ -352,10 +381,6 @@ export function show<T>(
dom.addClass(div, renderedClassName);
dom.addClass(div, themeClassName);

if (manageEphemeralFocus) {
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
}

// When we change `translate` multiple times in close succession,
// Chrome may choose to wait and apply them all at once.
// Since we want the translation to initial X, Y to be immediate,
Expand All @@ -364,7 +389,15 @@ export function show<T>(
// making the dropdown appear to fly in from (0, 0).
// Using both `left`, `top` for the initial translation and then `translate`
// for the animated transition to final X, Y is a workaround.
return positionInternal(primaryX, primaryY, secondaryX, secondaryY);
const atOrigin = positionInternal(primaryX, primaryY, secondaryX, secondaryY);

// Ephemeral focus must happen after the div is fully visible in order to
// ensure that it properly receives focus.
if (manageEphemeralFocus) {
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
}

return atOrigin;
}

const internal = {
Expand Down
1 change: 0 additions & 1 deletion core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,6 @@ export abstract class Field<T = any>
const id = this.id_;
if (!id) throw new Error('Expected ID to be defined prior to init.');
this.fieldGroup_ = dom.createSvgElement(Svg.G, {
'tabindex': '-1',
'id': id,
});
if (!this.isVisible()) {
Expand Down
11 changes: 11 additions & 0 deletions core/field_image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,17 @@ export class FieldImage extends Field<string> {
}
}

/**
* Check whether this field should be clickable.
*
* @returns Whether this field is clickable.
*/
isClickable(): boolean {
// Images are only clickable if they have a click handler and fulfill the
// contract to be clickable: enabled and attached to an editable block.
return super.isClickable() && !!this.clickHandler;
}

/**
* If field click is called, and click handler defined,
* call the handler.
Expand Down
Loading