Skip to content

Commit e94b350

Browse files
committed
release: merge develop into rc/v12.2.0
2 parents 60fc20a + 0d6da6c commit e94b350

43 files changed

Lines changed: 1093 additions & 589 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,6 +1721,11 @@ export class BlockSvg
17211721
this.dragStrategy = dragStrategy;
17221722
}
17231723

1724+
/** Returns whether this block is copyable or not. */
1725+
isCopyable(): boolean {
1726+
return this.isOwnDeletable() && this.isOwnMovable();
1727+
}
1728+
17241729
/** Returns whether this block is movable or not. */
17251730
override isMovable(): boolean {
17261731
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/comments.ts

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

7+
export {CommentEditor} from './comments/comment_editor.js';
78
export {CommentView} from './comments/comment_view.js';
89
export {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
910
export {WorkspaceComment} from './comments/workspace_comment.js';

core/comments/comment_editor.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* @license
3+
* Copyright 2024 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as browserEvents from '../browser_events.js';
8+
import {getFocusManager} from '../focus_manager.js';
9+
import {IFocusableNode} from '../interfaces/i_focusable_node.js';
10+
import {IFocusableTree} from '../interfaces/i_focusable_tree.js';
11+
import * as touch from '../touch.js';
12+
import * as dom from '../utils/dom.js';
13+
import {Size} from '../utils/size.js';
14+
import {Svg} from '../utils/svg.js';
15+
import {WorkspaceSvg} from '../workspace_svg.js';
16+
17+
/**
18+
* String added to the ID of a workspace comment to identify
19+
* the focusable node for the comment editor.
20+
*/
21+
export const COMMENT_EDITOR_FOCUS_IDENTIFIER = '_comment_textarea_';
22+
23+
/** The part of a comment that can be typed into. */
24+
export class CommentEditor implements IFocusableNode {
25+
id?: string;
26+
/** The foreignObject containing the HTML text area. */
27+
private foreignObject: SVGForeignObjectElement;
28+
29+
/** The text area where the user can type. */
30+
private textArea: HTMLTextAreaElement;
31+
32+
/** Listeners for changes to text. */
33+
private textChangeListeners: Array<
34+
(oldText: string, newText: string) => void
35+
> = [];
36+
37+
/** The current text of the comment. Updates on text area change. */
38+
private text: string = '';
39+
40+
constructor(
41+
public workspace: WorkspaceSvg,
42+
commentId?: string,
43+
private onFinishEditing?: () => void,
44+
) {
45+
this.foreignObject = dom.createSvgElement(Svg.FOREIGNOBJECT, {
46+
'class': 'blocklyCommentForeignObject',
47+
});
48+
const body = document.createElementNS(dom.HTML_NS, 'body');
49+
body.setAttribute('xmlns', dom.HTML_NS);
50+
body.className = 'blocklyMinimalBody';
51+
this.textArea = document.createElementNS(
52+
dom.HTML_NS,
53+
'textarea',
54+
) as HTMLTextAreaElement;
55+
dom.addClass(this.textArea, 'blocklyCommentText');
56+
dom.addClass(this.textArea, 'blocklyTextarea');
57+
dom.addClass(this.textArea, 'blocklyText');
58+
body.appendChild(this.textArea);
59+
this.foreignObject.appendChild(body);
60+
61+
if (commentId) {
62+
this.id = commentId + COMMENT_EDITOR_FOCUS_IDENTIFIER;
63+
this.textArea.setAttribute('id', this.id);
64+
}
65+
66+
// Register browser event listeners for the user typing in the textarea.
67+
browserEvents.conditionalBind(
68+
this.textArea,
69+
'change',
70+
this,
71+
this.onTextChange,
72+
);
73+
74+
// Register listener for pointerdown to focus the textarea.
75+
browserEvents.conditionalBind(
76+
this.textArea,
77+
'pointerdown',
78+
this,
79+
(e: PointerEvent) => {
80+
// don't allow this event to bubble up
81+
// and steal focus away from the editor/comment.
82+
e.stopPropagation();
83+
getFocusManager().focusNode(this);
84+
touch.clearTouchIdentifier();
85+
},
86+
);
87+
88+
// Register listener for keydown events that would finish editing.
89+
browserEvents.conditionalBind(
90+
this.textArea,
91+
'keydown',
92+
this,
93+
this.handleKeyDown,
94+
);
95+
}
96+
97+
/** Gets the dom structure for this comment editor. */
98+
getDom(): SVGForeignObjectElement {
99+
return this.foreignObject;
100+
}
101+
102+
/** Gets the current text of the comment. */
103+
getText(): string {
104+
return this.text;
105+
}
106+
107+
/** Sets the current text of the comment and fires change listeners. */
108+
setText(text: string) {
109+
this.textArea.value = text;
110+
this.onTextChange();
111+
}
112+
113+
/**
114+
* Triggers listeners when the text of the comment changes, either
115+
* programmatically or manually by the user.
116+
*/
117+
private onTextChange() {
118+
const oldText = this.text;
119+
this.text = this.textArea.value;
120+
// Loop through listeners backwards in case they remove themselves.
121+
for (let i = this.textChangeListeners.length - 1; i >= 0; i--) {
122+
this.textChangeListeners[i](oldText, this.text);
123+
}
124+
}
125+
126+
/**
127+
* Do something when the user indicates they've finished editing.
128+
*
129+
* @param e Keyboard event.
130+
*/
131+
private handleKeyDown(e: KeyboardEvent) {
132+
if (e.key === 'Escape' || (e.key === 'Enter' && (e.ctrlKey || e.metaKey))) {
133+
if (this.onFinishEditing) this.onFinishEditing();
134+
e.stopPropagation();
135+
}
136+
}
137+
138+
/** Registers a callback that listens for text changes. */
139+
addTextChangeListener(listener: (oldText: string, newText: string) => void) {
140+
this.textChangeListeners.push(listener);
141+
}
142+
143+
/** Removes the given listener from the list of text change listeners. */
144+
removeTextChangeListener(listener: () => void) {
145+
this.textChangeListeners.splice(
146+
this.textChangeListeners.indexOf(listener),
147+
1,
148+
);
149+
}
150+
151+
/** Sets the placeholder text displayed for an empty comment. */
152+
setPlaceholderText(text: string) {
153+
this.textArea.placeholder = text;
154+
}
155+
156+
/** Sets whether the textarea is editable. If not, the textarea will be readonly. */
157+
setEditable(isEditable: boolean) {
158+
if (isEditable) {
159+
this.textArea.removeAttribute('readonly');
160+
} else {
161+
this.textArea.setAttribute('readonly', 'true');
162+
}
163+
}
164+
165+
/** Update the size of the comment editor element. */
166+
updateSize(size: Size, topBarSize: Size) {
167+
this.foreignObject.setAttribute(
168+
'height',
169+
`${size.height - topBarSize.height}`,
170+
);
171+
this.foreignObject.setAttribute('width', `${size.width}`);
172+
this.foreignObject.setAttribute('y', `${topBarSize.height}`);
173+
if (this.workspace.RTL) {
174+
this.foreignObject.setAttribute('x', `${-size.width}`);
175+
}
176+
}
177+
178+
getFocusableElement(): HTMLElement | SVGElement {
179+
return this.textArea;
180+
}
181+
getFocusableTree(): IFocusableTree {
182+
return this.workspace;
183+
}
184+
onNodeFocus(): void {}
185+
onNodeBlur(): void {}
186+
canBeFocused(): boolean {
187+
if (this.id) return true;
188+
return false;
189+
}
190+
}

0 commit comments

Comments
 (0)