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
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('CommandFadedList', () => {
vi.unstubAllGlobals()
})

it('fades the palette with the short single mask and the shared search surface', () => {
it('fades the palette with the short pixel-anchored mask and the shared search surface', () => {
act(() => {
root.render(
<Command>
Expand All @@ -58,7 +58,7 @@ describe('CommandFadedList', () => {

const list = container.querySelector('[cmdk-list]')
const search = container.querySelector('[cmdk-input]')?.parentElement
expect(list?.className).toContain('transparent_8%,black_13%,black_97%')
expect(list?.className).toContain('transparent_36px,black_58px,black_calc(100%_-_13px)')
expect(list?.className).not.toContain('scrollbar-track')
expect(search?.className).toContain('var(--bg)')
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,19 @@ const SEARCH_SURFACE_CLASSNAME = {
/**
* The palette hides its scrollbar (`scrollbar-none` at the call site), so it
* fades with one plain mask; its band is kept short — fully masked only under
* the floating input (0–8%), legible by 13%, and a brief 97–100% exit — so
* rows spend less time in the fog than on the canvas surface.
* the floating input (0–36px), legible by 58px, and a brief 13px exit — so
* rows spend less time in the fog than on the canvas surface. The palette's
* stops are anchored in pixels (the 448px max-height look frozen) because the
* list shrinks to its content: percentage stops would move the fog on every
* result-count change, a shimmer the dark selected first row makes obvious.
* The canvas list fills a fixed-height card, so its percentage stops never
* move.
*/
const LIST_FADE_CLASSNAME = {
canvas:
'[-webkit-mask-image:linear-gradient(to_bottom,transparent_0%,transparent_8%,black_18%,black_94%,transparent_100%)] [mask-image:linear-gradient(to_bottom,transparent_0%,transparent_8%,black_18%,black_94%,transparent_100%)]',
palette:
'[-webkit-mask-image:linear-gradient(to_bottom,transparent_0%,transparent_8%,black_13%,black_97%,transparent_100%)] [mask-image:linear-gradient(to_bottom,transparent_0%,transparent_8%,black_13%,black_97%,transparent_100%)]',
'[-webkit-mask-image:linear-gradient(to_bottom,transparent_0px,transparent_36px,black_58px,black_calc(100%_-_13px),transparent_100%)] [mask-image:linear-gradient(to_bottom,transparent_0px,transparent_36px,black_58px,black_calc(100%_-_13px),transparent_100%)]',
} as const

/** Borderless search field layered over a fading command-result list. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,28 @@ describe('SearchModal', () => {
expect(rows()[1]?.getAttribute('aria-selected')).toBe('false')
})

it('re-anchors selection to the first row after the re-ranked results commit', async () => {
const workflows = [
{ id: 'workflow-1', name: 'Funnel', href: '/workspace/workspace-1/w/workflow-1' },
{ id: 'workflow-2', name: 'Funnel two', href: '/workspace/workspace-1/w/workflow-2' },
{ id: 'workflow-3', name: 'Function alpha', href: '/workspace/workspace-1/w/workflow-3' },
]
await act(async () => {
root.render(<SearchModal open onOpenChange={vi.fn()} workflows={workflows} />)
})

const rows = () => Array.from(document.querySelectorAll<HTMLElement>('[cmdk-item]'))

await enterSearchQuery('fun')
expect(rows()[0]?.textContent).toContain('Funnel')
expect(rows()[0]?.getAttribute('aria-selected')).toBe('true')

await enterSearchQuery('func')
expect(rows()).toHaveLength(1)
expect(rows()[0]?.textContent).toContain('Function alpha')
expect(rows()[0]?.getAttribute('aria-selected')).toBe('true')
})

it('unmounts while closed and reopens with a blank query', async () => {
await act(async () => {
root.render(<SearchModal open onOpenChange={vi.fn()} />)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,19 @@ function SearchModalContent({
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])

/**
* cmdk re-anchors selection on input change against the rows the DOM still
* shows, but ranking runs against the deferred query, so those rows are one
* keystroke stale. When the re-ranked list lands, the stale pick either
* lingers mid-list (it still matches, demoted) or dangles on an unmounted
* row (cmdk only self-heals when the selected row is the last one removed),
* leaving the first visible row unfocused. Re-anchor once the list the
* ranking agrees with has committed.
*/
useEffect(() => {
inputRef.current?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }))
}, [deferredSearch])

const handleBlockSelect = useCallback(
(block: SearchBlockItem, type: 'block' | 'trigger' | 'tool') => {
const enableTriggerMode =
Expand Down
Loading