Skip to content

Commit c92314d

Browse files
authored
release: v12.2.0
release: v12.2.0 Merge pull request #9224 from google/rc/v12.2.0
2 parents 60fc20a + fae8b7f commit c92314d

File tree

86 files changed

+3124
-2314
lines changed

Some content is hidden

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

86 files changed

+3124
-2314
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Workflow for running the keyboard navigation plugin's automated tests.
2+
3+
name: Keyboard Navigation Automated Tests
4+
5+
on:
6+
workflow_dispatch:
7+
pull_request:
8+
push:
9+
branches:
10+
- develop
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
webdriverio_tests:
17+
name: WebdriverIO tests
18+
timeout-minutes: 10
19+
runs-on: ${{ matrix.os }}
20+
21+
strategy:
22+
fail-fast: false
23+
matrix:
24+
os: [ubuntu-latest, macos-latest]
25+
26+
steps:
27+
- name: Checkout core Blockly
28+
uses: actions/checkout@v4
29+
with:
30+
path: core-blockly
31+
32+
- name: Checkout keyboard navigation plugin
33+
uses: actions/checkout@v4
34+
with:
35+
repository: 'google/blockly-keyboard-experimentation'
36+
ref: 'main'
37+
path: blockly-keyboard-experimentation
38+
39+
- name: Use Node.js 20.x
40+
uses: actions/setup-node@v4
41+
with:
42+
node-version: 20.x
43+
44+
- name: NPM install
45+
run: |
46+
cd core-blockly
47+
npm install
48+
cd ..
49+
cd blockly-keyboard-experimentation
50+
npm install
51+
cd ..
52+
53+
- name: Link latest core develop with plugin
54+
run: |
55+
cd core-blockly
56+
npm run package
57+
cd dist
58+
npm link
59+
cd ../../blockly-keyboard-experimentation
60+
npm link blockly
61+
cd ..
62+
63+
- name: Run keyboard navigation plugin tests
64+
run: |
65+
cd blockly-keyboard-experimentation
66+
npm run test

core/block.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,7 @@ export class Block {
791791
isDeletable(): boolean {
792792
return (
793793
this.deletable &&
794+
!this.isInFlyout &&
794795
!this.shadow &&
795796
!this.isDeadOrDying() &&
796797
!this.workspace.isReadOnly()
@@ -824,6 +825,7 @@ export class Block {
824825
isMovable(): boolean {
825826
return (
826827
this.movable &&
828+
!this.isInFlyout &&
827829
!this.shadow &&
828830
!this.isDeadOrDying() &&
829831
!this.workspace.isReadOnly()

core/block_svg.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,19 @@ export class BlockSvg
299299
}
300300

301301
const oldXY = this.getRelativeToSurfaceXY();
302+
const focusedNode = getFocusManager().getFocusedNode();
303+
const restoreFocus = this.getSvgRoot().contains(
304+
focusedNode?.getFocusableElement() ?? null,
305+
);
302306
if (newParent) {
303307
(newParent as BlockSvg).getSvgRoot().appendChild(svgRoot);
308+
// appendChild() clears focus state, so re-focus the previously focused
309+
// node in case it was this block and would otherwise lose its focus. Once
310+
// Element.moveBefore() has better browser support, it should be used
311+
// instead.
312+
if (restoreFocus && focusedNode) {
313+
getFocusManager().focusNode(focusedNode);
314+
}
304315
} else if (oldParent) {
305316
// If we are losing a parent, we want to move our DOM element to the
306317
// root of the workspace. Try to insert it before any top-level
@@ -319,6 +330,13 @@ export class BlockSvg
319330
canvas.insertBefore(svgRoot, draggingBlockElement);
320331
} else {
321332
canvas.appendChild(svgRoot);
333+
// appendChild() clears focus state, so re-focus the previously focused
334+
// node in case it was this block and would otherwise lose its focus. Once
335+
// Element.moveBefore() has better browser support, it should be used
336+
// instead.
337+
if (restoreFocus && focusedNode) {
338+
getFocusManager().focusNode(focusedNode);
339+
}
322340
}
323341
this.translate(oldXY.x, oldXY.y);
324342
}
@@ -849,10 +867,30 @@ export class BlockSvg
849867
Tooltip.dispose();
850868
ContextMenu.hide();
851869

852-
// If this block was focused, focus its parent or workspace instead.
870+
// If this block (or a descendant) was focused, focus its parent or
871+
// workspace instead.
853872
const focusManager = getFocusManager();
854-
if (focusManager.getFocusedNode() === this) {
855-
const parent = this.getParent();
873+
if (
874+
this.getSvgRoot().contains(
875+
focusManager.getFocusedNode()?.getFocusableElement() ?? null,
876+
)
877+
) {
878+
let parent: BlockSvg | undefined | null = this.getParent();
879+
if (!parent) {
880+
// In some cases, blocks are disconnected from their parents before
881+
// being deleted. Attempt to infer if there was a parent by checking
882+
// for a connection within a radius of 0. Even if this wasn't a parent,
883+
// it must be adjacent to this block and so is as good an option as any
884+
// to focus after deleting.
885+
const connection = this.outputConnection ?? this.previousConnection;
886+
if (connection) {
887+
const targetConnection = connection.closest(
888+
0,
889+
new Coordinate(0, 0),
890+
).connection;
891+
parent = targetConnection?.getSourceBlock();
892+
}
893+
}
856894
if (parent) {
857895
focusManager.focusNode(parent);
858896
} else {
@@ -1721,6 +1759,11 @@ export class BlockSvg
17211759
this.dragStrategy = dragStrategy;
17221760
}
17231761

1762+
/** Returns whether this block is copyable or not. */
1763+
isCopyable(): boolean {
1764+
return this.isOwnDeletable() && this.isOwnMovable();
1765+
}
1766+
17241767
/** Returns whether this block is movable or not. */
17251768
override isMovable(): boolean {
17261769
return this.dragStrategy.isMovable();

core/bubbles/mini_workspace_bubble.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ export class MiniWorkspaceBubble extends Bubble {
153153
* are dealt with by resizing the workspace to show them.
154154
*/
155155
private bumpBlocksIntoBounds() {
156-
if (this.miniWorkspace.isDragging()) return;
156+
if (
157+
this.miniWorkspace.isDragging() &&
158+
!this.miniWorkspace.keyboardMoveInProgress
159+
)
160+
return;
157161

158162
const MARGIN = 20;
159163

@@ -185,7 +189,15 @@ export class MiniWorkspaceBubble extends Bubble {
185189
* mini workspace.
186190
*/
187191
private updateBubbleSize() {
188-
if (this.miniWorkspace.isDragging()) return;
192+
if (
193+
this.miniWorkspace.isDragging() &&
194+
!this.miniWorkspace.keyboardMoveInProgress
195+
)
196+
return;
197+
198+
// Disable autolayout if a keyboard move is in progress to prevent the
199+
// mutator bubble from jumping around.
200+
this.autoLayout &&= !this.miniWorkspace.keyboardMoveInProgress;
189201

190202
const currSize = this.getSize();
191203
const newSize = this.calculateWorkspaceSize();

core/bubbles/textinput_bubble.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ export class TextInputBubble extends Bubble {
173173
browserEvents.conditionalBind(textArea, 'wheel', this, (e: Event) => {
174174
e.stopPropagation();
175175
});
176+
// Don't let the pointerdown event get to the workspace.
177+
browserEvents.conditionalBind(textArea, 'pointerdown', this, (e: Event) => {
178+
e.stopPropagation();
179+
touch.clearTouchIdentifier();
180+
});
176181

177182
browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange);
178183
}

core/clipboard.ts

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js';
1010
import * as registry from './clipboard/registry.js';
1111
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
12+
import {isSelectable} from './interfaces/i_selectable.js';
1213
import * as globalRegistry from './registry.js';
1314
import {Coordinate} from './utils/coordinate.js';
1415
import {WorkspaceSvg} from './workspace_svg.js';
@@ -18,18 +19,119 @@ let stashedCopyData: ICopyData | null = null;
1819

1920
let stashedWorkspace: WorkspaceSvg | null = null;
2021

22+
let stashedCoordinates: Coordinate | undefined = undefined;
23+
2124
/**
22-
* Private version of copy for stubbing in tests.
25+
* Copy a copyable item, and record its data and the workspace it was
26+
* copied from.
27+
*
28+
* This function does not perform any checks to ensure the copy
29+
* should be allowed, e.g. to ensure the block is deletable. Such
30+
* checks should be done before calling this function.
31+
*
32+
* Note that if the copyable item is not an `ISelectable` or its
33+
* `workspace` property is not a `WorkspaceSvg`, the copy will be
34+
* successful, but there will be no saved workspace data. This will
35+
* impact the ability to paste the data unless you explictily pass
36+
* a workspace into the paste method.
37+
*
38+
* @param toCopy item to copy.
39+
* @param location location to save as a potential paste location.
40+
* @returns the copied data if copy was successful, otherwise null.
2341
*/
24-
function copyInternal<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
42+
export function copy<T extends ICopyData>(
43+
toCopy: ICopyable<T>,
44+
location?: Coordinate,
45+
): T | null {
2546
const data = toCopy.toCopyData();
2647
stashedCopyData = data;
27-
stashedWorkspace = (toCopy as any).workspace ?? null;
48+
if (isSelectable(toCopy) && toCopy.workspace instanceof WorkspaceSvg) {
49+
stashedWorkspace = toCopy.workspace;
50+
} else {
51+
stashedWorkspace = null;
52+
}
53+
54+
stashedCoordinates = location;
2855
return data;
2956
}
3057

3158
/**
32-
* Paste a pasteable element into the workspace.
59+
* Gets the copy data for the last item copied. This is useful if you
60+
* are implementing custom copy/paste behavior. If you want the default
61+
* behavior, just use the copy and paste methods directly.
62+
*
63+
* @returns copy data for the last item copied, or null if none set.
64+
*/
65+
export function getLastCopiedData() {
66+
return stashedCopyData;
67+
}
68+
69+
/**
70+
* Sets the last copied item. You should call this method if you implement
71+
* custom copy behavior, so that other callers are working with the correct
72+
* data. This method is called automatically if you use the built-in copy
73+
* method.
74+
*
75+
* @param copyData copy data for the last item copied.
76+
*/
77+
export function setLastCopiedData(copyData: ICopyData) {
78+
stashedCopyData = copyData;
79+
}
80+
81+
/**
82+
* Gets the workspace that was last copied from. This is useful if you
83+
* are implementing custom copy/paste behavior and want to paste on the
84+
* same workspace that was copied from. If you want the default behavior,
85+
* just use the copy and paste methods directly.
86+
*
87+
* @returns workspace that was last copied from, or null if none set.
88+
*/
89+
export function getLastCopiedWorkspace() {
90+
return stashedWorkspace;
91+
}
92+
93+
/**
94+
* Sets the workspace that was last copied from. You should call this method
95+
* if you implement custom copy behavior, so that other callers are working
96+
* with the correct data. This method is called automatically if you use the
97+
* built-in copy method.
98+
*
99+
* @param workspace workspace that was last copied from.
100+
*/
101+
export function setLastCopiedWorkspace(workspace: WorkspaceSvg) {
102+
stashedWorkspace = workspace;
103+
}
104+
105+
/**
106+
* Gets the location that was last copied from. This is useful if you
107+
* are implementing custom copy/paste behavior. If you want the
108+
* default behavior, just use the copy and paste methods directly.
109+
*
110+
* @returns last saved location, or null if none set.
111+
*/
112+
export function getLastCopiedLocation() {
113+
return stashedCoordinates;
114+
}
115+
116+
/**
117+
* Sets the location that was last copied from. You should call this method
118+
* if you implement custom copy behavior, so that other callers are working
119+
* with the correct data. This method is called automatically if you use the
120+
* built-in copy method.
121+
*
122+
* @param location last saved location, which can be used to paste at.
123+
*/
124+
export function setLastCopiedLocation(location: Coordinate) {
125+
stashedCoordinates = location;
126+
}
127+
128+
/**
129+
* Paste a pasteable element into the given workspace.
130+
*
131+
* This function does not perform any checks to ensure the paste
132+
* is allowed, e.g. that the workspace is rendered or the block
133+
* is pasteable. Such checks should be done before calling this
134+
* function.
33135
*
34136
* @param copyData The data to paste into the workspace.
35137
* @param workspace The workspace to paste the data into.
@@ -43,7 +145,7 @@ export function paste<T extends ICopyData>(
43145
): ICopyable<T> | null;
44146

45147
/**
46-
* Pastes the last copied ICopyable into the workspace.
148+
* Pastes the last copied ICopyable into the last copied-from workspace.
47149
*
48150
* @returns the pasted thing if the paste was successful, null otherwise.
49151
*/
@@ -65,7 +167,7 @@ export function paste<T extends ICopyData>(
65167
): ICopyable<ICopyData> | null {
66168
if (!copyData || !workspace) {
67169
if (!stashedCopyData || !stashedWorkspace) return null;
68-
return pasteFromData(stashedCopyData, stashedWorkspace);
170+
return pasteFromData(stashedCopyData, stashedWorkspace, stashedCoordinates);
69171
}
70172
return pasteFromData(copyData, workspace, coordinate);
71173
}
@@ -85,31 +187,11 @@ function pasteFromData<T extends ICopyData>(
85187
): ICopyable<T> | null {
86188
workspace = workspace.isMutator
87189
? workspace
88-
: (workspace.getRootWorkspace() ?? workspace);
190+
: // Use the parent workspace if it exists (e.g. for pasting into flyouts)
191+
(workspace.options.parentWorkspace ?? workspace);
89192
return (globalRegistry
90193
.getObject(globalRegistry.Type.PASTER, copyData.paster, false)
91194
?.paste(copyData, workspace, coordinate) ?? null) as ICopyable<T> | null;
92195
}
93196

94-
/**
95-
* Private version of duplicate for stubbing in tests.
96-
*/
97-
function duplicateInternal<
98-
U extends ICopyData,
99-
T extends ICopyable<U> & IHasWorkspace,
100-
>(toDuplicate: T): T | null {
101-
const data = toDuplicate.toCopyData();
102-
if (!data) return null;
103-
return paste(data, toDuplicate.workspace) as T;
104-
}
105-
106-
interface IHasWorkspace {
107-
workspace: WorkspaceSvg;
108-
}
109-
110-
export const TEST_ONLY = {
111-
duplicateInternal,
112-
copyInternal,
113-
};
114-
115197
export {BlockCopyData, BlockPaster, registry};

core/comments.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
export {CollapseCommentBarButton} from './comments/collapse_comment_bar_button.js';
8+
export {CommentBarButton} from './comments/comment_bar_button.js';
9+
export {CommentEditor} from './comments/comment_editor.js';
710
export {CommentView} from './comments/comment_view.js';
11+
export {DeleteCommentBarButton} from './comments/delete_comment_bar_button.js';
812
export {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
913
export {WorkspaceComment} from './comments/workspace_comment.js';

0 commit comments

Comments
 (0)