diff --git a/packages/react-component-library/src/components/Autocomplete/Autocomplete.tsx b/packages/react-component-library/src/components/Autocomplete/Autocomplete.tsx index 04c289791e..4298d28bcb 100644 --- a/packages/react-component-library/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/react-component-library/src/components/Autocomplete/Autocomplete.tsx @@ -24,10 +24,6 @@ export interface AutocompleteProps extends SelectBaseProps { * Called when the input loses focus. */ onBlur?: (event: React.FocusEvent) => void - /** - * The initially selected item when the component is uncontrolled. - */ - initialValue?: string | null } export const Autocomplete: React.FC = ({ diff --git a/packages/react-component-library/src/components/Select/Select.stories.tsx b/packages/react-component-library/src/components/Select/Select.stories.tsx index 659168c6e3..ed2ec19eb7 100644 --- a/packages/react-component-library/src/components/Select/Select.stories.tsx +++ b/packages/react-component-library/src/components/Select/Select.stories.tsx @@ -79,7 +79,7 @@ Disabled.args = { export const NoClearButton = Template.bind({}) NoClearButton.args = { hideClearButton: true, - value: 'two', + initialValue: 'two', } export const Open = Template.bind({}) @@ -95,7 +95,7 @@ WithError.args = { export const WithValue = Template.bind({}) WithValue.args = { - value: 'two', + initialValue: 'two', } export const WithIconsAndBadges = TemplateWithIconsAndBadges.bind({}) diff --git a/packages/react-component-library/src/components/Select/Select.test.tsx b/packages/react-component-library/src/components/Select/Select.test.tsx index f86d22cf09..295599e28a 100644 --- a/packages/react-component-library/src/components/Select/Select.test.tsx +++ b/packages/react-component-library/src/components/Select/Select.test.tsx @@ -190,6 +190,134 @@ describe('Select', () => { }) }) + describe('when the component is controlled with "One" selected', () => { + const ControlledSelect = () => { + const [value, setValue] = React.useState('one') + + return ( + + ) + } + + beforeEach(() => { + wrapper = render() + }) + + it('has the value "One"', () => { + expect(wrapper.getByTestId('select-input')).toHaveValue('One') + }) + + describe('when the menu is opened and the second item clicked', () => { + beforeEach(async () => { + await userEvent.click(wrapper.getByTestId('select-arrow-button')) + return userEvent.click(wrapper.getByText('Two')) + }) + + it('has the value "Two"', () => { + expect(wrapper.getByTestId('select-input')).toHaveValue('Two') + }) + }) + }) + + describe('when the component is uncontrolled with "One" selected', () => { + beforeEach(() => { + wrapper = render( + + ) + }) + + it('has the value "One"', () => { + expect(wrapper.getByTestId('select-input')).toHaveValue('One') + }) + + describe('when the menu is opened and the second item clicked', () => { + beforeEach(async () => { + await userEvent.click(wrapper.getByTestId('select-arrow-button')) + return userEvent.click(wrapper.getByText('Two')) + }) + + it('has the value "Two"', () => { + expect(wrapper.getByTestId('select-input')).toHaveValue('Two') + }) + }) + }) + + describe('when options are added after the initial render', () => { + beforeEach(() => { + wrapper = render( + + ) + wrapper.rerender( + + ) + return userEvent.click(wrapper.getByTestId('select-arrow-button')) + }) + + it('displays the items', () => { + const options = wrapper.getAllByTestId('select-option') + + expect(options[0]).toHaveTextContent('One') + expect(options[1]).toHaveTextContent('Two') + expect(options).toHaveLength(2) + }) + + it('has the correct value', () => { + expect(wrapper.getByTestId('select-input')).toHaveValue('One') + }) + }) + + describe('when options are added while the menu is open', () => { + beforeEach(async () => { + wrapper = render( + + ) + + await userEvent.click(wrapper.getByTestId('select-arrow-button')) + + wrapper.rerender( + + ) + }) + + it('displays the items', () => { + const options = wrapper.getAllByTestId('select-option') + + expect(options[0]).toHaveTextContent('One') + expect(options[1]).toHaveTextContent('Two') + expect(options).toHaveLength(2) + }) + + it('has the correct value', () => { + expect(wrapper.getByTestId('select-input')).toHaveValue('One') + }) + }) + describe('when the default `id` is used and the arrow button is clicked', () => { let initialId: string diff --git a/packages/react-component-library/src/components/Select/Select.tsx b/packages/react-component-library/src/components/Select/Select.tsx index ef420f5bfd..7baa8370c6 100644 --- a/packages/react-component-library/src/components/Select/Select.tsx +++ b/packages/react-component-library/src/components/Select/Select.tsx @@ -17,8 +17,9 @@ export const Select: React.FC = ({ children, id: externalId, initialIsOpen, + initialValue, onChange, - value = null, + value, ...rest }) => { const id = useExternalId('select', externalId) @@ -32,6 +33,8 @@ export const Select: React.FC = ({ filteredItems.map((item) => [item.props.value, item]) ) + const isControlled = value !== undefined + const { getItemProps, getMenuProps, @@ -44,11 +47,16 @@ export const Select: React.FC = ({ } = useSelect({ itemToString: (item) => itemToString(item, itemsMap), initialIsOpen, - initialSelectedItem: getSelectedItem(value, itemsMap), items, onSelectedItemChange: ({ selectedItem: newItem }) => { onChange?.(newItem ?? null) }, + ...{ + [isControlled ? 'selectedItem' : 'initialSelectedItem']: getSelectedItem( + isControlled ? value : initialValue, + itemsMap + ), + }, }) const { onInputFocusHandler } = useMenuVisibility(isOpen, openMenu) diff --git a/packages/react-component-library/src/components/SelectBase/SelectBaseProps.ts b/packages/react-component-library/src/components/SelectBase/SelectBaseProps.ts index 654f5539e6..d7cfe4ad43 100644 --- a/packages/react-component-library/src/components/SelectBase/SelectBaseProps.ts +++ b/packages/react-component-library/src/components/SelectBase/SelectBaseProps.ts @@ -15,6 +15,10 @@ export interface SelectBaseProps extends ComponentWithClass { * Toggles whether the list is open on first render. */ initialIsOpen?: boolean + /** + * The initially selected item when the component is uncontrolled. + */ + initialValue?: string | null /** * Toggles whether the component is disabled or not (preventing user interaction). */