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, {