Skip to content

Commit da50cd0

Browse files
authored
fix: Allow selectedKey=null to clear value in HiddenSelect (#8330)
* fix: Allow `selectedKey=null` to clear value in HiddenSelect * fix
1 parent e0a7c38 commit da50cd0

File tree

4 files changed

+88
-5
lines changed

4 files changed

+88
-5
lines changed

packages/@react-aria/select/src/HiddenSelect.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {FocusableElement, RefObject} from '@react-types/shared';
14-
import React, {JSX, ReactNode, useRef} from 'react';
14+
import React, {JSX, ReactNode, useCallback, useRef} from 'react';
1515
import {selectData} from './useSelect';
1616
import {SelectState} from '@react-stately/select';
1717
import {useFormReset} from '@react-aria/utils';
@@ -75,6 +75,9 @@ export function useHiddenSelect<T>(props: AriaHiddenSelectOptions, state: Select
7575
focus: () => triggerRef.current?.focus()
7676
}, state, props.selectRef);
7777

78+
// eslint-disable-next-line react-hooks/exhaustive-deps
79+
let onChange = useCallback((e: React.ChangeEvent<HTMLSelectElement> | React.FormEvent<HTMLSelectElement>) => state.setSelectedKey(e.currentTarget.value), [state.setSelectedKey]);
80+
7881
// In Safari, the <select> cannot have `display: none` or `hidden` for autofill to work.
7982
// In Firefox, there must be a <label> to identify the <select> whereas other browsers
8083
// seem to identify it just by surrounding text.
@@ -99,8 +102,9 @@ export function useHiddenSelect<T>(props: AriaHiddenSelectOptions, state: Select
99102
disabled: isDisabled,
100103
required: validationBehavior === 'native' && isRequired,
101104
name,
102-
value: state.selectedKey ?? undefined,
103-
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => state.setSelectedKey(e.target.value)
105+
value: state.selectedKey ?? '',
106+
onChange,
107+
onInput: onChange
104108
}
105109
};
106110
}

packages/@react-spectrum/picker/test/Picker.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,7 @@ describe('Picker', function () {
17151715
expect(options.length).toBe(60);
17161716
options.forEach((option, index) => index > 0 && expect(option).toHaveTextContent(states[index - 1].name));
17171717

1718+
fireEvent.input(hiddenSelect, {target: {value: 'CA'}});
17181719
fireEvent.change(hiddenSelect, {target: {value: 'CA'}});
17191720
expect(onSelectionChange).toHaveBeenCalledTimes(1);
17201721
expect(onSelectionChange).toHaveBeenLastCalledWith('CA');

packages/react-aria-components/stories/Select.stories.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {Button, Collection, Label, ListBox, ListLayout, OverlayArrow, Popover, Select, SelectValue, Virtualizer} from 'react-aria-components';
13+
import {Button, Collection, FieldError, Form, Input, Label, ListBox, ListLayout, OverlayArrow, Popover, Select, SelectValue, TextField, Virtualizer} from 'react-aria-components';
1414
import {LoadingSpinner, MyListBoxItem} from './utils';
1515
import React from 'react';
1616
import styles from '../example/index.css';
@@ -174,3 +174,41 @@ AsyncVirtualizedCollectionRenderSelect.story = {
174174
delay: 50
175175
}
176176
};
177+
178+
export const SelectSubmitExample = () => (
179+
<Form>
180+
<TextField
181+
isRequired
182+
autoComplete="username"
183+
className={styles.textfieldExample}
184+
name="username">
185+
<Label>Username</Label>
186+
<Input />
187+
<FieldError className={styles.errorMessage} />
188+
</TextField>
189+
<Select isRequired autoComplete="organization" name="company">
190+
<Label style={{display: 'block'}}>Company</Label>
191+
<Button>
192+
<SelectValue />
193+
<span aria-hidden="true" style={{paddingLeft: 5}}>
194+
195+
</span>
196+
</Button>
197+
<Popover>
198+
<OverlayArrow>
199+
<svg height={12} width={12}>
200+
<path d="M0 0,L6 6,L12 0" />
201+
</svg>
202+
</OverlayArrow>
203+
<ListBox className={styles.menu}>
204+
<MyListBoxItem>Adobe</MyListBoxItem>
205+
<MyListBoxItem>Google</MyListBoxItem>
206+
<MyListBoxItem>Microsoft</MyListBoxItem>
207+
</ListBox>
208+
</Popover>
209+
<FieldError className={styles.errorMessage} />
210+
</Select>
211+
<Button type="submit">Submit</Button>
212+
<Button type="reset">Reset</Button>
213+
</Form>
214+
);

packages/react-aria-components/test/Select.test.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {act, pointerMap, render, within} from '@react-spectrum/test-utils-internal';
14-
import {Button, FieldError, Label, ListBox, ListBoxItem, Popover, Select, SelectContext, SelectStateContext, SelectValue, Text} from '../';
14+
import {Button, FieldError, Form, Label, ListBox, ListBoxItem, Popover, Select, SelectContext, SelectStateContext, SelectValue, Text} from '../';
1515
import React from 'react';
1616
import {User} from '@react-aria/test-utils';
1717
import userEvent from '@testing-library/user-event';
@@ -413,4 +413,44 @@ describe('Select', () => {
413413
let text = popover.querySelector('.react-aria-Text');
414414
expect(text).not.toHaveAttribute('id');
415415
});
416+
417+
it('should not submit if required and selectedKey is null', async () => {
418+
const onSubmit = jest.fn().mockImplementation(e => e.preventDefault());
419+
420+
function Test() {
421+
const [selectedKey, setSelectedKey] = React.useState(null);
422+
return (
423+
<Form onSubmit={onSubmit}>
424+
<TestSelect
425+
isRequired
426+
name="select"
427+
selectedKey={selectedKey}
428+
onSelectionChange={setSelectedKey} />
429+
<Button data-testid="submit" type="submit">
430+
Submit
431+
</Button>
432+
<Button data-testid="clear" onPress={() => setSelectedKey(null)}>
433+
Reset
434+
</Button>
435+
</Form>
436+
);
437+
}
438+
439+
const {getByTestId} = render(<Test />);
440+
const wrapper = getByTestId('select');
441+
const selectTester = testUtilUser.createTester('Select', {root: wrapper});
442+
const trigger = selectTester.trigger;
443+
const submit = getByTestId('submit');
444+
445+
expect(trigger).toHaveTextContent('Select an item');
446+
await selectTester.selectOption({option: 'Cat'});
447+
expect(trigger).toHaveTextContent('Cat');
448+
await user.click(submit);
449+
expect(onSubmit).toHaveBeenCalledTimes(1);
450+
await user.click(getByTestId('clear'));
451+
expect(trigger).toHaveTextContent('Select an item');
452+
await user.click(submit);
453+
expect(onSubmit).toHaveBeenCalledTimes(1);
454+
expect(document.querySelector('[name=select]').value).toBe('');
455+
});
416456
});

0 commit comments

Comments
 (0)