From ab658aa7e419ff155511370dff3d32d53ae5ea53 Mon Sep 17 00:00:00 2001 From: Bingbing Date: Tue, 29 Sep 2026 15:34:49 +0800 Subject: [PATCH] fix(key-value-editor): stop ListBox typeahead from swallowing typed spaces (#10563) Background: in the environment variables table view and the generic key-value editor, a space typed within 1s of the previous printable character is swallowed (typing "My key" produces "Mykey"); users had to insert spaces via arrow keys afterwards. Raw JSON editing is unaffected. The react-aria ListBox typeahead (useTypeSelect) intercepts space keydowns in the capture phase while its search buffer is non-empty. Disable typeahead via disallowTypeAhead on the ListBoxes that embed inline editors (environment key-value editor and the generic key-value editor), remove the now-obsolete space<->NBSP keydown workaround, and add a module augmentation for the prop that is missing from ListBoxProps typings in all published react-aria-components versions. Add smoke tests typing at human speed (120ms per keystroke) in the environment table, headers, and query params views; zero-delay bursts do not reproduce the bug. --- .../key-value-editor-space-typing.test.ts | 88 +++++++++++++++++++ .../key-value-editor.tsx | 1 + .../key-value-editor/key-value-editor.tsx | 28 ++---- .../insomnia/types/react-aria-components.d.ts | 30 +++++++ 4 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 packages/insomnia-smoke-test/tests/smoke/key-value-editor-space-typing.test.ts create mode 100644 packages/insomnia/types/react-aria-components.d.ts diff --git a/packages/insomnia-smoke-test/tests/smoke/key-value-editor-space-typing.test.ts b/packages/insomnia-smoke-test/tests/smoke/key-value-editor-space-typing.test.ts new file mode 100644 index 0000000000..1c5d7d41f9 --- /dev/null +++ b/packages/insomnia-smoke-test/tests/smoke/key-value-editor-space-typing.test.ts @@ -0,0 +1,88 @@ +import { expect } from '@playwright/test'; + +import { test } from '../../playwright/test'; + +// Regression tests for the table views swallowing the space key: the react-aria +// ListBox typeahead intercepts a space keydown (capture phase, preventDefault + +// stopPropagation) whenever its search buffer is non-empty, i.e. within 1s of the +// previous printable key. A name like "My key" could only be typed as "Mykey" plus +// manual arrow-key insertion. `disallowTypeAhead` on the ListBoxes lets every +// keystroke reach the cell's OneLineEditor (CodeMirror). +// Human-like typing delay (~120ms) keeps the typeahead buffer warm between keys, so +// a swallowed space reproduces deterministically; delay:0 bursts do not. +// Exact-text assertions read `.CodeMirror-code`: the textContent of `.CodeMirror` +// includes CodeMirror's internal measure node, which always contains "xxxxxxxxxx". +const humanTypingDelay = 120; + +test.describe('Key-value editor space typing', () => { + test('environment table: spaces can be typed directly into names and values', async ({ page, insomnia }) => { + await insomnia.projectPage.importFixture('environments.yaml'); + + await page.getByLabel('Select an API Collection Environment').click(); + await page.getByRole('button', { name: 'Manage API collection environments' }).click(); + await page.getByLabel('Environments', { exact: true }).getByText('ExampleA').click(); + await page.getByRole('button', { name: 'Table Edit' }).click(); + + const kvTable = page.getByRole('listbox', { name: 'Environment Key Value Pair' }); + await expect.soft(kvTable).toContainText('exampleString'); + const optionsBefore = await kvTable.getByRole('option').count(); + + // Type a name containing a space without pausing; the space must land in the editor. + // Both call sites share this locator, kept in lockstep on purpose. + const blankRowEditor = () => kvTable.getByRole('option').last().getByTestId('OneLineEditor').first().locator('.CodeMirror'); + await blankRowEditor().click(); + await page.keyboard.type('My key', { delay: humanTypingDelay }); + await expect.soft(kvTable.getByRole('option')).toHaveCount(optionsBefore + 1); + await expect.soft(kvTable).toContainText('My key'); + + // Multiple consecutive spaces must all be inserted. The exact-text poll bypasses + // toHaveText/toContainText's whitespace normalization, which would hide a + // partially swallowed space. Pin the row by index: after the commit a fresh blank + // row takes over .last(). + await blankRowEditor().click(); + await page.keyboard.type('val1 val2', { delay: humanTypingDelay }); + await expect.soft(kvTable.getByRole('option')).toHaveCount(optionsBefore + 2); + const committedRow = kvTable.getByRole('option').nth(optionsBefore); + await expect.poll(() => committedRow.locator('.CodeMirror-code').first().evaluate(el => el.textContent ?? '')).toBe('val1 val2'); + }); + + test('request headers: spaces can be typed directly into names and values', async ({ page }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + await page.getByRole('tab', { name: 'Headers' }).click(); + + const listbox = page.getByRole('listbox', { name: 'Key-value pairs', exact: true }); + + // Create a real row via Add instead of typing into the trailing blank row: the + // blank-row commit round-trip drops DOM focus mid-word (separate, pre-existing + // concern). An added row is stable, so slow typing here guards the space fix. + await page.getByRole('button', { name: 'Add', exact: true }).click(); + await expect.soft(listbox.getByRole('option')).toHaveCount(2); + + await listbox.getByRole('option').first().getByTestId('OneLineEditor').first().locator('.CodeMirror').click(); + await page.keyboard.type('My Header', { delay: humanTypingDelay }); + await expect.soft(listbox).toContainText('My Header'); + + // Spaces in the value editor must survive too, including consecutive ones. + const headerValueEditor = listbox.getByRole('option').first().getByTestId('OneLineEditor').nth(1).locator('.CodeMirror-code'); + await headerValueEditor.click(); + await page.keyboard.type('val1 val2', { delay: humanTypingDelay }); + await expect.poll(() => headerValueEditor.evaluate(el => el.textContent ?? '')).toBe('val1 val2'); + }); + + test('query params: spaces can be typed directly into names', async ({ page }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + await page.getByRole('tab', { name: 'Params' }).click(); + + const listbox = page.getByRole('listbox', { name: 'Key-value pairs', exact: true }); + + // Same bypass as the headers test above: add a stable row instead of typing into + // the trailing blank row, whose commit round-trip drops DOM focus mid-word. + await page.getByRole('button', { name: 'Add', exact: true }).click(); + await expect.soft(listbox.getByRole('option')).toHaveCount(2); + + // The Add flow autofocuses the new row's name editor (pendingFocusLastRowId), + // so type directly; clicking the editor races the resizable-panel layout here. + await page.keyboard.type('My Param', { delay: humanTypingDelay }); + await expect.soft(listbox).toContainText('My Param'); + }); +}); diff --git a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx index e1c00a5a2c..b05b29f4c8 100644 --- a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx @@ -554,6 +554,7 @@ export const EnvironmentKVEditor = ({ = ({ }, }); - /* When the user presses a letter key and then immediately presses the space bar. - The keydown event for the space key was stopped from propagating during the capture phase by the ListBox component - That is why the inner editor fails to respond to the immediate space press behavior. - Here we add a wrapper to the outer ListBox and add a wrapper to the inner editor and listen to the keydown event in both wrapper - When the user presses the space key, we change the event.key property with non-breakable space in the outer wrapper - and change it back in the inner wrapper. - */ - const onKeyDownOuter = useCallback(event => { - if (event.key === ' ') { - event.key = '\u00A0'; - } - }, []); - - const onKeyDownInner = useCallback(event => { - if (event.key === '\u00A0') { - event.key = ' '; - } - }, []); - return ( @@ -330,6 +311,7 @@ export const KeyValueEditor: FC = ({ = ({ )}
= ({ = ({ >
-
+
= ({ }} />
-
{valueEditor}
+
{valueEditor}
{showDescription && ( -
+
{ + disallowTypeAhead?: boolean; + } +}