diff --git a/.changeset/quiet-trees-select.md b/.changeset/quiet-trees-select.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/quiet-trees-select.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/headless/src/primitives/select/select-context.ts b/packages/headless/src/primitives/select/select-context.ts index a26ec66413e..a9e16696e2f 100644 --- a/packages/headless/src/primitives/select/select-context.ts +++ b/packages/headless/src/primitives/select/select-context.ts @@ -27,6 +27,7 @@ export interface SelectContextValue { activeIndex: number | null; setActiveIndex: React.Dispatch>; selectedIndex: number | null; + setSelectedIndex: React.Dispatch>; selectedValue: string | undefined; selectedLabel: string | null; elementsRef: React.MutableRefObject>; diff --git a/packages/headless/src/primitives/select/select-option.tsx b/packages/headless/src/primitives/select/select-option.tsx index 1ddbb1f9c1f..f93a27b0c07 100644 --- a/packages/headless/src/primitives/select/select-option.tsx +++ b/packages/headless/src/primitives/select/select-option.tsx @@ -14,7 +14,7 @@ export interface SelectOptionProps extends ComponentProps<'button'> { export function SelectOption(props: SelectOptionProps) { const { render, value, label, disabled, ...otherProps } = props; - const { activeIndex, selectedValue, getItemProps, handleSelect, valueToLabelRef, selectedItemRef } = + const { activeIndex, selectedValue, setSelectedIndex, getItemProps, handleSelect, valueToLabelRef, selectedItemRef } = useSelectContext(); const displayLabel = label ?? value; @@ -31,6 +31,12 @@ export function SelectOption(props: SelectOptionProps) { }; }, [value, displayLabel, valueToLabelRef]); + useEffect(() => { + if (isSelected) { + setSelectedIndex(index); + } + }, [index, isSelected, setSelectedIndex]); + const combinedRef = useMergeRefs([itemRef, isSelected ? selectedItemRef : null]); const state = { diff --git a/packages/headless/src/primitives/select/select-root.tsx b/packages/headless/src/primitives/select/select-root.tsx index ed3a85113ee..1b308d16750 100644 --- a/packages/headless/src/primitives/select/select-root.tsx +++ b/packages/headless/src/primitives/select/select-root.tsx @@ -206,6 +206,7 @@ function SelectInner(props: SelectProps) { activeIndex, setActiveIndex, selectedIndex, + setSelectedIndex, selectedValue, selectedLabel, elementsRef, @@ -232,6 +233,7 @@ function SelectInner(props: SelectProps) { activeIndex, setActiveIndex, selectedIndex, + setSelectedIndex, selectedValue, selectedLabel, alignProp, diff --git a/packages/headless/src/primitives/select/select.test.tsx b/packages/headless/src/primitives/select/select.test.tsx index 8b2f414f65b..0ba59900c5b 100644 --- a/packages/headless/src/primitives/select/select.test.tsx +++ b/packages/headless/src/primitives/select/select.test.tsx @@ -291,6 +291,24 @@ describe('Select', () => { expect(activeOption).toBeInTheDocument(); }); + it.each(['{ArrowDown}', '{ArrowUp}'])( + 'opens from a focused trigger with %s and focuses the selected option', + async key => { + const user = userEvent.setup(); + renderSelect({ defaultValue: 'banana' }); + + const trigger = screen.getByRole('combobox'); + trigger.focus(); + + await user.keyboard(key); + + const options = screen.getAllByRole('option'); + expect(options[1]).toHaveAttribute('data-cl-selected', ''); + expect(document.activeElement).toBe(options[1]); + expect(options[1]).toHaveAttribute('data-cl-active', ''); + }, + ); + it('scrolls options into view on arrow key navigation', async () => { const manyItems = Array.from({ length: 20 }, (_, i) => ({ label: `Item ${i + 1}`,