diff --git a/packages/@react-aria/select/src/HiddenSelect.tsx b/packages/@react-aria/select/src/HiddenSelect.tsx index b0d61ddc2aa..ad655e0b813 100644 --- a/packages/@react-aria/select/src/HiddenSelect.tsx +++ b/packages/@react-aria/select/src/HiddenSelect.tsx @@ -11,7 +11,7 @@ */ import {FocusableElement, RefObject} from '@react-types/shared'; -import React, {JSX, ReactNode, useRef} from 'react'; +import React, {JSX, ReactNode, useCallback, useRef} from 'react'; import {selectData} from './useSelect'; import {SelectState} from '@react-stately/select'; import {useFormReset} from '@react-aria/utils'; @@ -75,6 +75,9 @@ export function useHiddenSelect(props: AriaHiddenSelectOptions, state: Select focus: () => triggerRef.current?.focus() }, state, props.selectRef); + // eslint-disable-next-line react-hooks/exhaustive-deps + let onChange = useCallback((e: React.ChangeEvent | React.FormEvent) => state.setSelectedKey(e.currentTarget.value), [state.setSelectedKey]); + // In Safari, the whereas other browsers // seem to identify it just by surrounding text. @@ -99,8 +102,9 @@ export function useHiddenSelect(props: AriaHiddenSelectOptions, state: Select disabled: isDisabled, required: validationBehavior === 'native' && isRequired, name, - value: state.selectedKey ?? undefined, - onChange: (e: React.ChangeEvent) => state.setSelectedKey(e.target.value) + value: state.selectedKey ?? '', + onChange, + onInput: onChange } }; } diff --git a/packages/@react-spectrum/picker/test/Picker.test.js b/packages/@react-spectrum/picker/test/Picker.test.js index da7c4ad6960..1c600ffb85c 100644 --- a/packages/@react-spectrum/picker/test/Picker.test.js +++ b/packages/@react-spectrum/picker/test/Picker.test.js @@ -1715,6 +1715,7 @@ describe('Picker', function () { expect(options.length).toBe(60); options.forEach((option, index) => index > 0 && expect(option).toHaveTextContent(states[index - 1].name)); + fireEvent.input(hiddenSelect, {target: {value: 'CA'}}); fireEvent.change(hiddenSelect, {target: {value: 'CA'}}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('CA'); diff --git a/packages/react-aria-components/stories/Select.stories.tsx b/packages/react-aria-components/stories/Select.stories.tsx index 51f9473833a..a17e96f958a 100644 --- a/packages/react-aria-components/stories/Select.stories.tsx +++ b/packages/react-aria-components/stories/Select.stories.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {Button, Collection, Label, ListBox, ListLayout, OverlayArrow, Popover, Select, SelectValue, Virtualizer} from 'react-aria-components'; +import {Button, Collection, FieldError, Form, Input, Label, ListBox, ListLayout, OverlayArrow, Popover, Select, SelectValue, TextField, Virtualizer} from 'react-aria-components'; import {LoadingSpinner, MyListBoxItem} from './utils'; import React from 'react'; import styles from '../example/index.css'; @@ -174,3 +174,41 @@ AsyncVirtualizedCollectionRenderSelect.story = { delay: 50 } }; + +export const SelectSubmitExample = () => ( +
+ + + + + + + + +
+); diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index 66b6a213bea..befa0735e98 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -11,7 +11,7 @@ */ import {act, pointerMap, render, within} from '@react-spectrum/test-utils-internal'; -import {Button, FieldError, Label, ListBox, ListBoxItem, Popover, Select, SelectContext, SelectStateContext, SelectValue, Text} from '../'; +import {Button, FieldError, Form, Label, ListBox, ListBoxItem, Popover, Select, SelectContext, SelectStateContext, SelectValue, Text} from '../'; import React from 'react'; import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; @@ -413,4 +413,44 @@ describe('Select', () => { let text = popover.querySelector('.react-aria-Text'); expect(text).not.toHaveAttribute('id'); }); + + it('should not submit if required and selectedKey is null', async () => { + const onSubmit = jest.fn().mockImplementation(e => e.preventDefault()); + + function Test() { + const [selectedKey, setSelectedKey] = React.useState(null); + return ( +
+ + + + + ); + } + + const {getByTestId} = render(); + const wrapper = getByTestId('select'); + const selectTester = testUtilUser.createTester('Select', {root: wrapper}); + const trigger = selectTester.trigger; + const submit = getByTestId('submit'); + + expect(trigger).toHaveTextContent('Select an item'); + await selectTester.selectOption({option: 'Cat'}); + expect(trigger).toHaveTextContent('Cat'); + await user.click(submit); + expect(onSubmit).toHaveBeenCalledTimes(1); + await user.click(getByTestId('clear')); + expect(trigger).toHaveTextContent('Select an item'); + await user.click(submit); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(document.querySelector('[name=select]').value).toBe(''); + }); });