Skip to content

Commit 60fc20a

Browse files
authored
release: Blockly v12.1.0
Merge pull request #9119 from google/rc/v12.1.0
2 parents 852512f + 2ea750f commit 60fc20a

49 files changed

Lines changed: 2952 additions & 684 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/block_svg.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,17 @@ export class BlockSvg
849849
Tooltip.dispose();
850850
ContextMenu.hide();
851851

852+
// If this block was focused, focus its parent or workspace instead.
853+
const focusManager = getFocusManager();
854+
if (focusManager.getFocusedNode() === this) {
855+
const parent = this.getParent();
856+
if (parent) {
857+
focusManager.focusNode(parent);
858+
} else {
859+
setTimeout(() => focusManager.focusTree(this.workspace), 0);
860+
}
861+
}
862+
852863
if (animate) {
853864
this.unplug(healStack);
854865
blockAnimations.disposeUiEffect(this);

core/blockly.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
173173
import * as internalConstants from './internal_constants.js';
174174
import {LineCursor} from './keyboard_nav/line_cursor.js';
175175
import {Marker} from './keyboard_nav/marker.js';
176+
import {
177+
KeyboardNavigationController,
178+
keyboardNavigationController,
179+
} from './keyboard_navigation_controller.js';
176180
import type {LayerManager} from './layer_manager.js';
177181
import * as layers from './layers.js';
178182
import {MarkerManager} from './marker_manager.js';
@@ -580,6 +584,7 @@ export {
580584
ImageProperties,
581585
Input,
582586
InsertionMarkerPreviewer,
587+
KeyboardNavigationController,
583588
LabelFlyoutInflater,
584589
LayerManager,
585590
Marker,
@@ -631,6 +636,7 @@ export {
631636
isSelectable,
632637
isSerializable,
633638
isVariableBackedParameterModel,
639+
keyboardNavigationController,
634640
layers,
635641
renderManagement,
636642
serialization,

core/bubbles/bubble.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ export abstract class Bubble implements IBubble, ISelectable {
9898
* when automatically positioning.
9999
* @param overriddenFocusableElement An optional replacement to the focusable
100100
* element that's represented by this bubble (as a focusable node). This
101-
* element will have its ID and tabindex overwritten. If not provided, the
102-
* focusable element of this node will default to the bubble's SVG root.
101+
* element will have its ID overwritten. If not provided, the focusable
102+
* element of this node will default to the bubble's SVG root.
103103
*/
104104
constructor(
105105
public readonly workspace: WorkspaceSvg,
@@ -138,7 +138,6 @@ export abstract class Bubble implements IBubble, ISelectable {
138138

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

143142
browserEvents.conditionalBind(
144143
this.background,

core/comments/rendered_workspace_comment.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {IContextMenu} from '../interfaces/i_contextmenu.js';
1919
import {ICopyable} from '../interfaces/i_copyable.js';
2020
import {IDeletable} from '../interfaces/i_deletable.js';
2121
import {IDraggable} from '../interfaces/i_draggable.js';
22+
import {IFocusableNode} from '../interfaces/i_focusable_node.js';
2223
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
2324
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
2425
import {ISelectable} from '../interfaces/i_selectable.js';
@@ -42,7 +43,8 @@ export class RenderedWorkspaceComment
4243
ISelectable,
4344
IDeletable,
4445
ICopyable<WorkspaceCommentCopyData>,
45-
IContextMenu
46+
IContextMenu,
47+
IFocusableNode
4648
{
4749
/** The class encompassing the svg elements making up the workspace comment. */
4850
private view: CommentView;
@@ -63,7 +65,6 @@ export class RenderedWorkspaceComment
6365
this.view.setEditable(this.isEditable());
6466
this.view.getSvgRoot().setAttribute('data-id', this.id);
6567
this.view.getSvgRoot().setAttribute('id', this.id);
66-
this.view.getSvgRoot().setAttribute('tabindex', '-1');
6768

6869
this.addModelUpdateBindings();
6970

@@ -207,7 +208,12 @@ export class RenderedWorkspaceComment
207208
/** Disposes of the view. */
208209
override dispose() {
209210
this.disposing = true;
211+
const focusManager = getFocusManager();
212+
if (focusManager.getFocusedNode() === this) {
213+
setTimeout(() => focusManager.focusTree(this.workspace), 0);
214+
}
210215
if (!this.view.isDeadOrDying()) this.view.dispose();
216+
211217
super.dispose();
212218
}
213219

core/common.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88

99
import type {Block} from './block.js';
1010
import {BlockDefinition, Blocks} from './blocks.js';
11+
import * as browserEvents from './browser_events.js';
1112
import type {Connection} from './connection.js';
1213
import {EventType} from './events/type.js';
1314
import * as eventUtils from './events/utils.js';
1415
import {getFocusManager} from './focus_manager.js';
1516
import {ISelectable, isSelectable} from './interfaces/i_selectable.js';
17+
import {ShortcutRegistry} from './shortcut_registry.js';
1618
import type {Workspace} from './workspace.js';
1719
import type {WorkspaceSvg} from './workspace_svg.js';
1820

@@ -310,4 +312,29 @@ export function defineBlocks(blocks: {[key: string]: BlockDefinition}) {
310312
}
311313
}
312314

315+
/**
316+
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
317+
* is not visible.
318+
*
319+
* @internal
320+
* @param e Key down event.
321+
*/
322+
export function globalShortcutHandler(e: KeyboardEvent) {
323+
const mainWorkspace = getMainWorkspace() as WorkspaceSvg;
324+
if (!mainWorkspace) {
325+
return;
326+
}
327+
328+
if (
329+
browserEvents.isTargetInput(e) ||
330+
(mainWorkspace.rendered && !mainWorkspace.isVisible())
331+
) {
332+
// When focused on an HTML text input widget, don't trap any keys.
333+
// Ignore keypresses on rendered workspaces that have been explicitly
334+
// hidden.
335+
return;
336+
}
337+
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
338+
}
339+
313340
export const TEST_ONLY = {defineBlocksWithJsonArrayInternal};

core/css.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,6 @@ input[type=number] {
505505
.blocklyIconGroup,
506506
.blocklyTextarea
507507
) {
508-
outline-width: 0px;
508+
outline: none;
509509
}
510510
`;

core/dropdowndiv.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// Former goog.module ID: Blockly.dropDownDiv
1414

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

90+
/** Identifier for shortcut keydown listener used to unbind it. */
91+
let keydownListener: browserEvents.Data | null = null;
92+
8993
/**
9094
* Dropdown bounds info object used to encapsulate sizing information about a
9195
* bounding element (bounding box and width/height).
@@ -122,13 +126,21 @@ export function createDom() {
122126
}
123127
div = document.createElement('div');
124128
div.className = 'blocklyDropDownDiv';
129+
div.tabIndex = -1;
125130
const parentDiv = common.getParentContainer() || document.body;
126131
parentDiv.appendChild(div);
127132

128133
content = document.createElement('div');
129134
content.className = 'blocklyDropDownContent';
130135
div.appendChild(content);
131136

137+
keydownListener = browserEvents.conditionalBind(
138+
content,
139+
'keydown',
140+
null,
141+
common.globalShortcutHandler,
142+
);
143+
132144
arrow = document.createElement('div');
133145
arrow.className = 'blocklyDropDownArrow';
134146
div.appendChild(arrow);
@@ -167,6 +179,10 @@ export function getContentDiv(): HTMLDivElement {
167179

168180
/** Clear the content of the drop-down. */
169181
export function clearContent() {
182+
if (keydownListener) {
183+
browserEvents.unbind(keydownListener);
184+
keydownListener = null;
185+
}
170186
div.remove();
171187
createDom();
172188
}
@@ -192,17 +208,24 @@ export function setColour(backgroundColour: string, borderColour: string) {
192208
* @param block Block to position the drop-down around.
193209
* @param opt_onHide Optional callback for when the drop-down is hidden.
194210
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
211+
* @param manageEphemeralFocus Whether ephemeral focus should be managed
212+
* according to the drop-down div's lifetime. Note that if a false value is
213+
* passed in here then callers should manage ephemeral focus directly
214+
* otherwise focus may not properly restore when the widget closes. Defaults
215+
* to true.
195216
* @returns True if the menu rendered below block; false if above.
196217
*/
197218
export function showPositionedByBlock<T>(
198219
field: Field<T>,
199220
block: BlockSvg,
200221
opt_onHide?: () => void,
201222
opt_secondaryYOffset?: number,
223+
manageEphemeralFocus: boolean = true,
202224
): boolean {
203225
return showPositionedByRect(
204226
getScaledBboxOfBlock(block),
205227
field as Field,
228+
manageEphemeralFocus,
206229
opt_onHide,
207230
opt_secondaryYOffset,
208231
);
@@ -217,17 +240,24 @@ export function showPositionedByBlock<T>(
217240
* @param field The field to position the dropdown against.
218241
* @param opt_onHide Optional callback for when the drop-down is hidden.
219242
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
243+
* @param manageEphemeralFocus Whether ephemeral focus should be managed
244+
* according to the drop-down div's lifetime. Note that if a false value is
245+
* passed in here then callers should manage ephemeral focus directly
246+
* otherwise focus may not properly restore when the widget closes. Defaults
247+
* to true.
220248
* @returns True if the menu rendered below block; false if above.
221249
*/
222250
export function showPositionedByField<T>(
223251
field: Field<T>,
224252
opt_onHide?: () => void,
225253
opt_secondaryYOffset?: number,
254+
manageEphemeralFocus: boolean = true,
226255
): boolean {
227256
positionToField = true;
228257
return showPositionedByRect(
229258
getScaledBboxOfField(field as Field),
230259
field as Field,
260+
manageEphemeralFocus,
231261
opt_onHide,
232262
opt_secondaryYOffset,
233263
);
@@ -271,16 +301,15 @@ function getScaledBboxOfField(field: Field): Rect {
271301
* @param manageEphemeralFocus Whether ephemeral focus should be managed
272302
* according to the drop-down div's lifetime. Note that if a false value is
273303
* passed in here then callers should manage ephemeral focus directly
274-
* otherwise focus may not properly restore when the widget closes. Defaults
275-
* to true.
304+
* otherwise focus may not properly restore when the widget closes.
276305
* @returns True if the menu rendered below block; false if above.
277306
*/
278307
function showPositionedByRect(
279308
bBox: Rect,
280309
field: Field,
310+
manageEphemeralFocus: boolean,
281311
opt_onHide?: () => void,
282312
opt_secondaryYOffset?: number,
283-
manageEphemeralFocus: boolean = true,
284313
): boolean {
285314
// If we can fit it, render below the block.
286315
const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
@@ -352,10 +381,6 @@ export function show<T>(
352381
dom.addClass(div, renderedClassName);
353382
dom.addClass(div, themeClassName);
354383

355-
if (manageEphemeralFocus) {
356-
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
357-
}
358-
359384
// When we change `translate` multiple times in close succession,
360385
// Chrome may choose to wait and apply them all at once.
361386
// Since we want the translation to initial X, Y to be immediate,
@@ -364,7 +389,15 @@ export function show<T>(
364389
// making the dropdown appear to fly in from (0, 0).
365390
// Using both `left`, `top` for the initial translation and then `translate`
366391
// for the animated transition to final X, Y is a workaround.
367-
return positionInternal(primaryX, primaryY, secondaryX, secondaryY);
392+
const atOrigin = positionInternal(primaryX, primaryY, secondaryX, secondaryY);
393+
394+
// Ephemeral focus must happen after the div is fully visible in order to
395+
// ensure that it properly receives focus.
396+
if (manageEphemeralFocus) {
397+
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
398+
}
399+
400+
return atOrigin;
368401
}
369402

370403
const internal = {

core/field.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,6 @@ export abstract class Field<T = any>
312312
const id = this.id_;
313313
if (!id) throw new Error('Expected ID to be defined prior to init.');
314314
this.fieldGroup_ = dom.createSvgElement(Svg.G, {
315-
'tabindex': '-1',
316315
'id': id,
317316
});
318317
if (!this.isVisible()) {

core/field_image.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ export class FieldImage extends Field<string> {
212212
}
213213
}
214214

215+
/**
216+
* Check whether this field should be clickable.
217+
*
218+
* @returns Whether this field is clickable.
219+
*/
220+
isClickable(): boolean {
221+
// Images are only clickable if they have a click handler and fulfill the
222+
// contract to be clickable: enabled and attached to an editable block.
223+
return super.isClickable() && !!this.clickHandler;
224+
}
225+
215226
/**
216227
* If field click is called, and click handler defined,
217228
* call the handler.

0 commit comments

Comments
 (0)