Skip to content

Commit 93a9b6b

Browse files
authored
fix: Fix navigation for blocks with multiple statement inputs. (#9143)
* fix: Fix navigation for blocks with multiple statement inputs. * chore: Add tests to prevent regressions.
1 parent fd5a7f4 commit 93a9b6b

File tree

2 files changed

+153
-5
lines changed

2 files changed

+153
-5
lines changed

core/keyboard_nav/block_navigation_policy.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
2424
* @returns The first field or input of the given block, if any.
2525
*/
2626
getFirstChild(current: BlockSvg): IFocusableNode | null {
27-
const candidates = getBlockNavigationCandidates(current);
27+
const candidates = getBlockNavigationCandidates(current, true);
2828
return candidates[0];
2929
}
3030

@@ -58,6 +58,8 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
5858
return current.nextConnection?.targetBlock();
5959
} else if (current.outputConnection?.targetBlock()) {
6060
return navigateBlock(current, 1);
61+
} else if (current.getSurroundParent()) {
62+
return navigateBlock(current.getTopStackBlock(), 1);
6163
} else if (this.getParent(current) instanceof WorkspaceSvg) {
6264
return navigateStacks(current, 1);
6365
}
@@ -111,14 +113,27 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
111113
* @param block The block to retrieve the navigable children of.
112114
* @returns A list of navigable/focusable children of the given block.
113115
*/
114-
function getBlockNavigationCandidates(block: BlockSvg): IFocusableNode[] {
116+
function getBlockNavigationCandidates(
117+
block: BlockSvg,
118+
forward: boolean,
119+
): IFocusableNode[] {
115120
const candidates: IFocusableNode[] = block.getIcons();
116121

117122
for (const input of block.inputList) {
118123
if (!input.isVisible()) continue;
119124
candidates.push(...input.fieldRow);
120125
if (input.connection?.targetBlock()) {
121-
candidates.push(input.connection.targetBlock() as BlockSvg);
126+
const connectedBlock = input.connection.targetBlock() as BlockSvg;
127+
if (input.connection.type === ConnectionType.NEXT_STATEMENT && !forward) {
128+
const lastStackBlock = connectedBlock
129+
.lastConnectionInStack(false)
130+
?.getSourceBlock();
131+
if (lastStackBlock) {
132+
candidates.push(lastStackBlock);
133+
}
134+
} else {
135+
candidates.push(connectedBlock);
136+
}
122137
} else if (input.connection?.type === ConnectionType.INPUT_VALUE) {
123138
candidates.push(input.connection as RenderedConnection);
124139
}
@@ -174,11 +189,11 @@ export function navigateBlock(
174189
): IFocusableNode | null {
175190
const block =
176191
current instanceof BlockSvg
177-
? current.outputConnection.targetBlock()
192+
? (current.outputConnection?.targetBlock() ?? current.getSurroundParent())
178193
: current.getSourceBlock();
179194
if (!(block instanceof BlockSvg)) return null;
180195

181-
const candidates = getBlockNavigationCandidates(block);
196+
const candidates = getBlockNavigationCandidates(block, delta > 0);
182197
const currentIndex = candidates.indexOf(current);
183198
if (currentIndex === -1) return null;
184199

tests/mocha/cursor_test.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,33 @@ suite('Cursor', function () {
6060
'tooltip': '',
6161
'helpUrl': '',
6262
},
63+
{
64+
'type': 'multi_statement_input',
65+
'message0': '%1 %2',
66+
'args0': [
67+
{
68+
'type': 'input_statement',
69+
'name': 'FIRST',
70+
},
71+
{
72+
'type': 'input_statement',
73+
'name': 'SECOND',
74+
},
75+
],
76+
},
77+
{
78+
'type': 'simple_statement',
79+
'message0': '%1',
80+
'args0': [
81+
{
82+
'type': 'field_input',
83+
'name': 'NAME',
84+
'text': 'default',
85+
},
86+
],
87+
'previousStatement': null,
88+
'nextStatement': null,
89+
},
6390
]);
6491
this.workspace = Blockly.inject('blocklyDiv', {});
6592
this.cursor = this.workspace.getCursor();
@@ -145,6 +172,112 @@ suite('Cursor', function () {
145172
assert.equal(curNode, this.blocks.D.nextConnection);
146173
});
147174
});
175+
176+
suite('Multiple statement inputs', function () {
177+
setup(function () {
178+
sharedTestSetup.call(this);
179+
Blockly.defineBlocksWithJsonArray([
180+
{
181+
'type': 'multi_statement_input',
182+
'message0': '%1 %2',
183+
'args0': [
184+
{
185+
'type': 'input_statement',
186+
'name': 'FIRST',
187+
},
188+
{
189+
'type': 'input_statement',
190+
'name': 'SECOND',
191+
},
192+
],
193+
},
194+
{
195+
'type': 'simple_statement',
196+
'message0': '%1',
197+
'args0': [
198+
{
199+
'type': 'field_input',
200+
'name': 'NAME',
201+
'text': 'default',
202+
},
203+
],
204+
'previousStatement': null,
205+
'nextStatement': null,
206+
},
207+
]);
208+
this.workspace = Blockly.inject('blocklyDiv', {});
209+
this.cursor = this.workspace.getCursor();
210+
211+
this.multiStatement1 = createRenderedBlock(
212+
this.workspace,
213+
'multi_statement_input',
214+
);
215+
this.multiStatement2 = createRenderedBlock(
216+
this.workspace,
217+
'multi_statement_input',
218+
);
219+
this.firstStatement = createRenderedBlock(
220+
this.workspace,
221+
'simple_statement',
222+
);
223+
this.secondStatement = createRenderedBlock(
224+
this.workspace,
225+
'simple_statement',
226+
);
227+
this.thirdStatement = createRenderedBlock(
228+
this.workspace,
229+
'simple_statement',
230+
);
231+
this.fourthStatement = createRenderedBlock(
232+
this.workspace,
233+
'simple_statement',
234+
);
235+
this.multiStatement1
236+
.getInput('FIRST')
237+
.connection.connect(this.firstStatement.previousConnection);
238+
this.firstStatement.nextConnection.connect(
239+
this.secondStatement.previousConnection,
240+
);
241+
this.multiStatement1
242+
.getInput('SECOND')
243+
.connection.connect(this.thirdStatement.previousConnection);
244+
this.multiStatement2
245+
.getInput('FIRST')
246+
.connection.connect(this.fourthStatement.previousConnection);
247+
});
248+
249+
teardown(function () {
250+
sharedTestTeardown.call(this);
251+
});
252+
253+
test('In - from field in nested statement block to next nested statement block', function () {
254+
this.cursor.setCurNode(this.secondStatement.getField('NAME'));
255+
this.cursor.in();
256+
const curNode = this.cursor.getCurNode();
257+
assert.equal(curNode, this.thirdStatement);
258+
});
259+
test('In - from field in nested statement block to next stack', function () {
260+
this.cursor.setCurNode(this.thirdStatement.getField('NAME'));
261+
this.cursor.in();
262+
const curNode = this.cursor.getCurNode();
263+
assert.equal(curNode, this.multiStatement2);
264+
});
265+
266+
test('Out - from nested statement block to last field of previous nested statement block', function () {
267+
this.cursor.setCurNode(this.thirdStatement);
268+
this.cursor.out();
269+
const curNode = this.cursor.getCurNode();
270+
assert.equal(curNode, this.secondStatement.getField('NAME'));
271+
});
272+
273+
test('Out - from root block to last field of last nested statement block in previous stack', function () {
274+
this.cursor.setCurNode(this.multiStatement2);
275+
this.cursor.out();
276+
const curNode = this.cursor.getCurNode();
277+
assert.equal(curNode, this.thirdStatement.getField('NAME'));
278+
});
279+
});
280+
148281
suite('Searching', function () {
149282
setup(function () {
150283
sharedTestSetup.call(this);

0 commit comments

Comments
 (0)