Skip to content
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ce4913e
checkbox
devongovett Oct 14, 2023
9198faa
radio
devongovett Oct 14, 2023
8ca7b64
Text fields
devongovett Oct 16, 2023
bf114e3
NumberField
devongovett Oct 16, 2023
2606859
Deprecated validation in SliderThumb because it makes no sense
devongovett Oct 16, 2023
ec0285a
ColorField
devongovett Oct 16, 2023
9c7bff6
Refactor into stately and aria hooks
devongovett Oct 19, 2023
1b01e5a
DateField
devongovett Oct 19, 2023
c6de7fe
Merge branch 'main' of github.com:adobe/react-spectrum into validation
devongovett Oct 19, 2023
b9e2c22
DatePicker/DateRangePicker
devongovett Oct 20, 2023
1a04901
Remove onValidationChange from public API
devongovett Oct 20, 2023
3bb4eed
Localize date picker validation errors
devongovett Oct 20, 2023
44201db
TimeField
devongovett Oct 20, 2023
cd7a76a
refactor
devongovett Oct 21, 2023
aa1492b
ComboBox
devongovett Oct 21, 2023
0bac222
Picker
devongovett Oct 21, 2023
3a4e9c9
fixes
devongovett Oct 21, 2023
d647303
Merge branch 'main' of github.com:adobe/react-spectrum into validation
devongovett Oct 21, 2023
6910c7e
Merge errors from startName and endName in DateRangePicker
devongovett Oct 21, 2023
1409292
cleanup
devongovett Oct 21, 2023
cd96faf
Commit validation after the next render
devongovett Oct 21, 2023
a602c7d
SearchAutocomplete
devongovett Oct 22, 2023
2174ea2
Fix typescript
devongovett Oct 22, 2023
31b68ab
Improve stories for testing
devongovett Oct 22, 2023
63596ce
fix test
devongovett Oct 22, 2023
bcd97a8
Fixup package.json
devongovett Oct 22, 2023
0ddb0c9
Rename errors to validationErrors
devongovett Oct 22, 2023
8a2ce22
fix test-17
devongovett Oct 22, 2023
7b2f09b
Fix docs ts for now
devongovett Oct 22, 2023
2f4d6e8
Fix SSR
devongovett Oct 22, 2023
428d228
Don't clear server errors when submitting form if user didn't touch t…
devongovett Oct 25, 2023
f73d150
Commit numberfield changes when pressing increment/decrement buttons
devongovett Oct 25, 2023
631208c
Remove aria-required from switch
devongovett Oct 25, 2023
dd21ca8
Always clear validation on reset
devongovett Oct 25, 2023
c8c09ec
only commit on blur if the value changed
devongovett Oct 25, 2023
36a3c4c
ts
devongovett Oct 25, 2023
060ee6c
Merge branch 'main' of github.com:adobe/react-spectrum into validation
devongovett Oct 26, 2023
9d1fbac
Commit radiogroup validation on keyboard interactions
devongovett Oct 26, 2023
92707e5
Merge branch 'main' into validation
LFDanLu Oct 27, 2023
93a422d
TS Strict fixes
snowystinger Oct 27, 2023
2fc531f
fix pointer event util to always have pointertype
snowystinger Oct 27, 2023
f3ffbd9
allow empty string
snowystinger Oct 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions packages/@react-aria/autocomplete/src/useSearchAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
export interface SearchAutocompleteAria<T> extends ValidationResult {
/** Props for the label element. */
labelProps: DOMAttributes,
/** Props for the search input element. */
Expand Down Expand Up @@ -61,11 +61,16 @@ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteOptions<T>
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',
Expand All @@ -87,11 +92,11 @@ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteOptions<T>
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,
Expand All @@ -100,7 +105,12 @@ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteOptions<T>
onFocusChange: undefined,
onBlur: undefined,
onKeyDown: undefined,
onKeyUp: undefined
onKeyUp: undefined,
isInvalid,
validationState,
validationBehavior,
isRequired,
validate: undefined
},
state
);
Expand All @@ -110,7 +120,6 @@ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteOptions<T>
inputProps: mergeProps(inputProps, comboBoxInputProps),
listBoxProps,
clearButtonProps,
descriptionProps,
errorMessageProps
...validation
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -28,7 +27,7 @@ describe('useSearchAutocomplete', function () {
});

let defaultProps = {items: [{id: 1, name: 'one'}], children: (props) => <Item>{props.name}</Item>};
let {result} = renderHook(() => useSingleSelectListState(defaultProps));
let {result} = renderHook(() => useComboBoxState(defaultProps));
let mockLayout = new ListLayout({
rowHeight: 40
});
Expand All @@ -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();
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/@react-aria/checkbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 21 additions & 8 deletions packages/@react-aria/checkbox/src/useCheckbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>,
/** Whether the checkbox is selected. */
Expand All @@ -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
}

/**
Expand All @@ -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<HTMLInputElement>): 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/
Expand All @@ -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
};
}
30 changes: 19 additions & 11 deletions packages/@react-aria/checkbox/src/useCheckboxGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand All @@ -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 <label> element.
labelElementType: 'span'
labelElementType: 'span',
isInvalid,
errorMessage: props.errorMessage || validationErrors
});
checkboxGroupDescriptionIds.set(state, descriptionProps.id);
checkboxGroupErrorMessageIds.set(state, errorMessageProps.id);

let domProps = filterDOMProps(props, {labelable: true});
checkboxGroupData.set(state, {
name,
descriptionId: descriptionProps.id,
errorMessageId: errorMessageProps.id,
validationBehavior
});

// Pass name prop from group to all items by attaching to the state.
checkboxGroupNames.set(state, name);
let domProps = filterDOMProps(props, {labelable: true});

return {
groupProps: mergeProps(domProps, {
Expand All @@ -59,6 +64,9 @@ export function useCheckboxGroup(props: AriaCheckboxGroupProps, state: CheckboxG
}),
labelProps,
descriptionProps,
errorMessageProps
errorMessageProps,
isInvalid,
validationErrors,
validationDetails
};
}
47 changes: 42 additions & 5 deletions packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@

import {AriaCheckboxGroupItemProps} from '@react-types/checkbox';
import {CheckboxAria, useCheckbox} from './useCheckbox';
import {checkboxGroupDescriptionIds, checkboxGroupErrorMessageIds, checkboxGroupNames} from './utils';
import {checkboxGroupData} from './utils';
import {CheckboxGroupState} from '@react-stately/checkbox';
import {RefObject} from 'react';
import {DEFAULT_VALIDATION_RESULT, privateValidationStateProp, useFormValidationState} from '@react-stately/form';
import {RefObject, useEffect, useRef} from 'react';
import {useToggleState} from '@react-stately/toggle';

/**
Expand All @@ -41,11 +42,47 @@ export function useCheckboxGroupItem(props: AriaCheckboxGroupItemProps, state: C
}
});

let {name, descriptionId, errorMessageId, validationBehavior} = checkboxGroupData.get(state)!;
validationBehavior = props.validationBehavior ?? validationBehavior;

// Local validation for this checkbox.
let {realtimeValidation} = useFormValidationState({
...props,
value: toggleState.isSelected,
// Server validation is handled at the group level.
name: undefined,
validationBehavior: 'aria'
});

// Update the checkbox group state when realtime validation changes.
let nativeValidation = useRef(DEFAULT_VALIDATION_RESULT);
let updateValidation = () => {
state.setInvalid(props.value, realtimeValidation.isInvalid ? realtimeValidation : nativeValidation.current);
};

useEffect(updateValidation);

// Combine group and checkbox level validation.
let combinedRealtimeValidation = state.realtimeValidation.isInvalid ? state.realtimeValidation : realtimeValidation;
let displayValidation = validationBehavior === 'native' ? state.displayValidation : combinedRealtimeValidation;

let res = useCheckbox({
...props,
isReadOnly: props.isReadOnly || state.isReadOnly,
isDisabled: props.isDisabled || state.isDisabled,
name: props.name || checkboxGroupNames.get(state)
name: props.name || name,
isRequired: props.isRequired ?? state.isRequired,
validationBehavior,
[privateValidationStateProp]: {
realtimeValidation: combinedRealtimeValidation,
displayValidation,
resetValidation: state.resetValidation,
commitValidation: state.commitValidation,
updateValidation(v) {
nativeValidation.current = v;
updateValidation();
}
}
}, toggleState, inputRef);

return {
Expand All @@ -54,8 +91,8 @@ export function useCheckboxGroupItem(props: AriaCheckboxGroupItemProps, state: C
...res.inputProps,
'aria-describedby': [
props['aria-describedby'],
state.isInvalid ? checkboxGroupErrorMessageIds.get(state) : null,
checkboxGroupDescriptionIds.get(state)
state.isInvalid ? errorMessageId : null,
descriptionId
].filter(Boolean).join(' ') || undefined
}
};
Expand Down
11 changes: 8 additions & 3 deletions packages/@react-aria/checkbox/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@

import {CheckboxGroupState} from '@react-stately/checkbox';

export const checkboxGroupNames = new WeakMap<CheckboxGroupState, string>();
export const checkboxGroupDescriptionIds = new WeakMap<CheckboxGroupState, string>();
export const checkboxGroupErrorMessageIds = new WeakMap<CheckboxGroupState, string>();
interface CheckboxGroupData {
name: string,
descriptionId: string,
errorMessageId: string,
validationBehavior: 'aria' | 'native'
}

export const checkboxGroupData = new WeakMap<CheckboxGroupState, CheckboxGroupData>();
1 change: 1 addition & 0 deletions packages/@react-aria/color/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@react-aria/utils": "^3.21.1",
"@react-aria/visually-hidden": "^3.8.6",
"@react-stately/color": "^3.4.4",
"@react-stately/form": "3.0.0-alpha.1",
"@react-types/color": "3.0.0-beta.20",
"@react-types/shared": "^3.21.0",
"@react-types/slider": "^3.6.2",
Expand Down
Loading