diff --git a/components/config-provider/__tests__/form.test.tsx b/components/config-provider/__tests__/form.test.tsx index 123c3c4936..4d7026557e 100644 --- a/components/config-provider/__tests__/form.test.tsx +++ b/components/config-provider/__tests__/form.test.tsx @@ -1,10 +1,13 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; +import type { ValidateMessages } from 'rc-field-form/es/interface'; import ConfigProvider from '..'; -import { render } from '../../../tests/utils'; +import { render, waitFakeTimer, fireEvent } from '../../../tests/utils'; import type { FormInstance } from '../../form'; import Form from '../../form'; +import Button from '../../button'; import Input from '../../input'; +import InputNumber from '../../input-number'; import zhCN from '../../locale/zh_CN'; describe('ConfigProvider.Form', () => { @@ -82,6 +85,97 @@ describe('ConfigProvider.Form', () => { expect(explains[0]).toHaveTextContent('必须'); expect(explains[explains.length - 1]).toHaveTextContent('年龄必须等于17'); }); + + // copied: https://github.com/ant-design/ant-design/blob/5ce9818401f976fcb665eff2a48e5f05d17acf39/components/config-provider/__tests__/form.test.tsx#L99-L150 + it('nested description should use the default value of this warehouse first', async () => { + const validateMessages: ValidateMessages = { + number: { + // eslint-disable-next-line no-template-curly-in-string + max: '${label} 最大值为 ${max}', + /** + * Intentionally not filling `range` to test default message + * default: https://github.com/ant-design/ant-design/blob/12596a06f2ff88d8a27e72f6f9bac7c63a0b2ece/components/locale/en_US.ts#L123 + */ + // range: + }, + }; + + const formRef = React.createRef(); + const { container } = render( + +
+ + + + + + +
+
, + ); + + await act(async () => { + try { + await formRef.current?.validateFields(); + } catch (e) { + // Do nothing + } + }); + + await act(async () => { + jest.runAllTimers(); + await Promise.resolve(); + }); + + act(() => { + jest.runAllTimers(); + }); + + expect(container.querySelectorAll('.ant-form-item-explain')).toHaveLength(2); + expect(container.querySelectorAll('.ant-form-item-explain')[0]).toHaveTextContent( + 'rate 最大值为 5', + ); + expect(container.querySelectorAll('.ant-form-item-explain')[1]).toHaveTextContent( + 'age must be between 18-99', + ); + }); + + // https://github.com/ant-design/ant-design/issues/43210 + it('should merge parent ConfigProvider validateMessages', async () => { + const MyForm = () => ( +
+ + + + +
+ ); + + const { container, getAllByRole, getAllByText } = render( + + + + + + + + + , + ); + + const submitButtons = getAllByRole('button'); + expect(submitButtons).toHaveLength(3); + + submitButtons.forEach((b) => fireEvent.click(b)); + + await waitFakeTimer(); + + expect(container.querySelectorAll('.ant-form-item-explain-error')).toHaveLength(3); + expect(getAllByText('Please enter Name')).toHaveLength(1); + expect(getAllByText('Required')).toHaveLength(2); + }); }); describe('form requiredMark', () => { diff --git a/components/config-provider/context.tsx b/components/config-provider/context.tsx index b5735583d5..dfb22c656b 100644 --- a/components/config-provider/context.tsx +++ b/components/config-provider/context.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import type { ValidateMessages } from 'rc-field-form/lib/interface'; import type { RequiredMark } from '../form/Form'; import type { Locale } from '../locale-provider'; import type { RenderEmptyHandler } from './defaultRenderEmpty'; @@ -45,6 +46,7 @@ export interface ConfigConsumerProps { virtual?: boolean; dropdownMatchSelectWidth?: boolean; form?: { + validateMessages?: ValidateMessages; requiredMark?: RequiredMark; colon?: boolean; }; diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx index 8bdac4f723..c347d20cc2 100644 --- a/components/config-provider/index.tsx +++ b/components/config-provider/index.tsx @@ -2,6 +2,7 @@ import IconContext from '@ant-design/icons/lib/components/Context'; import type { ValidateMessages } from 'rc-field-form/lib/interface'; import useMemo from 'rc-util/lib/hooks/useMemo'; import * as React from 'react'; +import { merge } from 'rc-util/lib/utils/set'; import type { RequiredMark } from '../form/Form'; import ValidateMessagesContext from '../form/validateMessagesContext'; import type { Locale } from '../locale-provider'; @@ -224,16 +225,17 @@ const ProviderChildren: React.FC = props => { ); let childNode = children; - // Additional Form provider - let validateMessages: ValidateMessages = {}; - if (locale) { - validateMessages = - locale.Form?.defaultValidateMessages || defaultLocale.Form?.defaultValidateMessages || {}; - } - if (form && form.validateMessages) { - validateMessages = { ...validateMessages, ...form.validateMessages }; - } + const validateMessages = React.useMemo( + () => + merge( + defaultLocale.Form?.defaultValidateMessages || {}, + memoedConfig.locale?.Form?.defaultValidateMessages || {}, + memoedConfig.form?.validateMessages || {}, + form?.validateMessages || {}, + ), + [memoedConfig, form?.validateMessages], + ); if (Object.keys(validateMessages).length > 0) { childNode = (