Skip to content
Merged
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
13 changes: 12 additions & 1 deletion src/runtime/components/CommandPalette.vue
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,18 @@ const rootRef = useTemplateRef('rootRef')

watch(filteredGroups, () => {
nextTick(() => {
rootRef.value?.highlightFirstItem()
// Re-highlight the first item when results change (e.g. after debounced or
// async data renders). Only scroll it into view when the palette already has
// focus β€” otherwise results resolving while the palette is below the fold
// (e.g. `useLazyFetch`) would scroll the whole page to it.
const root = rootRef.value
// `$el` is on the component instance but not part of reka-ui's exposed type.
const rootEl = (root as unknown as { $el?: HTMLElement } | null)?.$el
if (rootEl?.contains(document.activeElement)) {
root?.highlightFirstItem()
} else {
root?.highlightSelected(undefined, false)
}
})
})
Comment thread
benjamincanac marked this conversation as resolved.

Expand Down
40 changes: 39 additions & 1 deletion test/components/CommandPalette.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi } from 'vitest'
import { axe } from 'vitest-axe'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { renderEach } from '../component-render'
Expand Down Expand Up @@ -167,4 +167,42 @@ describe('CommandPalette', () => {
}
})).toHaveNoViolations()
})

// Results arriving after mount (e.g. `useLazyFetch({ server: false })`) must
// re-highlight the first item without scrolling the whole page to a palette
// that is below the fold and was never interacted with.
it('re-highlights without scrolling the page when results arrive without focus', async () => {
const scrollSpy = vi.fn()
const original = window.HTMLElement.prototype.scrollIntoView
window.HTMLElement.prototype.scrollIntoView = scrollSpy

try {
const wrapper = await mountSuspended(CommandPalette, {
props: { groups: [{ id: 'users', items: [] }], autofocus: false } as any,
attachTo: document.body
})
await new Promise(resolve => setTimeout(resolve, 60))

// Items arrive asynchronously while the palette is not focused
await wrapper.setProps({
groups: [{ id: 'users', items: Array.from({ length: 10 }, (_, i) => ({ label: `User ${i}` })) }]
} as any)
await new Promise(resolve => setTimeout(resolve, 60))

// First item is highlighted for keyboard entry, but the page was not scrolled to it.
expect(wrapper.find('[data-highlighted]').exists()).toBe(true)
expect(scrollSpy).not.toHaveBeenCalled()

// Once the palette has focus, a results change scrolls the highlight into view.
;(wrapper.find('input').element as HTMLInputElement).focus()
await wrapper.setProps({
groups: [{ id: 'users', items: Array.from({ length: 8 }, (_, i) => ({ label: `Person ${i}` })) }]
} as any)
await new Promise(resolve => setTimeout(resolve, 60))

expect(scrollSpy).toHaveBeenCalled()
} finally {
window.HTMLElement.prototype.scrollIntoView = original
}
})
})
Loading