From 7b701ad07b1c34d121448e21d6f8b5c13ef07d73 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Wed, 21 Jan 2026 09:16:13 -0800 Subject: [PATCH] fix: Consistent keydown handling for EditableText and TagsInput escape key (#8204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR improves keyboard event handling consistency and fixes an issue where pressing Escape in nested input components would unintentionally close parent modals/dialogs. ## Changes ### Keyboard Event Fixes **TagsInput Escape Key Handling** - Added `@keydown.escape.stop` handler to `TagsInputInput.vue` to prevent Escape from bubbling up and closing parent modals - The handler blurs the input and exits editing mode without propagating the event **EditableText keyup → keydown Migration** - Changed `@keyup.enter` to `@keydown.enter` and `@keyup.escape` to `@keydown.escape` - Using `keydown` is more consistent with how other UI frameworks handle these events and provides more responsive feedback - Updated corresponding unit tests to use `keydown` triggers ### Why keydown over keyup? - `keydown` fires immediately when the key is pressed, providing faster perceived response - Better consistency with browser/OS conventions for action triggers - Prevents default behaviors (like form submission) more reliably when needed - Aligns with other keyboard handlers in the codebase ## Testing - Updated `EditableText.test.ts` to use `keydown` events - Updated `NodeHeader.test.ts` to use `keydown.enter` - Manual testing: Escape in TagsInput no longer closes parent modal ## Checklist - [x] Unit tests updated - [x] Keyboard event handlers consistent - [x] No breaking changes to component API ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8204-fix-Consistent-keydown-handling-for-EditableText-and-TagsInput-escape-key-2ef6d73d365081f0ac6bed8bcae57657) by [Unito](https://www.unito.io) --- src/components/common/EditableText.test.ts | 10 +++++----- src/components/common/EditableText.vue | 6 +++--- src/components/ui/tags-input/TagsInputInput.vue | 6 ++++++ .../extensions/vueNodes/components/NodeHeader.test.ts | 4 ++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/components/common/EditableText.test.ts b/src/components/common/EditableText.test.ts index 2d31123b9..df1ea5152 100644 --- a/src/components/common/EditableText.test.ts +++ b/src/components/common/EditableText.test.ts @@ -51,7 +51,7 @@ describe('EditableText', () => { isEditing: true }) await wrapper.findComponent(InputText).setValue('New Text') - await wrapper.findComponent(InputText).trigger('keyup.enter') + await wrapper.findComponent(InputText).trigger('keydown.enter') // Blur event should have been triggered expect(wrapper.findComponent(InputText).element).not.toBe( document.activeElement @@ -79,7 +79,7 @@ describe('EditableText', () => { await wrapper.findComponent(InputText).setValue('Modified Text') // Press escape - await wrapper.findComponent(InputText).trigger('keyup.escape') + await wrapper.findComponent(InputText).trigger('keydown.escape') // Should emit cancel event expect(wrapper.emitted('cancel')).toBeTruthy() @@ -103,7 +103,7 @@ describe('EditableText', () => { await wrapper.findComponent(InputText).setValue('Modified Text') // Press escape (which triggers blur internally) - await wrapper.findComponent(InputText).trigger('keyup.escape') + await wrapper.findComponent(InputText).trigger('keydown.escape') // Manually trigger blur to simulate the blur that happens after escape await wrapper.findComponent(InputText).trigger('blur') @@ -120,7 +120,7 @@ describe('EditableText', () => { isEditing: true }) await enterWrapper.findComponent(InputText).setValue('Saved Text') - await enterWrapper.findComponent(InputText).trigger('keyup.enter') + await enterWrapper.findComponent(InputText).trigger('keydown.enter') // Trigger blur that happens after enter await enterWrapper.findComponent(InputText).trigger('blur') expect(enterWrapper.emitted('edit')).toBeTruthy() @@ -133,7 +133,7 @@ describe('EditableText', () => { isEditing: true }) await escapeWrapper.findComponent(InputText).setValue('Cancelled Text') - await escapeWrapper.findComponent(InputText).trigger('keyup.escape') + await escapeWrapper.findComponent(InputText).trigger('keydown.escape') expect(escapeWrapper.emitted('cancel')).toBeTruthy() expect(escapeWrapper.emitted('edit')).toBeFalsy() }) diff --git a/src/components/common/EditableText.vue b/src/components/common/EditableText.vue index 322d332a4..440935fb9 100644 --- a/src/components/common/EditableText.vue +++ b/src/components/common/EditableText.vue @@ -3,7 +3,7 @@ {{ modelValue }} - + isEditing.value || isEmpty) const { forwardRef, currentElement } = useForwardExpose() const registerFocus = inject(tagsInputFocusKey, undefined) +function handleEscape() { + currentElement.value?.blur() + isEditing.value = false +} + onMounted(() => { registerFocus?.(() => currentElement.value?.focus()) }) @@ -44,5 +49,6 @@ onUnmounted(() => { className ) " + @keydown.escape.stop="handleEscape" /> diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts b/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts index 0c755116b..a813e88af 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts @@ -154,7 +154,7 @@ describe('NodeHeader.vue', () => { // Edit and confirm (EditableText uses blur or enter to emit) const input = wrapper.get('[data-testid="node-title-input"]') await input.setValue('My Custom Sampler') - await input.trigger('keyup.enter') + await input.trigger('keydown.enter') await input.trigger('blur') // NodeHeader should emit update:title with trimmed value @@ -169,7 +169,7 @@ describe('NodeHeader.vue', () => { await wrapper.get('[data-testid="node-header-1"]').trigger('dblclick') const input = wrapper.get('[data-testid="node-title-input"]') await input.setValue('Should Not Save') - await input.trigger('keyup.escape') + await input.trigger('keydown.escape') // Should not emit update:title expect(wrapper.emitted('update:title')).toBeFalsy()