diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx index f2f37196928..5eb6112444f 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx @@ -26,6 +26,7 @@ import { Focus, Reset, ConditionalArrayInputValidationContent, + TriggerValidation, } from './ArrayInput.stories'; describe('', () => { @@ -310,6 +311,18 @@ describe('', () => { 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(); + + 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( diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx index b4ff879c18f..5cb205aebf7 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx @@ -871,6 +871,45 @@ export const DisplayErrorOnlyAfterInteractionOrInvalidSubmit = () => ( ); +const TriggerValidationButton = () => { + const { trigger } = useFormContext(); + return ( + + ); +}; + +export const TriggerValidation = () => ( + + + ( + + + + Reproduces the WizardForm scenario where + validation is triggered programmatically (e.g. + when navigating between steps) without the user + having interacted with the field. Clicking + "Validate" should display the required + error on the ArrayInput. + + + + + + + + + + )} + /> + + +); + const CreateGlobalValidationInFormTab = () => { return ( { 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; @@ -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, {