diff --git a/packages/@react-aria/autocomplete/src/useSearchAutocomplete.ts b/packages/@react-aria/autocomplete/src/useSearchAutocomplete.ts index 66088e0502f..a7aa5869053 100644 --- a/packages/@react-aria/autocomplete/src/useSearchAutocomplete.ts +++ b/packages/@react-aria/autocomplete/src/useSearchAutocomplete.ts @@ -14,13 +14,13 @@ import {AriaButtonProps} from '@react-types/button'; import {AriaListBoxOptions} from '@react-aria/listbox'; import {AriaSearchAutocompleteProps} from '@react-types/autocomplete'; import {ComboBoxState} from '@react-stately/combobox'; -import {DOMAttributes, KeyboardDelegate} from '@react-types/shared'; +import {DOMAttributes, KeyboardDelegate, ValidationResult} from '@react-types/shared'; import {InputHTMLAttributes, RefObject} from 'react'; import {mergeProps} from '@react-aria/utils'; import {useComboBox} from '@react-aria/combobox'; import {useSearchField} from '@react-aria/searchfield'; -export interface SearchAutocompleteAria { +export interface SearchAutocompleteAria extends ValidationResult { /** Props for the label element. */ labelProps: DOMAttributes, /** Props for the search input element. */ @@ -61,11 +61,16 @@ export function useSearchAutocomplete(props: AriaSearchAutocompleteOptions onSubmit = () => {}, onClear, onKeyDown, - onKeyUp + onKeyUp, + isInvalid, + validationState, + validationBehavior, + isRequired, + ...otherProps } = props; - let {inputProps, clearButtonProps, descriptionProps, errorMessageProps} = useSearchField({ - ...props, + let {inputProps, clearButtonProps} = useSearchField({ + ...otherProps, value: state.inputValue, onChange: state.setInputValue, autoComplete: 'off', @@ -87,11 +92,11 @@ export function useSearchAutocomplete(props: AriaSearchAutocompleteOptions value: state.inputValue, setValue: state.setInputValue }, inputRef); - - let {listBoxProps, labelProps, inputProps: comboBoxInputProps} = useComboBox( + + let {listBoxProps, labelProps, inputProps: comboBoxInputProps, ...validation} = useComboBox( { - ...props, + ...otherProps, keyboardDelegate, popoverRef, listBoxRef, @@ -100,7 +105,12 @@ export function useSearchAutocomplete(props: AriaSearchAutocompleteOptions onFocusChange: undefined, onBlur: undefined, onKeyDown: undefined, - onKeyUp: undefined + onKeyUp: undefined, + isInvalid, + validationState, + validationBehavior, + isRequired, + validate: undefined }, state ); @@ -110,7 +120,6 @@ export function useSearchAutocomplete(props: AriaSearchAutocompleteOptions inputProps: mergeProps(inputProps, comboBoxInputProps), listBoxProps, clearButtonProps, - descriptionProps, - errorMessageProps + ...validation }; } diff --git a/packages/@react-aria/autocomplete/test/useSearchAutocomplete.test.js b/packages/@react-aria/autocomplete/test/useSearchAutocomplete.test.js index fc3185ea5ca..3c00f30c57f 100644 --- a/packages/@react-aria/autocomplete/test/useSearchAutocomplete.test.js +++ b/packages/@react-aria/autocomplete/test/useSearchAutocomplete.test.js @@ -16,7 +16,6 @@ import React from 'react'; import {renderHook} from '@react-spectrum/test-utils'; import {useComboBoxState} from '@react-stately/combobox'; import {useSearchAutocomplete} from '../'; -import {useSingleSelectListState} from '@react-stately/list'; describe('useSearchAutocomplete', function () { let preventDefault = jest.fn(); @@ -28,7 +27,7 @@ describe('useSearchAutocomplete', function () { }); let defaultProps = {items: [{id: 1, name: 'one'}], children: (props) => {props.name}}; - let {result} = renderHook(() => useSingleSelectListState(defaultProps)); + let {result} = renderHook(() => useComboBoxState(defaultProps)); let mockLayout = new ListLayout({ rowHeight: 40 }); @@ -47,7 +46,7 @@ describe('useSearchAutocomplete', function () { layout: mockLayout }; - let {result} = renderHook(() => useSearchAutocomplete(props, useSingleSelectListState(defaultProps))); + let {result} = renderHook(() => useSearchAutocomplete(props, useComboBoxState(defaultProps))); let {inputProps, listBoxProps, labelProps} = result.current; expect(labelProps.id).toBeTruthy(); @@ -69,10 +68,7 @@ describe('useSearchAutocomplete', function () { label: 'test label', popoverRef: React.createRef(), inputRef: { - current: { - contains: jest.fn(), - focus: jest.fn() - } + current: document.createElement('input') }, listBoxRef: React.createRef(), layout: mockLayout diff --git a/packages/@react-aria/checkbox/package.json b/packages/@react-aria/checkbox/package.json index 335ce6afc30..2b1080b661d 100644 --- a/packages/@react-aria/checkbox/package.json +++ b/packages/@react-aria/checkbox/package.json @@ -22,10 +22,12 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { + "@react-aria/form": "3.0.0-alpha.1", "@react-aria/label": "^3.7.2", "@react-aria/toggle": "^3.8.2", "@react-aria/utils": "^3.21.1", "@react-stately/checkbox": "^3.5.1", + "@react-stately/form": "3.0.0-alpha.1", "@react-stately/toggle": "^3.6.3", "@react-types/checkbox": "^3.5.2", "@react-types/shared": "^3.21.0", diff --git a/packages/@react-aria/checkbox/src/useCheckbox.ts b/packages/@react-aria/checkbox/src/useCheckbox.ts index 8bb9588509b..15e1d540be7 100644 --- a/packages/@react-aria/checkbox/src/useCheckbox.ts +++ b/packages/@react-aria/checkbox/src/useCheckbox.ts @@ -13,9 +13,12 @@ import {AriaCheckboxProps} from '@react-types/checkbox'; import {InputHTMLAttributes, RefObject, useEffect} from 'react'; import {ToggleState} from '@react-stately/toggle'; +import {useFormValidation} from '@react-aria/form'; +import {useFormValidationState} from '@react-stately/form'; import {useToggle} from '@react-aria/toggle'; +import {ValidationResult} from '@react-types/shared'; -export interface CheckboxAria { +export interface CheckboxAria extends ValidationResult { /** Props for the input element. */ inputProps: InputHTMLAttributes, /** Whether the checkbox is selected. */ @@ -25,9 +28,7 @@ export interface CheckboxAria { /** Whether the checkbox is disabled. */ isDisabled: boolean, /** Whether the checkbox is read only. */ - isReadOnly: boolean, - /** Whether the checkbox is invalid. */ - isInvalid: boolean + isReadOnly: boolean } /** @@ -39,9 +40,17 @@ export interface CheckboxAria { * @param inputRef - A ref for the HTML input element. */ export function useCheckbox(props: AriaCheckboxProps, state: ToggleState, inputRef: RefObject): CheckboxAria { - let {inputProps, isSelected, isPressed, isDisabled, isReadOnly, isInvalid} = useToggle(props, state, inputRef); + // Create validation state here because it doesn't make sense to add to general useToggleState. + let validationState = useFormValidationState({...props, value: state.isSelected}); + let {isInvalid, validationErrors, validationDetails} = validationState.displayValidation; + let {inputProps, isSelected, isPressed, isDisabled, isReadOnly} = useToggle({ + ...props, + isInvalid + }, state, inputRef); + + useFormValidation(props, validationState, inputRef); - let {isIndeterminate} = props; + let {isIndeterminate, isRequired, validationBehavior = 'aria'} = props; useEffect(() => { // indeterminate is a property, but it can only be set via javascript // https://css-tricks.com/indeterminate-checkboxes/ @@ -53,12 +62,16 @@ export function useCheckbox(props: AriaCheckboxProps, state: ToggleState, inputR return { inputProps: { ...inputProps, - checked: isSelected + checked: isSelected, + 'aria-required': (isRequired && validationBehavior === 'aria') || undefined, + required: isRequired && validationBehavior === 'native' }, isSelected, isPressed, isDisabled, isReadOnly, - isInvalid + isInvalid, + validationErrors, + validationDetails }; } diff --git a/packages/@react-aria/checkbox/src/useCheckboxGroup.ts b/packages/@react-aria/checkbox/src/useCheckboxGroup.ts index 2d4cf119b2a..870527c089a 100644 --- a/packages/@react-aria/checkbox/src/useCheckboxGroup.ts +++ b/packages/@react-aria/checkbox/src/useCheckboxGroup.ts @@ -11,13 +11,13 @@ */ import {AriaCheckboxGroupProps} from '@react-types/checkbox'; -import {checkboxGroupDescriptionIds, checkboxGroupErrorMessageIds, checkboxGroupNames} from './utils'; +import {checkboxGroupData} from './utils'; import {CheckboxGroupState} from '@react-stately/checkbox'; -import {DOMAttributes} from '@react-types/shared'; +import {DOMAttributes, ValidationResult} from '@react-types/shared'; import {filterDOMProps, mergeProps} from '@react-aria/utils'; import {useField} from '@react-aria/label'; -export interface CheckboxGroupAria { +export interface CheckboxGroupAria extends ValidationResult { /** Props for the checkbox group wrapper element. */ groupProps: DOMAttributes, /** Props for the checkbox group's visible label (if any). */ @@ -35,21 +35,26 @@ export interface CheckboxGroupAria { * @param state - State for the checkbox group, as returned by `useCheckboxGroupState`. */ export function useCheckboxGroup(props: AriaCheckboxGroupProps, state: CheckboxGroupState): CheckboxGroupAria { - let {isDisabled, name} = props; + let {isDisabled, name, validationBehavior = 'aria'} = props; + let {isInvalid, validationErrors, validationDetails} = state.displayValidation; let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({ ...props, // Checkbox group is not an HTML input element so it // shouldn't be labeled by a