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
36 changes: 35 additions & 1 deletion packages/blockly/core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,32 @@ export class BlockSvg
return this.svgGroup;
}

/**
* Returns the closest live block to this one, if any.
*/
private getNearestNeighbour() {
if (!this.workspace.rendered) return null;

const blocks = this.workspace
.getAllBlocks(false)
.filter((block) => !block.isDeadOrDying());
let nearestNeighbour = null;
let closestDistance = Number.MAX_SAFE_INTEGER;
const self = this.getRelativeToSurfaceXY();
for (const block of blocks) {
const other = block.getRelativeToSurfaceXY();
const distance = Math.sqrt(
Math.pow(other.x - self.x, 2) + Math.pow(other.y - self.y, 2),
);
if (distance < closestDistance) {
nearestNeighbour = block;
closestDistance = distance;
}
}

return nearestNeighbour;
}

/**
* Dispose of this block.
*
Expand Down Expand Up @@ -904,7 +930,15 @@ export class BlockSvg
if (parent) {
focusManager.focusNode(parent);
} else {
setTimeout(() => focusManager.focusTree(this.workspace), 0);
const nearestNeighbour = this.getNearestNeighbour();
if (nearestNeighbour) {
focusManager.focusNode(nearestNeighbour);
} else {
setTimeout(() => {
if (!this.workspace.rendered) return;
focusManager.focusTree(this.workspace);
}, 0);
}
}
}

Expand Down
115 changes: 115 additions & 0 deletions packages/blockly/tests/mocha/block_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2946,4 +2946,119 @@ suite('Blocks', function () {
);
});
});

suite('Disposal focus management', function () {
setup(function () {
this.workspace = Blockly.inject('blocklyDiv');
const firstBlock = this.workspace.newBlock('stack_block');
firstBlock.moveBy(-500, -500);
});

test('Deleting the sole block on the workspace focuses the workspace', function () {
const block = this.workspace.getTopBlocks(false)[0];
Blockly.getFocusManager().focusNode(block);
block.dispose();
this.clock.runAll();

assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
this.workspace,
'Focus should move to the workspace when the focused block is deleted',
);
});

test('Deleting a block with several adjacent blocks focuses the closest one', function () {
this.workspace.newBlock('stack_block');
const blockMiddle = this.workspace.newBlock('stack_block');
const blockRight = this.workspace.newBlock('stack_block');
blockMiddle.moveBy(60, 0);
blockRight.moveBy(100, 0);

Blockly.getFocusManager().focusNode(blockMiddle);
blockMiddle.dispose();
this.clock.runAll();

const focused = Blockly.getFocusManager().getFocusedNode();
assert.strictEqual(
focused,
blockRight,
'Focus should move to the closest remaining block (blockRight at (100, 0))',
);
});

test('Bulk deleting blocks does not focus another dying block', function () {
const blocks = this.workspace.getTopBlocks(false);
for (let i = 0; i < 5; i++) {
blocks.push(this.workspace.newBlock('stack_block'));
}

// Focus the last block we added; clearing the workspace proceeds in block
// creation order, so if we focused an earlier block, it would (correctly)
// assign focus to a later-added block which is not yet dying, on down the
// chain. If we focus the last block, by the time deletion gets to it, all
// the other blocks will have already been marked as disposing, and should
// thus be ineligible to be focused.
Blockly.getFocusManager().focusNode(
this.workspace.getTopBlocks(false)[5],
);

const spy = sinon.spy(Blockly.getFocusManager(), 'focusNode');

this.workspace.clear();
this.clock.runAll();

for (const block of blocks) {
assert.isFalse(spy.calledWith(block));
}
assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
this.workspace,
'Focus should move to the workspace, not a dying peer block',
);

spy.restore();
});

test('Deleting a block focuses its parent block', function () {
const parent = this.workspace.newBlock('stack_block');
const child = this.workspace.newBlock('stack_block');
parent.nextConnection.connect(child.previousConnection);

Blockly.getFocusManager().focusNode(child);
child.dispose();
this.clock.runAll();

assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
parent,
'Focus should move to the parent block when a connected child is deleted',
);
});

test('Deleting an unfocused block does not change focus', function () {
const a = this.workspace.getTopBlocks(false)[0];
const b = this.workspace.newBlock('stack_block');
this.workspace.newBlock('stack_block');

Blockly.getFocusManager().focusNode(a);
b.dispose();
this.clock.runAll();

assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
a,
'Focus should not change when an unfocused block is deleted',
);
});

test('Disposing a workspace with a focused block succeeds', function () {
Blockly.getFocusManager().focusNode(
this.workspace.getTopBlocks(false)[0],
);
this.workspace.dispose();
this.clock.runAll();

// No assert, this just shouldn't throw.
});
});
});