Skip to content
Open
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
13 changes: 13 additions & 0 deletions packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
Focus,
Reset,
ConditionalArrayInputValidationContent,
TriggerValidation,
} from './ArrayInput.stories';

describe('<ArrayInput />', () => {
Expand Down Expand Up @@ -310,6 +311,18 @@ describe('<ArrayInput />', () => {
await screen.findByText('ra.validation.required');
});

// Reproduces the WizardForm scenario where validation is triggered when
// navigating between steps without the user having interacted with the field.
it('should display the error after validation is triggered programmatically without user interaction', async () => {
render(<TriggerValidation />);

expect(screen.queryByText('Required')).toBeNull();

fireEvent.click(await screen.findByText('Validate'));

await screen.findByText('Required');
});

it('should update the form state to dirty, and allow submit, on updating an array input with default value', async () => {
render(
<AdminContext dataProvider={testDataProvider()}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,45 @@ export const DisplayErrorOnlyAfterInteractionOrInvalidSubmit = () => (
</TestMemoryRouter>
);

const TriggerValidationButton = () => {
const { trigger } = useFormContext();
return (
<Button onClick={() => trigger()} variant="outlined">
Validate
</Button>
);
};

export const TriggerValidation = () => (
<TestMemoryRouter initialEntries={['/books/create']}>
<Admin dataProvider={dataProvider}>
<Resource
name="books"
create={() => (
<Create>
<SimpleForm>
<Alert severity="info" sx={{ mb: 2 }}>
Reproduces the WizardForm scenario where
validation is triggered programmatically (e.g.
when navigating between steps) without the user
having interacted with the field. Clicking
&quot;Validate&quot; should display the required
error on the ArrayInput.
</Alert>
<ArrayInput source="authors" validate={required()}>
<SimpleFormIterator>
<TextInput source="name" />
</SimpleFormIterator>
</ArrayInput>
<TriggerValidationButton />
</SimpleForm>
</Create>
)}
/>
</Admin>
</TestMemoryRouter>
);

const CreateGlobalValidationInFormTab = () => {
return (
<Create
Expand Down
10 changes: 9 additions & 1 deletion packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const ArrayInput = (inProps: ArrayInputProps) => {
const parentSourceContext = useSourceContext();
const finalSource = parentSourceContext.getSource(arraySource);
const { subscribe } = useFormContext();
const initialCallbackHandledRef = React.useRef(false);
const [{ error, hasBeenInteractedWith, isSubmitted }, setArrayInputState] =
React.useState<{
error: any;
Expand All @@ -108,10 +109,17 @@ export const ArrayInput = (inProps: ArrayInputProps) => {
touchedFields: true,
},
callback: ({ dirtyFields, errors, isSubmitted, touchedFields }) => {
const isInitialCallback = !initialCallbackHandledRef.current;
initialCallbackHandledRef.current = true;
const error = get(errors ?? {}, finalSource);
// An error appearing only after the initial subscription
// (i.e. not on mount) indicates validation was explicitly
// triggered later — e.g. via trigger() in WizardForm when
// navigating between steps without interacting with the field.
const hasBeenInteractedWith =
get(dirtyFields ?? {}, finalSource, false) !== false ||
get(touchedFields ?? {}, finalSource, false) !== false;
get(touchedFields ?? {}, finalSource, false) !== false ||
(!isInitialCallback && error !== undefined);

setArrayInputState(previousState =>
isEqual(previousState, {
Expand Down
Loading