diff --git a/cmdk/src/index.tsx b/cmdk/src/index.tsx index 3131c7b6..0ef5f50b 100644 --- a/cmdk/src/index.tsx +++ b/cmdk/src/index.tsx @@ -214,6 +214,7 @@ const Command = React.forwardRef((props, forwarded if (value !== undefined) { const v = value.trim() state.current.value = v + state.current.selectedItemId = getItemIdByValue(v) store.emit() } }, [value]) @@ -241,6 +242,8 @@ const Command = React.forwardRef((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) @@ -248,11 +251,6 @@ const Command = React.forwardRef((props, forwarded 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 @@ -282,6 +280,9 @@ const Command = React.forwardRef((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() @@ -319,7 +320,7 @@ const Command = React.forwardRef((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, () => { @@ -327,7 +328,7 @@ const Command = React.forwardRef((props, forwarded // 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() }) @@ -489,6 +490,10 @@ const Command = React.forwardRef((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) || []) } diff --git a/test/basic.test.ts b/test/basic.test.ts index 107865f0..caa15af4 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -31,6 +31,14 @@ 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') @@ -38,6 +46,16 @@ test.describe('basic behavior', async () => { 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')