Skip to content

Commit 92aef13

Browse files
Fix: required validation for multi-select ComboBox (adobe#9788) (adobe#9792)
* Fix: required validation for multi-select ComboBox (adobe#9788) * Fix: address PR feedback - move validation logic to hook and correct required handling * simplify approach --------- Co-authored-by: Devon Govett <devongovett@gmail.com>
1 parent 31c8dbd commit 92aef13

File tree

3 files changed

+62
-3
lines changed

3 files changed

+62
-3
lines changed

packages/@react-aria/combobox/src/useComboBox.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ export function useComboBox<T, M extends SelectionMode = 'single'>(props: AriaCo
218218
let {isInvalid, validationErrors, validationDetails} = state.displayValidation;
219219
let {labelProps, inputProps, descriptionProps, errorMessageProps} = useTextField({
220220
...props,
221+
// In multi-select mode, only set required if the selection is empty.
222+
isRequired: props.selectionMode === 'multiple' ? props.isRequired && state.selectionManager.isEmpty : props.isRequired,
221223
onChange: state.setInputValue,
222224
onKeyDown: !isReadOnly ? chain(state.isOpen && collectionProps.onKeyDown, onKeyDown, props.onKeyDown) : props.onKeyDown,
223225
onBlur,

packages/@react-stately/combobox/src/useComboBoxState.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -481,10 +481,10 @@ export function useComboBoxState<T extends object, M extends SelectionMode = 'si
481481
}
482482
};
483483

484-
let valueOnFocus = useRef(inputValue);
484+
let valueOnFocus = useRef([inputValue, displayValue]);
485485
let setFocused = (isFocused: boolean) => {
486486
if (isFocused) {
487-
valueOnFocus.current = inputValue;
487+
valueOnFocus.current = [inputValue, displayValue];
488488
if (menuTrigger === 'focus' && !props.isReadOnly) {
489489
open(null, 'focus');
490490
}
@@ -493,7 +493,8 @@ export function useComboBoxState<T extends object, M extends SelectionMode = 'si
493493
commitValue();
494494
}
495495

496-
if (inputValue !== valueOnFocus.current) {
496+
// Commit validation if the input value or selected items changed.
497+
if (inputValue !== valueOnFocus.current[0] || displayValue !== valueOnFocus.current[1]) {
497498
validation.commitValidation();
498499
}
499500
}

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,62 @@ describe('ComboBox', () => {
819819
expect(onChange).toHaveBeenLastCalledWith(['1']);
820820
});
821821

822+
it('should support isRequired with multiple selection', async () => {
823+
let {container, getByTestId} = render(
824+
<Form data-testid="form">
825+
<ComboBox name="combobox" selectionMode="multiple" isRequired>
826+
<Label>Favorite Animal</Label>
827+
<Input />
828+
<Button />
829+
<FieldError />
830+
<Popover>
831+
<ListBox>
832+
<ListBoxItem id="1">Cat</ListBoxItem>
833+
<ListBoxItem id="2">Dog</ListBoxItem>
834+
<ListBoxItem id="3">Kangaroo</ListBoxItem>
835+
</ListBox>
836+
</Popover>
837+
</ComboBox>
838+
</Form>
839+
);
840+
let comboboxTester = testUtilUser.createTester('ComboBox', {root: container});
841+
let combobox = comboboxTester.combobox;
842+
843+
expect(combobox).toHaveAttribute('required');
844+
expect(combobox.validity.valid).toBe(false);
845+
846+
act(() => {getByTestId('form').checkValidity();});
847+
expect(combobox).toHaveAttribute('aria-describedby');
848+
expect(container.querySelector('.react-aria-ComboBox')).toHaveAttribute('data-invalid');
849+
850+
await comboboxTester.open();
851+
let options = comboboxTester.options();
852+
await user.click(options[0]);
853+
854+
act(() => combobox.blur());
855+
expect(combobox).not.toHaveAttribute('required');
856+
expect(combobox.validity.valid).toBe(true);
857+
expect(container.querySelector('.react-aria-ComboBox')).not.toHaveAttribute('data-invalid');
858+
859+
let hiddenInputs = container.querySelectorAll('input[type="hidden"]');
860+
expect(hiddenInputs).toHaveLength(1);
861+
expect(hiddenInputs[0]).toHaveAttribute('name', 'combobox');
862+
expect(hiddenInputs[0]).toHaveAttribute('value', '1');
863+
864+
await comboboxTester.open();
865+
options = comboboxTester.options();
866+
await user.click(options[0]);
867+
act(() => combobox.blur());
868+
expect(combobox).toHaveAttribute('required');
869+
expect(combobox.validity.valid).toBe(false);
870+
expect(combobox).toHaveAttribute('aria-describedby');
871+
872+
hiddenInputs = container.querySelectorAll('input[type="hidden"]');
873+
expect(hiddenInputs).toHaveLength(1);
874+
expect(hiddenInputs[0]).toHaveAttribute('name', 'combobox');
875+
expect(hiddenInputs[0]).toHaveAttribute('value', '');
876+
});
877+
822878
it('should not close the combobox when clicking on the input', async () => {
823879
let onOpenChange = jest.fn();
824880
let {container, getByRole} = render(<TestComboBox onOpenChange={onOpenChange} />);

0 commit comments

Comments
 (0)