From 70c6777b5efcd4cb77e5415ff2c6e63a42b3b703 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Thu, 15 Oct 2026 10:38:55 +0100 Subject: [PATCH] Fix ERD tables becoming immovable after a Shift-drag selection. #9890 (#10581) --- .../tools/erd/static/js/erd_tool/ERDCore.js | 45 +++++++++++++- .../javascript/erd/erd_modifier_keys_spec.js | 62 +++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 web/regression/javascript/erd/erd_modifier_keys_spec.js diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js index 117e7577b..f1d020e9f 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js +++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js @@ -26,6 +26,16 @@ import UniqueConstraintSchema from '../../../../../browser/server_groups/servers import PrimaryKeySchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.ui'; import { boundingBoxFromPolygons } from '@projectstorm/geometry'; +/* Modifier keys as recorded by the canvas event bus, mapped to the matching + * flag on mouse events. */ +const MODIFIER_KEYS = { + shift: 'shiftKey', + control: 'ctrlKey', + alt: 'altKey', + meta: 'metaKey', +}; +const MOUSE_EVENT_TYPES = ['mousedown', 'mousemove', 'mouseup', 'wheel']; + export default class ERDCore { constructor() { this._cache = {}; @@ -38,7 +48,40 @@ export default class ERDCore { } createEngine(options) { - return createEngine(options); + let engine = createEngine(options); + this.releaseStaleModifierKeysOnMouseEvents(engine.getActionEventBus()); + return engine; + } + + /* + * The canvas event bus only learns that a key was released from its keyup + * event. If that never reaches the page (for example, Shift is released + * whilst focus is elsewhere), it believes Shift is still held and stays in + * the selecting state, where every drag draws a selection box, and tables + * can no longer be moved nor the canvas panned. Mouse events carry the real + * modifier state, so release any key the event says is no longer held + * before the event is handled. + */ + releaseStaleModifierKeysOnMouseEvents(eventBus) { + const fireAction = eventBus.fireAction.bind(eventBus); + eventBus.fireAction = (actionEvent)=>{ + const event = actionEvent?.event; + if(MOUSE_EVENT_TYPES.includes(event?.type)) { + for(const key of eventBus.getKeys()) { + if(MODIFIER_KEYS[key] && event[MODIFIER_KEYS[key]] === false) { + fireAction({ + event: { + type: 'keyup', + key: key, + preventDefault: () => {/*This is intentional (SonarQube)*/}, + stopPropagation: () => {/*This is intentional (SonarQube)*/}, + }, + }); + } + } + } + fireAction(actionEvent); + }; } initializeEngine() { diff --git a/web/regression/javascript/erd/erd_modifier_keys_spec.js b/web/regression/javascript/erd/erd_modifier_keys_spec.js new file mode 100644 index 000000000..1a5ad671c --- /dev/null +++ b/web/regression/javascript/erd/erd_modifier_keys_spec.js @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2026, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +import ERDCore from 'pgadmin.tools.erd/erd_tool/ERDCore'; + +describe('ERDCore modifier keys', ()=>{ + let engine, canvas; + + const fire = (event)=>{ + engine.getActionEventBus().fireAction({ + event: { + preventDefault: ()=>{/* intentional */}, + stopPropagation: ()=>{/* intentional */}, + target: canvas, + ...event, + }, + }); + }; + const currentState = ()=>engine.getStateMachine().getCurrentState().getOptions().name; + + beforeEach(()=>{ + engine = new ERDCore().getEngine(); + canvas = document.createElement('div'); + document.body.appendChild(canvas); + engine.setCanvas(canvas); + }); + + afterEach(()=>{ + canvas.remove(); + }); + + it('draws a selection box when dragging with Shift held', ()=>{ + fire({type: 'keydown', key: 'Shift'}); + fire({type: 'mousedown', clientX: 10, clientY: 10, buttons: 1, shiftKey: true}); + expect(currentState()).toBe('selection-box'); + }); + + it('pans the canvas once Shift is released, even if the keyup was missed', ()=>{ + fire({type: 'keydown', key: 'Shift'}); + fire({type: 'mousedown', clientX: 10, clientY: 10, buttons: 1, shiftKey: true}); + fire({type: 'mousemove', clientX: 50, clientY: 50, buttons: 1, shiftKey: true}); + fire({type: 'mouseup', clientX: 50, clientY: 50, buttons: 0, shiftKey: true}); + expect(currentState()).toBe('selecting'); + + /* No keyup for Shift arrives, but the next mouse event says it is up */ + fire({type: 'mousedown', clientX: 10, clientY: 10, buttons: 1, shiftKey: false}); + expect(engine.getActionEventBus().getKeys()).not.toContain('shift'); + expect(currentState()).toBe('drag-canvas'); + }); + + it('leaves keys alone for events without modifier flags', ()=>{ + fire({type: 'keydown', key: 'Shift'}); + fire({type: 'mousedown', clientX: 10, clientY: 10, buttons: 1}); + expect(engine.getActionEventBus().getKeys()).toContain('shift'); + expect(currentState()).toBe('selection-box'); + }); +});