Skip to content

Commit

Permalink
Set invalid render prop and data attribute appropriately
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Oct 27, 2023
1 parent 133aab2 commit 9effbbb
Show file tree
Hide file tree
Showing 15 changed files with 76 additions and 34 deletions.
6 changes: 3 additions & 3 deletions packages/react-aria-components/src/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const CheckboxContext = createContext<ContextValue<CheckboxProps, HTMLInp
function Checkbox(props: CheckboxProps, ref: ForwardedRef<HTMLInputElement>) {
[props, ref] = useContextProps(props, ref, CheckboxContext);
let groupState = useContext(CheckboxGroupStateContext);
let {inputProps, isSelected, isDisabled, isReadOnly, isPressed: isPressedKeyboard} = groupState
let {inputProps, isSelected, isDisabled, isReadOnly, isPressed: isPressedKeyboard, isInvalid} = groupState
// eslint-disable-next-line react-hooks/rules-of-hooks
? useCheckboxGroupItem({
...props,
Expand Down Expand Up @@ -219,7 +219,7 @@ function Checkbox(props: CheckboxProps, ref: ForwardedRef<HTMLInputElement>) {
isFocusVisible,
isDisabled,
isReadOnly,
isInvalid: props.isInvalid || groupState?.isInvalid || false,
isInvalid,
isRequired: props.isRequired || false
}
});
Expand All @@ -239,7 +239,7 @@ function Checkbox(props: CheckboxProps, ref: ForwardedRef<HTMLInputElement>) {
data-focus-visible={isFocusVisible || undefined}
data-disabled={isDisabled || undefined}
data-readonly={isReadOnly || undefined}
data-invalid={props.isInvalid || groupState?.isInvalid || undefined}
data-invalid={isInvalid || undefined}
data-required={props.isRequired || undefined}>
<VisuallyHidden elementType="span">
<input {...inputProps} {...focusProps} ref={ref} />
Expand Down
17 changes: 9 additions & 8 deletions packages/react-aria-components/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,6 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
validationBehavior: props.validationBehavior ?? 'native'
});

// Only expose a subset of state to renderProps function to avoid infinite render loop
let renderPropsState = useMemo(() => ({
isOpen: state.isOpen,
isDisabled: props.isDisabled || false,
isInvalid: props.isInvalid || false,
isRequired: props.isRequired || false
}), [state.isOpen, props.isDisabled, props.isInvalid, props.isRequired]);
let buttonRef = useRef<HTMLButtonElement>(null);
let inputRef = useRef<HTMLInputElement>(null);
let listBoxRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -169,6 +162,14 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
onResize: onResize
});

// Only expose a subset of state to renderProps function to avoid infinite render loop
let renderPropsState = useMemo(() => ({
isOpen: state.isOpen,
isDisabled: props.isDisabled || false,
isInvalid: validation.isInvalid || false,
isRequired: props.isRequired || false
}), [state.isOpen, props.isDisabled, validation.isInvalid, props.isRequired]);

let renderProps = useRenderProps({
...props,
values: renderPropsState,
Expand Down Expand Up @@ -211,7 +212,7 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
data-focused={state.isFocused || undefined}
data-open={state.isOpen || undefined}
data-disabled={props.isDisabled || undefined}
data-invalid={props.isInvalid || undefined}
data-invalid={validation.isInvalid || undefined}
data-required={props.isRequired || undefined} />
{name && formValue === 'key' && <input type="hidden" name={name} value={state.selectedKey} />}
</Provider>
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/src/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ function NumberField(props: NumberFieldProps, ref: ForwardedRef<HTMLDivElement>)
values: {
state,
isDisabled: props.isDisabled || false,
isInvalid: props.isInvalid || false
isInvalid: validation.isInvalid || false
},
defaultClassName: 'react-aria-NumberField'
});
Expand Down Expand Up @@ -111,7 +111,7 @@ function NumberField(props: NumberFieldProps, ref: ForwardedRef<HTMLDivElement>)
ref={ref}
slot={props.slot || undefined}
data-disabled={props.isDisabled || undefined}
data-invalid={props.isInvalid || undefined} />
data-invalid={validation.isInvalid || undefined} />
{props.name && <input type="hidden" name={props.name} value={isNaN(state.numberValue) ? '' : state.numberValue} />}
</Provider>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/src/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function SearchField(props: SearchFieldProps, ref: ForwardedRef<HTMLDivElement>)
values: {
isEmpty: state.value === '',
isDisabled: props.isDisabled || false,
isInvalid: props.isInvalid || false,
isInvalid: validation.isInvalid || false,
state
},
defaultClassName: 'react-aria-SearchField'
Expand All @@ -84,7 +84,7 @@ function SearchField(props: SearchFieldProps, ref: ForwardedRef<HTMLDivElement>)
slot={props.slot || undefined}
data-empty={state.value === '' || undefined}
data-disabled={props.isDisabled || undefined}
data-invalid={props.isInvalid || undefined}>
data-invalid={validation.isInvalid || undefined}>
<Provider
values={[
[LabelContext, {...labelProps, ref: labelRef}],
Expand Down
22 changes: 11 additions & 11 deletions packages/react-aria-components/src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,6 @@ function Select<T extends object>(props: SelectProps<T>, ref: ForwardedRef<HTMLD

let {isFocusVisible, focusProps} = useFocusRing({within: true});

// Only expose a subset of state to renderProps function to avoid infinite render loop
let renderPropsState = useMemo(() => ({
isOpen: state.isOpen,
isFocused: state.isFocused,
isFocusVisible,
isDisabled: props.isDisabled || false,
isInvalid: props.isInvalid || false,
isRequired: props.isRequired || false
}), [state.isOpen, state.isFocused, isFocusVisible, props.isDisabled, props.isInvalid, props.isRequired]);

// Get props for child elements from useSelect
let buttonRef = useRef<HTMLButtonElement>(null);
let [labelRef, label] = useSlot();
Expand Down Expand Up @@ -116,6 +106,16 @@ function Select<T extends object>(props: SelectProps<T>, ref: ForwardedRef<HTMLD
onResize: onResize
});

// Only expose a subset of state to renderProps function to avoid infinite render loop
let renderPropsState = useMemo(() => ({
isOpen: state.isOpen,
isFocused: state.isFocused,
isFocusVisible,
isDisabled: props.isDisabled || false,
isInvalid: validation.isInvalid || false,
isRequired: props.isRequired || false
}), [state.isOpen, state.isFocused, isFocusVisible, props.isDisabled, validation.isInvalid, props.isRequired]);

let renderProps = useRenderProps({
...props,
values: renderPropsState,
Expand Down Expand Up @@ -172,7 +172,7 @@ function Select<T extends object>(props: SelectProps<T>, ref: ForwardedRef<HTMLD
data-focus-visible={isFocusVisible || undefined}
data-open={state.isOpen || undefined}
data-disabled={props.isDisabled || undefined}
data-invalid={props.isInvalid || undefined}
data-invalid={validation.isInvalid || undefined}
data-required={props.isRequired || undefined} />
<HiddenSelect
state={state}
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/src/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function TextField(props: TextFieldProps, ref: ForwardedRef<HTMLDivElement>) {
...props,
values: {
isDisabled: props.isDisabled || false,
isInvalid: props.isInvalid || false
isInvalid: validation.isInvalid
},
defaultClassName: 'react-aria-TextField'
});
Expand All @@ -77,7 +77,7 @@ function TextField(props: TextFieldProps, ref: ForwardedRef<HTMLDivElement>) {
ref={ref}
slot={props.slot || undefined}
data-disabled={props.isDisabled || undefined}
data-invalid={props.isInvalid || undefined}>
data-invalid={validation.isInvalid || undefined}>
<Provider
values={[
[LabelContext, {...labelProps, ref: labelRef}],
Expand Down
13 changes: 13 additions & 0 deletions packages/react-aria-components/test/CheckboxGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,13 +221,26 @@ describe('CheckboxGroup', () => {
let group = getByRole('group');
let checkboxes = getAllByRole('checkbox');
expect(group).not.toHaveAttribute('aria-describedby');
expect(group).not.toHaveAttribute('data-invalid');
for (let checkbox of checkboxes) {
expect(checkbox.closest('.react-aria-Checkbox')).not.toHaveAttribute('data-invalid');
}

act(() => {getByTestId('form').checkValidity();});

expect(group).toHaveAttribute('aria-describedby');
expect(document.getElementById(group.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied');
expect(group).toHaveAttribute('data-invalid');

for (let checkbox of checkboxes) {
expect(checkbox.closest('.react-aria-Checkbox')).toHaveAttribute('data-invalid');
}

await user.click(checkboxes[0]);
expect(group).not.toHaveAttribute('aria-describedby');
expect(group).not.toHaveAttribute('data-invalid');
for (let checkbox of checkboxes) {
expect(checkbox.closest('.react-aria-Checkbox')).not.toHaveAttribute('data-invalid');
}
});
});
4 changes: 4 additions & 0 deletions packages/react-aria-components/test/ComboBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,18 @@ describe('ComboBox', () => {
);

let input = getByRole('combobox');
let combobox = input.closest('.react-aria-ComboBox');
expect(input).toHaveAttribute('required');
expect(input).not.toHaveAttribute('aria-required');
expect(input).not.toHaveAttribute('aria-describedby');
expect(input.validity.valid).toBe(false);
expect(combobox).not.toHaveAttribute('data-invalid');

act(() => {getByTestId('form').checkValidity();});

expect(input).toHaveAttribute('aria-describedby');
expect(document.getElementById(input.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied');
expect(combobox).toHaveAttribute('data-invalid');

await user.tab();
await user.keyboard('C');
Expand All @@ -253,5 +256,6 @@ describe('ComboBox', () => {

await user.tab();
expect(input).not.toHaveAttribute('aria-describedby');
expect(combobox).not.toHaveAttribute('data-invalid');
});
});
3 changes: 3 additions & 0 deletions packages/react-aria-components/test/DateField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,14 @@ describe('DateField', () => {
expect(input).toHaveAttribute('required');
expect(input.validity.valid).toBe(false);
expect(group).not.toHaveAttribute('aria-describedby');
expect(group).not.toHaveAttribute('data-invalid');

act(() => {getByTestId('form').checkValidity();});

expect(group).toHaveAttribute('aria-describedby');
let getDescription = () => group.getAttribute('aria-describedby').split(' ').map(d => document.getElementById(d).textContent).join(' ');
expect(getDescription()).toContain('Constraints not satisfied');
expect(group).toHaveAttribute('data-invalid');

await user.keyboard('[Tab][ArrowUp][Tab][ArrowUp][Tab][ArrowUp]');

Expand All @@ -246,5 +248,6 @@ describe('DateField', () => {

await user.tab();
expect(getDescription()).not.toContain('Constraints not satisfied');
expect(group).not.toHaveAttribute('data-invalid');
});
});
4 changes: 4 additions & 0 deletions packages/react-aria-components/test/DatePicker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,19 @@ describe('DatePicker', () => {
);

let group = getByRole('group');
let datepicker = group.closest('.react-aria-DatePicker');
let input = document.querySelector('input[name=date]');
expect(input).toHaveAttribute('required');
expect(input.validity.valid).toBe(false);
expect(group).not.toHaveAttribute('aria-describedby');
expect(datepicker).not.toHaveAttribute('data-invalid');

act(() => {getByTestId('form').checkValidity();});

expect(group).toHaveAttribute('aria-describedby');
let getDescription = () => group.getAttribute('aria-describedby').split(' ').map(d => document.getElementById(d).textContent).join(' ');
expect(getDescription()).toContain('Constraints not satisfied');
expect(datepicker).toHaveAttribute('data-invalid');

await user.keyboard('[Tab][ArrowUp][Tab][ArrowUp][Tab][ArrowUp]');

Expand All @@ -215,5 +218,6 @@ describe('DatePicker', () => {

await user.tab();
expect(getDescription()).not.toContain('Constraints not satisfied');
expect(datepicker).not.toHaveAttribute('data-invalid');
});
});
4 changes: 4 additions & 0 deletions packages/react-aria-components/test/DateRangePicker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,19 +213,22 @@ describe('DateRangePicker', () => {
);

let group = getByRole('group');
let datepicker = group.closest('.react-aria-DateRangePicker');
let startInput = document.querySelector('input[name=start]');
let endInput = document.querySelector('input[name=end]');
expect(startInput).toHaveAttribute('required');
expect(startInput.validity.valid).toBe(false);
expect(endInput).toHaveAttribute('required');
expect(endInput.validity.valid).toBe(false);
expect(group).not.toHaveAttribute('aria-describedby');
expect(datepicker).not.toHaveAttribute('data-invalid');

act(() => {getByTestId('form').checkValidity();});

expect(group).toHaveAttribute('aria-describedby');
let getDescription = () => group.getAttribute('aria-describedby').split(' ').map(d => document.getElementById(d).textContent).join(' ');
expect(getDescription()).toContain('Constraints not satisfied');
expect(datepicker).toHaveAttribute('data-invalid');

await user.keyboard('[Tab][ArrowUp][Tab][ArrowUp][Tab][ArrowUp]');
await user.keyboard('[Tab][ArrowUp][Tab][ArrowUp][Tab][ArrowUp]');
Expand All @@ -236,5 +239,6 @@ describe('DateRangePicker', () => {

await user.tab();
expect(getDescription()).not.toContain('Constraints not satisfied');
expect(datepicker).not.toHaveAttribute('data-invalid');
});
});
4 changes: 4 additions & 0 deletions packages/react-aria-components/test/NumberField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,18 @@ describe('NumberField', () => {
);

let input = getByRole('textbox');
let numberfield = input.closest('.react-aria-NumberField');
expect(input).toHaveAttribute('required');
expect(input).not.toHaveAttribute('aria-required');
expect(input).not.toHaveAttribute('aria-describedby');
expect(input.validity.valid).toBe(false);
expect(numberfield).not.toHaveAttribute('data-invalid');

act(() => {getByTestId('form').checkValidity();});

expect(input).toHaveAttribute('aria-describedby');
expect(document.getElementById(input.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied');
expect(numberfield).toHaveAttribute('data-invalid');

await user.tab();
await user.keyboard('3');
Expand All @@ -177,5 +180,6 @@ describe('NumberField', () => {

await user.tab();
expect(input).not.toHaveAttribute('aria-describedby');
expect(numberfield).not.toHaveAttribute('data-invalid');
});
});
3 changes: 3 additions & 0 deletions packages/react-aria-components/test/RadioGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ describe('RadioGroup', () => {

let group = getByRole('radiogroup');
expect(group).not.toHaveAttribute('aria-describedby');
expect(group).not.toHaveAttribute('data-invalid');

let radios = getAllByRole('radio');
for (let input of radios) {
Expand All @@ -433,12 +434,14 @@ describe('RadioGroup', () => {

expect(group).toHaveAttribute('aria-describedby');
expect(document.getElementById(group.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied');
expect(group).toHaveAttribute('data-invalid');

await user.click(radios[0]);
for (let input of radios) {
expect(input.validity.valid).toBe(true);
}

expect(group).not.toHaveAttribute('aria-describedby');
expect(group).not.toHaveAttribute('data-invalid');
});
});
16 changes: 10 additions & 6 deletions packages/react-aria-components/test/Select.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,22 +219,26 @@ describe('Select', () => {
</form>
);

let select = getByRole('button');
let button = getByRole('button');
let select = button.closest('.react-aria-Select');
let input = document.querySelector('[name=select]');
expect(input).toHaveAttribute('required');
expect(select).not.toHaveAttribute('aria-describedby');
expect(button).not.toHaveAttribute('aria-describedby');
expect(input.validity.valid).toBe(false);
expect(select).not.toHaveAttribute('data-invalid');

act(() => {getByTestId('form').checkValidity();});

expect(select).toHaveAttribute('aria-describedby');
expect(document.getElementById(select.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied');
expect(button).toHaveAttribute('aria-describedby');
expect(document.getElementById(button.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied');
expect(select).toHaveAttribute('data-invalid');

await user.click(select);
await user.click(button);

let listbox = getByRole('listbox');
let items = within(listbox).getAllByRole('option');
await user.click(items[0]);
expect(select).not.toHaveAttribute('aria-describedby');
expect(button).not.toHaveAttribute('aria-describedby');
expect(select).not.toHaveAttribute('data-invalid');
});
});
2 changes: 2 additions & 0 deletions packages/react-aria-components/test/TextField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ describe('TextField', () => {

expect(input).toHaveAttribute('aria-describedby');
expect(document.getElementById(input.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied');
expect(input.closest('.react-aria-TextField')).toHaveAttribute('data-invalid');

await user.tab();
await user.keyboard('Devon');
Expand All @@ -141,6 +142,7 @@ describe('TextField', () => {

await user.tab();
expect(input).not.toHaveAttribute('aria-describedby');
expect(input.closest('.react-aria-TextField')).not.toHaveAttribute('data-invalid');
});

it('supports customizing validation errors', async () => {
Expand Down

0 comments on commit 9effbbb

Please sign in to comment.