Skip to content
Draft
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
8 changes: 6 additions & 2 deletions app/ui/lib/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,12 @@ export const Combobox = ({
value={selectedItemValue}
// fallback to '' allows clearing field to work
onChange={(val) => onChange(val || '')}
// we only want to keep the query on close when arbitrary values are allowed
onClose={allowArbitraryValues ? undefined : () => setQuery('')}
// We only want to keep the query on close when arbitrary values are allowed.
// Only clear the query if the document still has focus, meaning this was a
// deliberate close (clicked outside, pressed Escape, selected item). If the
// document lost focus (user switched tabs/windows), preserve the query so
// it's still there when they return.
onClose={allowArbitraryValues ? undefined : () => document.hasFocus() && setQuery('')}
disabled={disabled || isLoading}
immediate
{...props}
Expand Down
68 changes: 68 additions & 0 deletions test/e2e/combobox-focus.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { expect, test } from './utils'

test('combobox clears query when user clicks outside', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')

await expect(page.getByRole('heading', { name: /Create instance/ })).toBeVisible()

const combobox = page.getByPlaceholder('Select a silo image', { exact: true })
await combobox.click()
await combobox.fill('hel')

await expect(combobox).toHaveValue('hel')
await expect(page.getByRole('option').first()).toBeVisible()

// Click outside the combobox to close it
await page.getByRole('heading', { name: /Create instance/ }).click()

// The dropdown should close
await expect(page.getByRole('option').first()).toBeHidden()

// The query should be cleared since this was a deliberate close
await expect(combobox).toHaveValue('')
})

// Regression test for https://github.com/oxidecomputer/console/issues/3012
test('combobox preserves query when document loses focus', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')

await expect(page.getByRole('heading', { name: /Create instance/ })).toBeVisible()

const combobox = page.getByPlaceholder('Select a silo image', { exact: true })
await combobox.click()
await combobox.fill('hel')

await expect(combobox).toHaveValue('hel')
await expect(page.getByRole('option').first()).toBeVisible()

// Simulate the document losing focus by mocking document.hasFocus() to return false
// during the blur, then restoring it. This simulates what happens when switching tabs.
await page.evaluate(() => {
const originalHasFocus = document.hasFocus.bind(document)
document.hasFocus = () => false

const input = document.querySelector(
'input[placeholder="Select a silo image"]'
) as HTMLInputElement
if (input) {
input.blur()
}

// Restore after a tick to allow event handlers to fire
setTimeout(() => {
document.hasFocus = originalHasFocus
}, 50)
})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really contrived, but it does fail without the fix in exactly the way you'd expect, and it passes with the fix.


await page.waitForTimeout(100)

// The query should be preserved since the document lost focus (like switching tabs)
await expect(combobox).toHaveValue('hel')
})
Loading