Skip to content

Commit 3d78491

Browse files
authored
fix: Focus nearest neighbor when deleting a focused block (#9599)
* fix: Focus the nearest neighbor on block deletion * test: Add tests * fix: Use `strictEqual` * chore: Reduce the number of test blocks * fix: Explicitly verify that dying blocks are not focused * fix: Fix exception when disposing of a workspace with a focused block * chore: Run formatter
1 parent 3b14950 commit 3d78491

2 files changed

Lines changed: 150 additions & 1 deletion

File tree

packages/blockly/core/block_svg.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,32 @@ export class BlockSvg
863863
return this.svgGroup;
864864
}
865865

866+
/**
867+
* Returns the closest live block to this one, if any.
868+
*/
869+
private getNearestNeighbour() {
870+
if (!this.workspace.rendered) return null;
871+
872+
const blocks = this.workspace
873+
.getAllBlocks(false)
874+
.filter((block) => !block.isDeadOrDying());
875+
let nearestNeighbour = null;
876+
let closestDistance = Number.MAX_SAFE_INTEGER;
877+
const self = this.getRelativeToSurfaceXY();
878+
for (const block of blocks) {
879+
const other = block.getRelativeToSurfaceXY();
880+
const distance = Math.sqrt(
881+
Math.pow(other.x - self.x, 2) + Math.pow(other.y - self.y, 2),
882+
);
883+
if (distance < closestDistance) {
884+
nearestNeighbour = block;
885+
closestDistance = distance;
886+
}
887+
}
888+
889+
return nearestNeighbour;
890+
}
891+
866892
/**
867893
* Dispose of this block.
868894
*
@@ -904,7 +930,15 @@ export class BlockSvg
904930
if (parent) {
905931
focusManager.focusNode(parent);
906932
} else {
907-
setTimeout(() => focusManager.focusTree(this.workspace), 0);
933+
const nearestNeighbour = this.getNearestNeighbour();
934+
if (nearestNeighbour) {
935+
focusManager.focusNode(nearestNeighbour);
936+
} else {
937+
setTimeout(() => {
938+
if (!this.workspace.rendered) return;
939+
focusManager.focusTree(this.workspace);
940+
}, 0);
941+
}
908942
}
909943
}
910944

packages/blockly/tests/mocha/block_test.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2946,4 +2946,119 @@ suite('Blocks', function () {
29462946
);
29472947
});
29482948
});
2949+
2950+
suite('Disposal focus management', function () {
2951+
setup(function () {
2952+
this.workspace = Blockly.inject('blocklyDiv');
2953+
const firstBlock = this.workspace.newBlock('stack_block');
2954+
firstBlock.moveBy(-500, -500);
2955+
});
2956+
2957+
test('Deleting the sole block on the workspace focuses the workspace', function () {
2958+
const block = this.workspace.getTopBlocks(false)[0];
2959+
Blockly.getFocusManager().focusNode(block);
2960+
block.dispose();
2961+
this.clock.runAll();
2962+
2963+
assert.strictEqual(
2964+
Blockly.getFocusManager().getFocusedNode(),
2965+
this.workspace,
2966+
'Focus should move to the workspace when the focused block is deleted',
2967+
);
2968+
});
2969+
2970+
test('Deleting a block with several adjacent blocks focuses the closest one', function () {
2971+
this.workspace.newBlock('stack_block');
2972+
const blockMiddle = this.workspace.newBlock('stack_block');
2973+
const blockRight = this.workspace.newBlock('stack_block');
2974+
blockMiddle.moveBy(60, 0);
2975+
blockRight.moveBy(100, 0);
2976+
2977+
Blockly.getFocusManager().focusNode(blockMiddle);
2978+
blockMiddle.dispose();
2979+
this.clock.runAll();
2980+
2981+
const focused = Blockly.getFocusManager().getFocusedNode();
2982+
assert.strictEqual(
2983+
focused,
2984+
blockRight,
2985+
'Focus should move to the closest remaining block (blockRight at (100, 0))',
2986+
);
2987+
});
2988+
2989+
test('Bulk deleting blocks does not focus another dying block', function () {
2990+
const blocks = this.workspace.getTopBlocks(false);
2991+
for (let i = 0; i < 5; i++) {
2992+
blocks.push(this.workspace.newBlock('stack_block'));
2993+
}
2994+
2995+
// Focus the last block we added; clearing the workspace proceeds in block
2996+
// creation order, so if we focused an earlier block, it would (correctly)
2997+
// assign focus to a later-added block which is not yet dying, on down the
2998+
// chain. If we focus the last block, by the time deletion gets to it, all
2999+
// the other blocks will have already been marked as disposing, and should
3000+
// thus be ineligible to be focused.
3001+
Blockly.getFocusManager().focusNode(
3002+
this.workspace.getTopBlocks(false)[5],
3003+
);
3004+
3005+
const spy = sinon.spy(Blockly.getFocusManager(), 'focusNode');
3006+
3007+
this.workspace.clear();
3008+
this.clock.runAll();
3009+
3010+
for (const block of blocks) {
3011+
assert.isFalse(spy.calledWith(block));
3012+
}
3013+
assert.strictEqual(
3014+
Blockly.getFocusManager().getFocusedNode(),
3015+
this.workspace,
3016+
'Focus should move to the workspace, not a dying peer block',
3017+
);
3018+
3019+
spy.restore();
3020+
});
3021+
3022+
test('Deleting a block focuses its parent block', function () {
3023+
const parent = this.workspace.newBlock('stack_block');
3024+
const child = this.workspace.newBlock('stack_block');
3025+
parent.nextConnection.connect(child.previousConnection);
3026+
3027+
Blockly.getFocusManager().focusNode(child);
3028+
child.dispose();
3029+
this.clock.runAll();
3030+
3031+
assert.strictEqual(
3032+
Blockly.getFocusManager().getFocusedNode(),
3033+
parent,
3034+
'Focus should move to the parent block when a connected child is deleted',
3035+
);
3036+
});
3037+
3038+
test('Deleting an unfocused block does not change focus', function () {
3039+
const a = this.workspace.getTopBlocks(false)[0];
3040+
const b = this.workspace.newBlock('stack_block');
3041+
this.workspace.newBlock('stack_block');
3042+
3043+
Blockly.getFocusManager().focusNode(a);
3044+
b.dispose();
3045+
this.clock.runAll();
3046+
3047+
assert.strictEqual(
3048+
Blockly.getFocusManager().getFocusedNode(),
3049+
a,
3050+
'Focus should not change when an unfocused block is deleted',
3051+
);
3052+
});
3053+
3054+
test('Disposing a workspace with a focused block succeeds', function () {
3055+
Blockly.getFocusManager().focusNode(
3056+
this.workspace.getTopBlocks(false)[0],
3057+
);
3058+
this.workspace.dispose();
3059+
this.clock.runAll();
3060+
3061+
// No assert, this just shouldn't throw.
3062+
});
3063+
});
29493064
});

0 commit comments

Comments
 (0)