From bf8aec3cd07f603e7fbdb96f0ba9a64a317be3c7 Mon Sep 17 00:00:00 2001
From: braks <78412429+bcakmakoglu@users.noreply.github.com>
Date: Sun, 14 Jun 2026 09:13:33 +0200
Subject: [PATCH] fix(core): don't swallow input keystrokes held with a
modifier (#1999)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
vue-flow's pan/zoom/selection/multi-selection key detection acted (and
`preventDefault`-ed) even when the keydown targeted a page input while a
modifier was held. Since `panActivationKeyCode` defaults to `Space` and
`selectionKeyCode` to `Shift`, pressing Space in an `` anywhere on the
page with Shift held had its space swallowed — `useKeyPress` listens on
`document`, so any input was affected.
Pass `actInsideInputWithModifier: false` to those `useKeyPress` calls (the
pattern already used for `deleteKeyCode`), so they defer to the focused input.
Adds a regression test that types Shift+Space into an input beside the flow.
Fixes #1999.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.changeset/keypress-input-modifier.md | 5 +++
packages/core/src/container/Pane/Pane.vue | 4 ++-
.../core/src/container/ZoomPane/ZoomPane.vue | 9 +++--
.../2-vue-flow/keyPressInsideInput.cy.ts | 36 +++++++++++++++++++
4 files changed, 50 insertions(+), 4 deletions(-)
create mode 100644 .changeset/keypress-input-modifier.md
create mode 100644 tests/cypress/component/2-vue-flow/keyPressInsideInput.cy.ts
diff --git a/.changeset/keypress-input-modifier.md b/.changeset/keypress-input-modifier.md
new file mode 100644
index 000000000..f9ac1acd7
--- /dev/null
+++ b/.changeset/keypress-input-modifier.md
@@ -0,0 +1,5 @@
+---
+"@vue-flow/core": patch
+---
+
+Don't swallow keystrokes typed into page inputs while a modifier is held. The pan/zoom/selection/multi-selection key detection now passes `actInsideInputWithModifier: false` (matching `deleteKeyCode`), so e.g. pressing Space in an `` with Shift held is no longer `preventDefault`-ed by the pan-activation shortcut. Fixes #1999.
diff --git a/packages/core/src/container/Pane/Pane.vue b/packages/core/src/container/Pane/Pane.vue
index a72b63bd5..714ef60ec 100644
--- a/packages/core/src/container/Pane/Pane.vue
+++ b/packages/core/src/container/Pane/Pane.vue
@@ -51,7 +51,9 @@ let selectionStarted = false
const deleteKeyPressed = useKeyPress(deleteKeyCode, { actInsideInputWithModifier: false })
-const multiSelectKeyPressed = useKeyPress(multiSelectionKeyCode)
+// see `deleteKeyCode` above — don't let the multi-selection key (often Ctrl/Meta) hijack keystrokes like
+// Ctrl+A/C/V while the user is typing in a page input
+const multiSelectKeyPressed = useKeyPress(multiSelectionKeyCode, { actInsideInputWithModifier: false })
watch(deleteKeyPressed, (isKeyPressed) => {
if (!isKeyPressed) {
diff --git a/packages/core/src/container/ZoomPane/ZoomPane.vue b/packages/core/src/container/ZoomPane/ZoomPane.vue
index 6227a2dd3..aa0a6003a 100644
--- a/packages/core/src/container/ZoomPane/ZoomPane.vue
+++ b/packages/core/src/container/ZoomPane/ZoomPane.vue
@@ -37,11 +37,14 @@ const {
connectionStartHandle,
} = storeToRefs(useStore())
-const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode)
+// `actInsideInputWithModifier: false`: while a modifier is held, don't activate zoom/pan/selection when
+// the event targets a page input — otherwise these keys (e.g. Space for pan) `preventDefault` keystrokes
+// the user is typing into an input anywhere on the page. Mirrors `deleteKeyCode` in `Pane.vue`.
+const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode, { actInsideInputWithModifier: false })
-const panKeyPressed = useKeyPress(panActivationKeyCode)
+const panKeyPressed = useKeyPress(panActivationKeyCode, { actInsideInputWithModifier: false })
-const selectionKeyPressed = useKeyPress(selectionKeyCode)
+const selectionKeyPressed = useKeyPress(selectionKeyCode, { actInsideInputWithModifier: false })
const shouldPanOnDrag = toRef(() => !selectionKeyPressed.value && (panKeyPressed.value || panOnDrag.value))
diff --git a/tests/cypress/component/2-vue-flow/keyPressInsideInput.cy.ts b/tests/cypress/component/2-vue-flow/keyPressInsideInput.cy.ts
new file mode 100644
index 000000000..4333dfc14
--- /dev/null
+++ b/tests/cypress/component/2-vue-flow/keyPressInsideInput.cy.ts
@@ -0,0 +1,36 @@
+import { defineComponent, h, ref } from 'vue'
+import { VueFlow } from '@vue-flow/core'
+
+// Regression for #1999: while a modifier (e.g. Shift) is held, vue-flow's pan/zoom/selection key
+// detection must NOT `preventDefault` keystrokes that target a page input. `panActivationKeyCode`
+// defaults to `Space` and `selectionKeyCode` to `Shift`, so Shift+Space used to be swallowed in inputs.
+const App = defineComponent({
+ setup() {
+ const text = ref('')
+ const nodes = ref([{ id: '1', position: { x: 0, y: 0 }, data: { label: 'n1' } }])
+
+ return () =>
+ h('div', [
+ h('input', {
+ 'data-testid': 'page-input',
+ 'value': text.value,
+ 'onInput': (e: any) => (text.value = e.target.value),
+ }),
+ h('div', { style: 'width: 300px; height: 300px' }, [
+ h(VueFlow, { 'nodes': nodes.value, 'onUpdate:nodes': (v: any[]) => (nodes.value = v) }),
+ ]),
+ ])
+ },
+})
+
+describe('Issue #1999: key press inside a page input', () => {
+ it('does not swallow Space typed while Shift is held (pan key + selection modifier)', () => {
+ cy.mount(App)
+
+ cy.get('[data-testid=page-input]').focus().type('a{shift} b', { delay: 20 })
+
+ // the input keeps every character it was given — the Shift+Space is not preventDefaulted away
+ cy.get('[data-testid=page-input]').invoke('val').should('contain', ' ')
+ cy.get('[data-testid=page-input]').invoke('val').should('have.length', 3)
+ })
+})