Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 20 additions & 5 deletions core/keyboard_nav/block_navigation_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @returns The first field or input of the given block, if any.
*/
getFirstChild(current: BlockSvg): IFocusableNode | null {
const candidates = getBlockNavigationCandidates(current);
const candidates = getBlockNavigationCandidates(current, true);
return candidates[0];
}

Expand Down Expand Up @@ -58,6 +58,8 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
return current.nextConnection?.targetBlock();
} else if (current.outputConnection?.targetBlock()) {
return navigateBlock(current, 1);
} else if (current.getSurroundParent()) {
return navigateBlock(current.getTopStackBlock(), 1);
} else if (this.getParent(current) instanceof WorkspaceSvg) {
return navigateStacks(current, 1);
}
Expand Down Expand Up @@ -111,14 +113,27 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @param block The block to retrieve the navigable children of.
* @returns A list of navigable/focusable children of the given block.
*/
function getBlockNavigationCandidates(block: BlockSvg): IFocusableNode[] {
function getBlockNavigationCandidates(
block: BlockSvg,
forward: boolean,
): IFocusableNode[] {
const candidates: IFocusableNode[] = block.getIcons();

for (const input of block.inputList) {
if (!input.isVisible()) continue;
candidates.push(...input.fieldRow);
if (input.connection?.targetBlock()) {
candidates.push(input.connection.targetBlock() as BlockSvg);
const connectedBlock = input.connection.targetBlock() as BlockSvg;
if (input.connection.type === ConnectionType.NEXT_STATEMENT && !forward) {
const lastStackBlock = connectedBlock
.lastConnectionInStack(false)
?.getSourceBlock();
if (lastStackBlock) {
candidates.push(lastStackBlock);
}
} else {
candidates.push(connectedBlock);
}
} else if (input.connection?.type === ConnectionType.INPUT_VALUE) {
candidates.push(input.connection as RenderedConnection);
}
Expand Down Expand Up @@ -174,11 +189,11 @@ export function navigateBlock(
): IFocusableNode | null {
const block =
current instanceof BlockSvg
? current.outputConnection.targetBlock()
? (current.outputConnection?.targetBlock() ?? current.getSurroundParent())
: current.getSourceBlock();
if (!(block instanceof BlockSvg)) return null;

const candidates = getBlockNavigationCandidates(block);
const candidates = getBlockNavigationCandidates(block, delta > 0);
const currentIndex = candidates.indexOf(current);
if (currentIndex === -1) return null;

Expand Down
133 changes: 133 additions & 0 deletions tests/mocha/cursor_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,33 @@ suite('Cursor', function () {
'tooltip': '',
'helpUrl': '',
},
{
'type': 'multi_statement_input',
'message0': '%1 %2',
'args0': [
{
'type': 'input_statement',
'name': 'FIRST',
},
{
'type': 'input_statement',
'name': 'SECOND',
},
],
},
{
'type': 'simple_statement',
'message0': '%1',
'args0': [
{
'type': 'field_input',
'name': 'NAME',
'text': 'default',
},
],
'previousStatement': null,
'nextStatement': null,
},
]);
this.workspace = Blockly.inject('blocklyDiv', {});
this.cursor = this.workspace.getCursor();
Expand Down Expand Up @@ -145,6 +172,112 @@ suite('Cursor', function () {
assert.equal(curNode, this.blocks.D.nextConnection);
});
});

suite('Multiple statement inputs', function () {
setup(function () {
sharedTestSetup.call(this);
Blockly.defineBlocksWithJsonArray([
{
'type': 'multi_statement_input',
'message0': '%1 %2',
'args0': [
{
'type': 'input_statement',
'name': 'FIRST',
},
{
'type': 'input_statement',
'name': 'SECOND',
},
],
},
{
'type': 'simple_statement',
'message0': '%1',
'args0': [
{
'type': 'field_input',
'name': 'NAME',
'text': 'default',
},
],
'previousStatement': null,
'nextStatement': null,
},
]);
this.workspace = Blockly.inject('blocklyDiv', {});
this.cursor = this.workspace.getCursor();

this.multiStatement1 = createRenderedBlock(
this.workspace,
'multi_statement_input',
);
this.multiStatement2 = createRenderedBlock(
this.workspace,
'multi_statement_input',
);
this.firstStatement = createRenderedBlock(
this.workspace,
'simple_statement',
);
this.secondStatement = createRenderedBlock(
this.workspace,
'simple_statement',
);
this.thirdStatement = createRenderedBlock(
this.workspace,
'simple_statement',
);
this.fourthStatement = createRenderedBlock(
this.workspace,
'simple_statement',
);
this.multiStatement1
.getInput('FIRST')
.connection.connect(this.firstStatement.previousConnection);
this.firstStatement.nextConnection.connect(
this.secondStatement.previousConnection,
);
this.multiStatement1
.getInput('SECOND')
.connection.connect(this.thirdStatement.previousConnection);
this.multiStatement2
.getInput('FIRST')
.connection.connect(this.fourthStatement.previousConnection);
});

teardown(function () {
sharedTestTeardown.call(this);
});

test('In - from field in nested statement block to next nested statement block', function () {
this.cursor.setCurNode(this.secondStatement.getField('NAME'));
this.cursor.in();
const curNode = this.cursor.getCurNode();
assert.equal(curNode, this.thirdStatement);
});
test('In - from field in nested statement block to next stack', function () {
this.cursor.setCurNode(this.thirdStatement.getField('NAME'));
this.cursor.in();
const curNode = this.cursor.getCurNode();
assert.equal(curNode, this.multiStatement2);
});

test('Out - from nested statement block to last field of previous nested statement block', function () {
this.cursor.setCurNode(this.thirdStatement);
this.cursor.out();
const curNode = this.cursor.getCurNode();
assert.equal(curNode, this.secondStatement.getField('NAME'));
});

test('Out - from root block to last field of last nested statement block in previous stack', function () {
this.cursor.setCurNode(this.multiStatement2);
this.cursor.out();
const curNode = this.cursor.getCurNode();
assert.equal(curNode, this.thirdStatement.getField('NAME'));
});
});

suite('Searching', function () {
setup(function () {
sharedTestSetup.call(this);
Expand Down