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
38 changes: 29 additions & 9 deletions main/blocklyinit.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ if (!Blockly.serialization.registry.getClass?.('flockLock')) {
let workspace = null;
export { workspace };

function installWorkspaceJumpDebug(workspace) {
export function installWorkspaceJumpDebug(workspace) {
if (!workspace || workspace.__jumpDebugInstalled) return;
workspace.__jumpDebugInstalled = true;

Expand All @@ -180,6 +180,21 @@ function installWorkspaceJumpDebug(workspace) {
}
});

// Also treat a direct tap on the canvas as a field interaction, so the jump is
// suppressed on the tap that opens a field editor, not just after it closes.
let lastCanvasPointerDown = null;
const parentSvg = workspace.getParentSvg?.();
if (parentSvg) {
parentSvg.addEventListener(
'pointerdown',
(e) => {
if (e.target.closest?.('.blocklyFlyout, .blocklyToolboxDiv')) return;
lastCanvasPointerDown = { timestamp: performance.now() };
},
true
);
}

const workspaceScroll = workspace.scroll?.bind(workspace);
if (workspaceScroll) {
workspace.scroll = function (...args) {
Expand All @@ -193,19 +208,24 @@ function installWorkspaceJumpDebug(workspace) {
const msSinceFieldEdit = lastFieldEdit
? Math.round(performance.now() - lastFieldEdit.timestamp)
: null;
const msSinceCanvasPointerDown = lastCanvasPointerDown
? Math.round(performance.now() - lastCanvasPointerDown.timestamp)
: null;
const fromFocusScroll = stack.some(
(line) => line.includes('scrollBoundsIntoView') || line.includes('onNodeFocus')
);
const largeHorizontalJump =
typeof requestedX === 'number' && Math.abs(requestedX - beforeX) > 100;

if (
fromFocusScroll &&
typeof msSinceFieldEdit === 'number' &&
msSinceFieldEdit < 1500 &&
largeHorizontalJump
) {
return;
const recentFieldInteraction =
(typeof msSinceFieldEdit === 'number' && msSinceFieldEdit < 1500) ||
(typeof msSinceCanvasPointerDown === 'number' && msSinceCanvasPointerDown < 800);

if (fromFocusScroll && recentFieldInteraction && largeHorizontalJump) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Keep X pinned (suppress the unwanted jump) but still apply Y —
// scrollBoundsIntoView bundles both into one call, and a wanted
// vertical correction shouldn't be dropped along with the horizontal one.
const requestedY = args[1];
return workspaceScroll(beforeX, typeof requestedY === 'number' ? requestedY : this.scrollY);
}

return workspaceScroll(...args);
Expand Down
89 changes: 89 additions & 0 deletions tests/blocklyinit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { expect } from 'chai';
import * as Blockly from 'blockly';
import { installWorkspaceJumpDebug } from '../main/blocklyinit.js';
import { defineControlBlocks } from '../blocks/control.js';

export function runBlocklyInitTests(_flock) {
describe('main/blocklyinit @blocklyinit', function () {
let workspace;
let container;

before(function () {
defineControlBlocks();
});

beforeEach(function () {
container = document.createElement('div');
container.style.width = '300px';
container.style.height = '200px';
document.body.appendChild(container);
workspace = Blockly.inject(container, {
move: { scrollbars: { horizontal: true, vertical: true }, drag: true, wheel: true },
});

// scroll() clamps to the content bounding box (see blockly_compressed.js):
// a single block barely bigger than the viewport gives zero scroll slack,
// so the box has to be much larger than the viewport in both dimensions to
// leave real scroll range for the assertions below.
const near = workspace.newBlock('wait');
near.initSvg();
near.render();

const far = workspace.newBlock('wait');
far.initSvg();
far.render();
far.moveBy(3000, 3000);

installWorkspaceJumpDebug(workspace);
});

afterEach(function () {
workspace?.dispose();
container?.remove();
});

describe('focus-scroll jump suppression', function () {
// Named to match the stack-trace check in installWorkspaceJumpDebug, which
// only reacts to Blockly's own focus-follow path (scrollBoundsIntoView /
// onNodeFocus), mirroring the real call site in blockly_compressed.js.
function scrollBoundsIntoView(x, y) {
workspace.scroll(x, y);
}

it('suppresses a large horizontal jump but keeps the accompanying vertical scroll after a direct canvas tap', function () {
workspace
.getParentSvg()
.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));

const beforeX = workspace.scrollX;
const beforeY = workspace.scrollY;
scrollBoundsIntoView(beforeX - 400, beforeY - 50);

expect(workspace.scrollX).to.equal(beforeX);
expect(workspace.scrollY).to.equal(beforeY - 50);
});

it('applies both axes normally when there was no recent canvas tap', function () {
const beforeX = workspace.scrollX;
const beforeY = workspace.scrollY;
scrollBoundsIntoView(beforeX - 400, beforeY - 50);

expect(workspace.scrollX).to.equal(beforeX - 400);
expect(workspace.scrollY).to.equal(beforeY - 50);
});

it('applies both axes normally for a non-focus-driven scroll even after a canvas tap', function () {
workspace
.getParentSvg()
.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));

const beforeX = workspace.scrollX;
const beforeY = workspace.scrollY;
workspace.scroll(beforeX - 400, beforeY - 50);

expect(workspace.scrollX).to.equal(beforeX - 400);
expect(workspace.scrollY).to.equal(beforeY - 50);
});
});
});
}
7 changes: 7 additions & 0 deletions tests/tests.html
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,13 @@ <h1>Flock Test Example</h1>
importFn: 'runSecurityTests',
pattern: '@security',
},
{
id: 'blocklyinit',
name: 'Blockly Init Tests',
importPath: './blocklyinit.test.js',
importFn: 'runBlocklyInitTests',
pattern: 'main/blocklyinit',
},
];

import * as flockmodule from '../flock.js';
Expand Down
Loading