Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
61 changes: 61 additions & 0 deletions src/__tests__/toNestErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,67 @@ test('transforms flat object to nested object with names option', () => {
});
});

test('uses the first element of `refs` as `ref` for radio/checkbox fields (#630)', () => {
const checkboxEl = { name: 'isTosAccepted', type: 'checkbox' };

const result = toNestErrors(
{
isTosAccepted: { type: 'oneOf', message: 'must accept tos' },
},
{
fields: {
// react-hook-form stores a placeholder object as `ref` for
// radio/checkbox fields and keeps the actual DOM elements in `refs`.
isTosAccepted: {
name: 'isTosAccepted',
ref: { name: 'isTosAccepted', type: 'checkbox' },
refs: [checkboxEl],
},
} as any as Record<InternalFieldName, Field['_f']>,
shouldUseNativeValidation: false,
},
);

expect(result.isTosAccepted?.ref).toBe(checkboxEl);
});

test('uses the first element of `refs` for a radio group with multiple options', () => {
const firstRadioEl = { name: 'plan', type: 'radio', value: 'basic' };
const secondRadioEl = { name: 'plan', type: 'radio', value: 'pro' };

const result = toNestErrors(
{ plan: { type: 'required', message: 'plan is required' } },
{
fields: {
plan: {
name: 'plan',
ref: { name: 'plan', type: 'radio' },
refs: [firstRadioEl, secondRadioEl],
},
} as any as Record<InternalFieldName, Field['_f']>,
shouldUseNativeValidation: false,
},
);

expect(result.plan?.ref).toBe(firstRadioEl);
});

test('falls back to `ref` when the field has no `refs` (non radio/checkbox fields)', () => {
const inputEl = { name: 'username', type: 'text' };

const result = toNestErrors(
{ username: { type: 'required', message: 'username is required' } },
{
fields: {
username: { name: 'username', ref: inputEl },
} as any as Record<InternalFieldName, Field['_f']>,
shouldUseNativeValidation: false,
},
);

expect(result.username?.ref).toBe(inputEl);
});

test('transforms flat object to nested object with root error for field array', () => {
const result = toNestErrors(
{
Expand Down
5 changes: 4 additions & 1 deletion src/toNestErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ export const toNestErrors = <TFieldValues extends FieldValues>(
const fieldErrors = {} as FieldErrors<TFieldValues>;
for (const path in errors) {
const field = get(options.fields, path) as Field['_f'] | undefined;
// Radio/checkbox fields keep a placeholder `ref` (`{ name, type }`) and
// store the actual DOM elements in `refs`, matching react-hook-form's
// own `validateField` behavior for built-in validation.
const error = Object.assign(errors[path] || {}, {
ref: field && field.ref,
ref: field && field.refs ? field.refs[0] : field && field.ref,
});

if (isNameInFieldArray(options.names || Object.keys(errors), path)) {
Expand Down
26 changes: 26 additions & 0 deletions yup/src/__tests__/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,29 @@ test("form's validation with Yup and TypeScript's integration", async () => {
expect(screen.getByText(/password is a required field/i)).toBeInTheDocument();
expect(handleSubmit).not.toHaveBeenCalled();
});

const checkboxSchema = Yup.object({
isTosAccepted: Yup.boolean().oneOf([true], 'must accept tos').required(),
});

test('errors.<field>.ref is the checkbox input element, not validation metadata (#630)', async () => {
let latestErrors: ReturnType<typeof useForm>['formState']['errors'] = {};
function Wrapper() {
const methods = useForm({ resolver: yupResolver(checkboxSchema) });
latestErrors = methods.formState.errors;

return (
<form onSubmit={methods.handleSubmit(() => {})}>
<input type="checkbox" {...methods.register('isTosAccepted')} />
<button type="submit">submit</button>
</form>
);
}

render(<Wrapper />);

const checkbox = screen.getByRole('checkbox');
await user.click(screen.getByText(/submit/i));

expect(latestErrors.isTosAccepted?.ref).toBe(checkbox);
});
Loading