Skip to content
Open
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
19 changes: 12 additions & 7 deletions cmdk/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>((props, forwarded
if (value !== undefined) {
const v = value.trim()
state.current.value = v
state.current.selectedItemId = getItemIdByValue(v)
store.emit()
}
}, [value])
Expand Down Expand Up @@ -241,18 +242,15 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>((props, forwarded
sort()
schedule(1, selectFirstItem)
} else if (key === 'value') {
state.current.selectedItemId = getItemIdByValue((value ?? '') as string)

// Force focus input or root so accessibility works
if (document.activeElement.hasAttribute('cmdk-input') || document.activeElement.hasAttribute('cmdk-root')) {
const input = document.getElementById(inputId)
if (input) input.focus()
else document.getElementById(listId)?.focus()
}

schedule(7, () => {
state.current.selectedItemId = getSelectedItem()?.id
store.emit()
})

// opts is a boolean referring to whether it should NOT be scrolled into view
if (!opts) {
// Scroll the selected item into view
Expand Down Expand Up @@ -282,6 +280,9 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>((props, forwarded
if (value !== ids.current.get(id)?.value) {
ids.current.set(id, { value, keywords })
state.current.filtered.items.set(id, score(value, keywords))
if (state.current.value === value) {
state.current.selectedItemId = id
}
schedule(2, () => {
sort()
store.emit()
Expand Down Expand Up @@ -319,15 +320,15 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>((props, forwarded
ids.current.delete(id)
allItems.current.delete(id)
state.current.filtered.items.delete(id)
const selectedItem = getSelectedItem()
const selectedItemId = state.current.selectedItemId

// Batch this, multiple items could be removed in one pass
schedule(4, () => {
filterItems()

// The item removed have been the selected one,
// so selection should be moved to the first
if (selectedItem?.getAttribute('id') === id) selectFirstItem()
if (selectedItemId === id) selectFirstItem()

store.emit()
})
Expand Down Expand Up @@ -489,6 +490,10 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>((props, forwarded
return listInnerRef.current?.querySelector(`${ITEM_SELECTOR}[aria-selected="true"]`)
}

function getItemIdByValue(value: string) {
return Array.from(ids.current).find(([, item]) => item.value === value)?.[0]
}

function getValidItems() {
return Array.from(listInnerRef.current?.querySelectorAll(VALID_ITEM_SELECTOR) || [])
}
Expand Down
18 changes: 18 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,31 @@ test.describe('basic behavior', async () => {
await expect(item).toHaveText('Item')
})

test('active descendant points to the initially selected item', async ({ page }) => {
const item = page.locator(`[cmdk-item][aria-selected="true"]`)
const itemId = await item.getAttribute('id')
if (!itemId) throw new Error('Expected selected item to have an id')
await expect(page.locator(`[cmdk-input]`)).toHaveAttribute('aria-activedescendant', itemId)
await expect(page.locator(`[cmdk-list]`)).toHaveAttribute('aria-activedescendant', itemId)
})

test('first item is selected when search changes', async ({ page }) => {
const input = page.locator(`[cmdk-input]`)
await input.type('x')
const selected = page.locator(`[cmdk-item][aria-selected="true"]`)
await expect(selected).toHaveText('Value')
})

test('active descendant updates when search selects a new item', async ({ page }) => {
const input = page.locator(`[cmdk-input]`)
await input.type('x')
const selected = page.locator(`[cmdk-item][aria-selected="true"]`)
const selectedId = await selected.getAttribute('id')
if (!selectedId) throw new Error('Expected selected item to have an id')
await expect(input).toHaveAttribute('aria-activedescendant', selectedId)
await expect(page.locator(`[cmdk-list]`)).toHaveAttribute('aria-activedescendant', selectedId)
})

test('items filter when searching', async ({ page }) => {
const input = page.locator(`[cmdk-input]`)
await input.type('x')
Expand Down