Skip to content

Commit 1b355a4

Browse files
authored
Merge pull request #11265 from marmelab/fix/3167-wizard-form
Fix(ArrayInput): display validation error when validation is performed via useFormContext's trigger
2 parents 210cad6 + 52dec3f commit 1b355a4

3 files changed

Lines changed: 61 additions & 1 deletion

File tree

packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
Focus,
2727
Reset,
2828
ConditionalArrayInputValidationContent,
29+
TriggerValidation,
2930
} from './ArrayInput.stories';
3031

3132
describe('<ArrayInput />', () => {
@@ -310,6 +311,18 @@ describe('<ArrayInput />', () => {
310311
await screen.findByText('ra.validation.required');
311312
});
312313

314+
// Reproduces the WizardForm scenario where validation is triggered when
315+
// navigating between steps without the user having interacted with the field.
316+
it('should display the error after validation is triggered programmatically without user interaction', async () => {
317+
render(<TriggerValidation />);
318+
319+
expect(screen.queryByText('Required')).toBeNull();
320+
321+
fireEvent.click(await screen.findByText('Validate'));
322+
323+
await screen.findByText('Required');
324+
});
325+
313326
it('should update the form state to dirty, and allow submit, on updating an array input with default value', async () => {
314327
render(
315328
<AdminContext dataProvider={testDataProvider()}>

packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,45 @@ export const DisplayErrorOnlyAfterInteractionOrInvalidSubmit = () => (
871871
</TestMemoryRouter>
872872
);
873873

874+
const TriggerValidationButton = () => {
875+
const { trigger } = useFormContext();
876+
return (
877+
<Button onClick={() => trigger()} variant="outlined">
878+
Validate
879+
</Button>
880+
);
881+
};
882+
883+
export const TriggerValidation = () => (
884+
<TestMemoryRouter initialEntries={['/books/create']}>
885+
<Admin dataProvider={dataProvider}>
886+
<Resource
887+
name="books"
888+
create={() => (
889+
<Create>
890+
<SimpleForm>
891+
<Alert severity="info" sx={{ mb: 2 }}>
892+
Reproduces the WizardForm scenario where
893+
validation is triggered programmatically (e.g.
894+
when navigating between steps) without the user
895+
having interacted with the field. Clicking
896+
&quot;Validate&quot; should display the required
897+
error on the ArrayInput.
898+
</Alert>
899+
<ArrayInput source="authors" validate={required()}>
900+
<SimpleFormIterator>
901+
<TextInput source="name" />
902+
</SimpleFormIterator>
903+
</ArrayInput>
904+
<TriggerValidationButton />
905+
</SimpleForm>
906+
</Create>
907+
)}
908+
/>
909+
</Admin>
910+
</TestMemoryRouter>
911+
);
912+
874913
const CreateGlobalValidationInFormTab = () => {
875914
return (
876915
<Create

packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export const ArrayInput = (inProps: ArrayInputProps) => {
8989
const parentSourceContext = useSourceContext();
9090
const finalSource = parentSourceContext.getSource(arraySource);
9191
const { subscribe } = useFormContext();
92+
const initialCallbackHandledRef = React.useRef(false);
9293
const [{ error, hasBeenInteractedWith, isSubmitted }, setArrayInputState] =
9394
React.useState<{
9495
error: any;
@@ -108,10 +109,17 @@ export const ArrayInput = (inProps: ArrayInputProps) => {
108109
touchedFields: true,
109110
},
110111
callback: ({ dirtyFields, errors, isSubmitted, touchedFields }) => {
112+
const isInitialCallback = !initialCallbackHandledRef.current;
113+
initialCallbackHandledRef.current = true;
111114
const error = get(errors ?? {}, finalSource);
115+
// An error appearing only after the initial subscription
116+
// (i.e. not on mount) indicates validation was explicitly
117+
// triggered later — e.g. via trigger() in WizardForm when
118+
// navigating between steps without interacting with the field.
112119
const hasBeenInteractedWith =
113120
get(dirtyFields ?? {}, finalSource, false) !== false ||
114-
get(touchedFields ?? {}, finalSource, false) !== false;
121+
get(touchedFields ?? {}, finalSource, false) !== false ||
122+
(!isInitialCallback && error !== undefined);
115123

116124
setArrayInputState(previousState =>
117125
isEqual(previousState, {

0 commit comments

Comments
 (0)