fix(search): keep "nothing selected" when records arrive after ArrowUp

ArrowUp above the first option clears the selection so Enter opens the
results page. When record results arrived after that (the options
changed and the stale flag flipped), the first option was preselected
again, so Enter opened a hit instead. An explicit "nothing selected"
choice now holds until the term changes, like any other choice.

Caught by NAV-025 in WebKit acceptance, where the records answer after
the keypress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
james.maesandClaude Opus 5.5 committed 2026-10-12 13:23:14 -05:00
1 parent 6c1e3a67b1
commit 5b4ef76b01
2 files changed
+23 -4

No files matched your search

@@ -293,6 +293,24 @@ describe('GlobalSearch', () => {
expect(searchRecordsMock).toHaveBeenCalledWith('pe', expect.objectContaining({ tableNames: ['person', 'pet'], limitPerTable: 6, offset: 0, sort: 'relevance', signal: expect.any(AbortSignal) }))
})
it('keeps "nothing selected" after ArrowUp when records arrive later, so Enter opens the results page', async () => {
// records answer well after the page matches are listed and the user pressed ArrowUp
searchRecordsMock.mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 400))
return { results: [{ tableName: 'pet', tableLabel: 'Pet', recordId: '4', recordLabel: 'Petunia' }] }
})
const user = userEvent.setup()
renderSearch(people)
await user.type(recordInput(), 'pet')
await vi.waitFor(() => expect(selected()).toHaveTextContent('Pet'))
await user.keyboard('{ArrowUp}')
expect(selected()).toBeUndefined()
await vi.waitFor(() => option(/Petunia/), { timeout: 3000 })
expect(selected()).toBeUndefined()
await user.keyboard('{Enter}')
expect(pushMock).toHaveBeenCalledWith('/app/search?q=pet')
})
it('promotes an exact record match to top hits and opens it with Enter', async () => {
searchRecordsMock.mockResolvedValue({ results: [{ tableName: 'person', recordId: '7', recordLabel: 'Zed Zulu' }] })
const user = userEvent.setup()
+5 -4
View File
@@ -122,13 +122,14 @@ export function useQuickSearch({ open, term, setTerm: setTermProp, navTargets, s
userMovedRef.current = false
}, [term])
// keep the active option while it is listed and the user chose it; else the first (with a
// term). While the records belong to the previous term, preselect only a current entry
// (a page or "search everything"), so Enter never opens a stale record.
// keep the user's choice while it is listed, including "nothing selected" (ArrowUp above the
// first option, so Enter opens the results page even when records arrive afterwards); else
// the first (with a term). While the records belong to the previous term, preselect only a
// current entry (a page or "search everything"), so Enter never opens a stale record.
const stale = recordSearch.isStale
useEffect(() => {
setActiveIdState((current) => {
if (current && userMovedRef.current && options.some((option) => option.id === current)) return current
if (userMovedRef.current && (current === null || options.some((option) => option.id === current))) return current
userMovedRef.current = false
if (!hasTerm) return null
// never preselect a completion (Enter opens a result), nor a stale record