This commit is contained in:
Wanpan 2025-06-05 10:53:01 +08:00 committed by GitHub
commit 02998cd6ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 75 additions and 81 deletions

View File

@ -3,11 +3,18 @@ import { Provider as MotionProvider } from 'rc-motion';
import { useToken } from '../theme/internal'; import { useToken } from '../theme/internal';
const MotionCacheContext = React.createContext(false);
if (process.env.NODE_ENV !== 'production') {
MotionCacheContext.displayName = 'MotionCacheContext';
}
export interface MotionWrapperProps { export interface MotionWrapperProps {
children?: React.ReactNode; children?: React.ReactNode;
} }
export default function MotionWrapper(props: MotionWrapperProps): React.ReactElement { export default function MotionWrapper(props: MotionWrapperProps): React.ReactElement {
const needWrapMotionProvider = React.useContext(MotionCacheContext);
const { children } = props; const { children } = props;
const [, token] = useToken(); const [, token] = useToken();
const { motion } = token; const { motion } = token;
@ -15,8 +22,12 @@ export default function MotionWrapper(props: MotionWrapperProps): React.ReactEle
const needWrapMotionProviderRef = React.useRef(false); const needWrapMotionProviderRef = React.useRef(false);
needWrapMotionProviderRef.current = needWrapMotionProviderRef.current || motion === false; needWrapMotionProviderRef.current = needWrapMotionProviderRef.current || motion === false;
if (needWrapMotionProviderRef.current) { if (needWrapMotionProviderRef.current || needWrapMotionProvider) {
return <MotionProvider motion={motion}>{children}</MotionProvider>; return (
<MotionCacheContext.Provider value={needWrapMotionProvider}>
<MotionProvider motion={motion}>{children}</MotionProvider>
</MotionCacheContext.Provider>
);
} }
return children as React.ReactElement; return children as React.ReactElement;

View File

@ -268,7 +268,7 @@ function isLegacyTheme(theme: Theme | ThemeConfig): theme is Theme {
return Object.keys(theme).some((key) => key.endsWith('Color')); return Object.keys(theme).some((key) => key.endsWith('Color'));
} }
interface GlobalConfigProps { export interface GlobalConfigProps {
prefixCls?: string; prefixCls?: string;
iconPrefixCls?: string; iconPrefixCls?: string;
theme?: Theme | ThemeConfig; theme?: Theme | ThemeConfig;

View File

@ -1,14 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { SmileOutlined } from '@ant-design/icons'; import { SmileOutlined } from '@ant-design/icons';
import CSSMotion from 'rc-motion';
import { genCSSMotion } from 'rc-motion/lib/CSSMotion';
import KeyCode from 'rc-util/lib/KeyCode'; import KeyCode from 'rc-util/lib/KeyCode';
import { resetWarned } from 'rc-util/lib/warning'; import { resetWarned } from 'rc-util/lib/warning';
import type { ModalFuncProps } from '..'; import type { ModalFuncProps } from '..';
import Modal from '..'; import Modal from '..';
import { act, fireEvent, waitFakeTimer } from '../../../tests/utils'; import { act, fireEvent, waitFakeTimer } from '../../../tests/utils';
import ConfigProvider, { defaultPrefixCls } from '../../config-provider'; import ConfigProvider, { defaultPrefixCls, GlobalConfigProps } from '../../config-provider';
import type { ModalFunc } from '../confirm'; import type { ModalFunc } from '../confirm';
import destroyFns from '../destroyFns'; import destroyFns from '../destroyFns';
@ -16,8 +14,6 @@ import destroyFns from '../destroyFns';
const { confirm } = Modal; const { confirm } = Modal;
jest.mock('rc-motion');
// TODO: Remove this. Mock for React 19 // TODO: Remove this. Mock for React 19
jest.mock('react-dom', () => { jest.mock('react-dom', () => {
const realReactDOM = jest.requireActual('react-dom'); const realReactDOM = jest.requireActual('react-dom');
@ -70,11 +66,10 @@ jest.mock('../../_util/ActionButton', () => {
}); });
describe('Modal.confirm triggers callbacks correctly', () => { describe('Modal.confirm triggers callbacks correctly', () => {
// Inject CSSMotion to replace with No transition support const configWarp = (conf?: GlobalConfigProps) => {
const MockCSSMotion = genCSSMotion(false); ConfigProvider.config({ ...conf, theme: { token: { motion: false } } });
Object.keys(MockCSSMotion).forEach((key) => { };
(CSSMotion as any)[key] = (MockCSSMotion as any)[key]; configWarp();
});
// // Mock for rc-util raf // // Mock for rc-util raf
// window.requestAnimationFrame = callback => { // window.requestAnimationFrame = callback => {
@ -101,6 +96,9 @@ describe('Modal.confirm triggers callbacks correctly', () => {
if (errorStr.includes('was not wrapped in act(...)')) { if (errorStr.includes('was not wrapped in act(...)')) {
return; return;
} }
if (errorStr.includes('Static function can not')) {
return;
}
originError(...args); originError(...args);
}; };
@ -143,6 +141,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
confirm({ confirm({
content: 'some descriptions', content: 'some descriptions',
}); });
await waitFakeTimer(); await waitFakeTimer();
expect(document.querySelector('.ant-modal-confirm-title')).toBe(null); expect(document.querySelector('.ant-modal-confirm-title')).toBe(null);
}); });
@ -515,7 +514,6 @@ describe('Modal.confirm triggers callbacks correctly', () => {
}); });
it('should warning when pass a string as icon props', async () => { it('should warning when pass a string as icon props', async () => {
const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
confirm({ confirm({
content: 'some descriptions', content: 'some descriptions',
icon: 'ab', icon: 'ab',
@ -523,7 +521,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
await waitFakeTimer(); await waitFakeTimer();
expect(warnSpy).not.toHaveBeenCalled(); expect(errorSpy).not.toHaveBeenCalled();
confirm({ confirm({
content: 'some descriptions', content: 'some descriptions',
icon: 'question', icon: 'question',
@ -531,10 +529,9 @@ describe('Modal.confirm triggers callbacks correctly', () => {
await waitFakeTimer(); await waitFakeTimer();
expect(warnSpy).toHaveBeenCalledWith( expect(errorSpy).toHaveBeenCalledWith(
`Warning: [antd: Modal] \`icon\` is using ReactNode instead of string naming in v4. Please check \`question\` at https://ant.design/components/icon`, `Warning: [antd: Modal] \`icon\` is using ReactNode instead of string naming in v4. Please check \`question\` at https://ant.design/components/icon`,
); );
warnSpy.mockRestore();
}); });
it('icon can be null to hide icon', async () => { it('icon can be null to hide icon', async () => {
@ -583,7 +580,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
}); });
it('should be able to global config rootPrefixCls', async () => { it('should be able to global config rootPrefixCls', async () => {
ConfigProvider.config({ prefixCls: 'my', iconPrefixCls: 'bamboo' }); configWarp({ prefixCls: 'my', iconPrefixCls: 'bamboo' });
confirm({ title: 'title', icon: <SmileOutlined /> }); confirm({ title: 'title', icon: <SmileOutlined /> });
await waitFakeTimer(); await waitFakeTimer();
@ -592,7 +589,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
expect(document.querySelectorAll('.my-btn').length).toBe(2); expect(document.querySelectorAll('.my-btn').length).toBe(2);
expect(document.querySelectorAll('.bamboo-smile').length).toBe(1); expect(document.querySelectorAll('.bamboo-smile').length).toBe(1);
expect(document.querySelectorAll('.my-modal-confirm').length).toBe(1); expect(document.querySelectorAll('.my-modal-confirm').length).toBe(1);
ConfigProvider.config({ prefixCls: defaultPrefixCls, iconPrefixCls: undefined }); configWarp({ prefixCls: defaultPrefixCls, iconPrefixCls: undefined });
}); });
it('should be able to config rootPrefixCls', async () => { it('should be able to config rootPrefixCls', async () => {
@ -884,8 +881,8 @@ describe('Modal.confirm triggers callbacks correctly', () => {
expect(document.querySelector('.custom-footer-ele')).toBeTruthy(); expect(document.querySelector('.custom-footer-ele')).toBeTruthy();
}); });
it('should be able to config holderRender', async () => { it('should be able to config holderRender', async () => {
ConfigProvider.config({ configWarp({
holderRender: (children) => ( holderRender: (children: React.ReactNode) => (
<ConfigProvider prefixCls="test" iconPrefixCls="icon"> <ConfigProvider prefixCls="test" iconPrefixCls="icon">
{children} {children}
</ConfigProvider> </ConfigProvider>
@ -897,12 +894,14 @@ describe('Modal.confirm triggers callbacks correctly', () => {
expect(document.querySelectorAll('.anticon-exclamation-circle')).toHaveLength(0); expect(document.querySelectorAll('.anticon-exclamation-circle')).toHaveLength(0);
expect(document.querySelectorAll('.test-modal-root')).toHaveLength(1); expect(document.querySelectorAll('.test-modal-root')).toHaveLength(1);
expect(document.querySelectorAll('.icon-exclamation-circle')).toHaveLength(1); expect(document.querySelectorAll('.icon-exclamation-circle')).toHaveLength(1);
ConfigProvider.config({ holderRender: undefined }); configWarp({ holderRender: undefined });
}); });
it('should be able to config holderRender config rtl', async () => { it('should be able to config holderRender config rtl', async () => {
document.body.innerHTML = ''; document.body.innerHTML = '';
ConfigProvider.config({ configWarp({
holderRender: (children) => <ConfigProvider direction="rtl">{children}</ConfigProvider>, holderRender: (children: React.ReactNode) => (
<ConfigProvider direction="rtl">{children}</ConfigProvider>
),
}); });
Modal.confirm({ content: 'hai' }); Modal.confirm({ content: 'hai' });
await waitFakeTimer(); await waitFakeTimer();
@ -917,18 +916,18 @@ describe('Modal.confirm triggers callbacks correctly', () => {
Modal.confirm({ content: 'hai', direction: 'ltr' }); Modal.confirm({ content: 'hai', direction: 'ltr' });
await waitFakeTimer(); await waitFakeTimer();
expect(document.querySelector('.ant-modal-confirm-rtl')).toBeFalsy(); expect(document.querySelector('.ant-modal-confirm-rtl')).toBeFalsy();
ConfigProvider.config({ holderRender: undefined }); configWarp({ holderRender: undefined });
}); });
it('should be able to config holderRender and static config', async () => { it('should be able to config holderRender and static config', async () => {
// level 1 // level 1
ConfigProvider.config({ prefixCls: 'prefix-1' }); configWarp({ prefixCls: 'prefix-1' });
Modal.confirm({ content: 'hai' }); Modal.confirm({ content: 'hai' });
await waitFakeTimer(); await waitFakeTimer();
expect(document.querySelectorAll('.prefix-1-modal-root')).toHaveLength(1); expect(document.querySelectorAll('.prefix-1-modal-root')).toHaveLength(1);
expect($$('.prefix-1-btn')).toHaveLength(2); expect($$('.prefix-1-btn')).toHaveLength(2);
// level 2 // level 2
document.body.innerHTML = ''; document.body.innerHTML = '';
ConfigProvider.config({ configWarp({
prefixCls: 'prefix-1', prefixCls: 'prefix-1',
holderRender: (children) => <ConfigProvider prefixCls="prefix-2">{children}</ConfigProvider>, holderRender: (children) => <ConfigProvider prefixCls="prefix-2">{children}</ConfigProvider>,
}); });
@ -945,11 +944,11 @@ describe('Modal.confirm triggers callbacks correctly', () => {
expect(document.querySelectorAll('.prefix-3-btn')).toHaveLength(2); expect(document.querySelectorAll('.prefix-3-btn')).toHaveLength(2);
// clear // clear
Modal.config({ rootPrefixCls: '' }); Modal.config({ rootPrefixCls: '' });
ConfigProvider.config({ prefixCls: '', holderRender: undefined }); configWarp({ prefixCls: '', holderRender: undefined });
}); });
it('should be able to config holderRender antd locale', async () => { it('should be able to config holderRender antd locale', async () => {
document.body.innerHTML = ''; document.body.innerHTML = '';
ConfigProvider.config({ configWarp({
holderRender: (children) => ( holderRender: (children) => (
<ConfigProvider locale={{ Modal: { okText: 'test' } } as any}>{children}</ConfigProvider> <ConfigProvider locale={{ Modal: { okText: 'test' } } as any}>{children}</ConfigProvider>
), ),
@ -957,7 +956,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
Modal.confirm({ content: 'hai' }); Modal.confirm({ content: 'hai' });
await waitFakeTimer(); await waitFakeTimer();
expect(document.querySelector('.ant-btn-primary')?.textContent).toBe('test'); expect(document.querySelector('.ant-btn-primary')?.textContent).toBe('test');
ConfigProvider.config({ holderRender: undefined }); configWarp({ holderRender: undefined });
}); });
it('onCancel and onOk return any results and should be closed', async () => { it('onCancel and onOk return any results and should be closed', async () => {

View File

@ -1,18 +1,15 @@
import React from 'react'; import React from 'react';
import CSSMotion from 'rc-motion';
import { genCSSMotion } from 'rc-motion/lib/CSSMotion';
import KeyCode from 'rc-util/lib/KeyCode'; import KeyCode from 'rc-util/lib/KeyCode';
import Modal from '..'; import Modal from '..';
import { act, fireEvent, render, waitFakeTimer } from '../../../tests/utils'; import { act, fireEvent, render, waitFakeTimer } from '../../../tests/utils';
import Button from '../../button'; import Button from '../../button';
import ConfigProvider from '../../config-provider'; import ConfigProvider, { ConfigProviderProps } from '../../config-provider';
import Input from '../../input'; import Input from '../../input';
import zhCN from '../../locale/zh_CN'; import zhCN from '../../locale/zh_CN';
import type { ModalFunc } from '../confirm'; import type { ModalFunc } from '../confirm';
jest.mock('rc-util/lib/Portal'); jest.mock('rc-util/lib/Portal');
jest.mock('rc-motion');
// TODO: Remove this. Mock for React 19 // TODO: Remove this. Mock for React 19
jest.mock('react-dom', () => { jest.mock('react-dom', () => {
@ -27,12 +24,9 @@ jest.mock('react-dom', () => {
}); });
describe('Modal.hook', () => { describe('Modal.hook', () => {
// Inject CSSMotion to replace with No transition support const ConfigWarp = (conf?: ConfigProviderProps) => {
const MockCSSMotion = genCSSMotion(false); return <ConfigProvider {...conf} theme={{ token: { motion: false } }} />;
Object.keys(MockCSSMotion).forEach((key) => { };
// @ts-ignore
CSSMotion[key] = MockCSSMotion[key];
});
it('hooks support context', () => { it('hooks support context', () => {
jest.useFakeTimers(); jest.useFakeTimers();
@ -60,7 +54,11 @@ describe('Modal.hook', () => {
); );
}; };
const { container } = render(<Demo />); const { container } = render(
<ConfigWarp>
<Demo />
</ConfigWarp>,
);
fireEvent.click(container.querySelectorAll('button')[0]); fireEvent.click(container.querySelectorAll('button')[0]);
expect(document.body.querySelectorAll('.test-hook')[0].textContent).toBe('bamboo'); expect(document.body.querySelectorAll('.test-hook')[0].textContent).toBe('bamboo');
@ -105,12 +103,12 @@ describe('Modal.hook', () => {
} }
return ( return (
<div className="App"> <ConfigWarp>
{contextHolder} {contextHolder}
<div className="open-hook-modal-btn" onClick={showConfirm}> <div className="open-hook-modal-btn" onClick={showConfirm}>
confirm confirm
</div> </div>
</div> </ConfigWarp>
); );
}; };
@ -145,9 +143,9 @@ describe('Modal.hook', () => {
}; };
const { container } = render( const { container } = render(
<ConfigProvider direction="rtl"> <ConfigWarp direction="rtl">
<Demo /> <Demo />
</ConfigProvider>, </ConfigWarp>,
); );
fireEvent.click(container.querySelectorAll('button')[0]); fireEvent.click(container.querySelectorAll('button')[0]);
@ -172,12 +170,12 @@ describe('Modal.hook', () => {
}, [modal]); }, [modal]);
return ( return (
<div className="App"> <ConfigWarp>
{contextHolder} {contextHolder}
<div className="open-hook-modal-btn" onClick={openBrokenModal}> <div className="open-hook-modal-btn" onClick={openBrokenModal}>
Test hook modal Test hook modal
</div> </div>
</div> </ConfigWarp>
); );
}; };
@ -212,12 +210,12 @@ describe('Modal.hook', () => {
}; };
return ( return (
<div className="App"> <ConfigWarp>
{contextHolder} {contextHolder}
<div className="open-hook-modal-btn" onClick={openBrokenModal}> <div className="open-hook-modal-btn" onClick={openBrokenModal}>
Test hook modal Test hook modal
</div> </div>
</div> </ConfigWarp>
); );
}; };
@ -307,12 +305,12 @@ describe('Modal.hook', () => {
}, [modal]); }, [modal]);
return ( return (
<div className="App"> <ConfigWarp>
{contextHolder} {contextHolder}
<div className="open-hook-modal-btn" onClick={openBrokenModal}> <div className="open-hook-modal-btn" onClick={openBrokenModal}>
Test hook modal Test hook modal
</div> </div>
</div> </ConfigWarp>
); );
}; };
@ -399,7 +397,7 @@ describe('Modal.hook', () => {
}); });
}, []); }, []);
return <ConfigProvider autoInsertSpaceInButton={false}>{contextHolder}</ConfigProvider>; return <ConfigWarp autoInsertSpaceInButton={false}>{contextHolder}</ConfigWarp>;
}; };
render(<Demo />); render(<Demo />);
@ -414,7 +412,7 @@ describe('Modal.hook', () => {
React.useEffect(() => { React.useEffect(() => {
modal.confirm({ title: 'Confirm', afterClose }); modal.confirm({ title: 'Confirm', afterClose });
}, []); }, []);
return contextHolder; return <ConfigWarp>{contextHolder}</ConfigWarp>;
}; };
render(<Demo />); render(<Demo />);
@ -427,37 +425,23 @@ describe('Modal.hook', () => {
it('should be applied correctly locale', async () => { it('should be applied correctly locale', async () => {
jest.useFakeTimers(); jest.useFakeTimers();
const Demo: React.FC<{ count: number }> = ({ count }) => { const Demo: React.FC<{ zh?: boolean }> = ({ zh }) => {
const [modal, contextHolder] = Modal.useModal();
React.useEffect(() => { React.useEffect(() => {
const instance = Modal.confirm({}); const instance = modal.confirm({});
return () => { return () => instance.destroy();
instance.destroy(); }, []);
};
}, [count]);
let node = null; return <ConfigWarp locale={zh ? zhCN : undefined}>{contextHolder}</ConfigWarp>;
for (let i = 0; i < count; i += 1) {
node = <ConfigProvider locale={zhCN}>{node}</ConfigProvider>;
}
return node;
}; };
const { rerender } = render(<div />); const { unmount } = render(<Demo zh />);
await waitFakeTimer();
expect(document.body.querySelector('.ant-btn-primary')!.textContent).toEqual('确 定');
unmount();
for (let i = 10; i > 0; i -= 1) { render(<Demo />);
rerender(<Demo count={i} />);
await waitFakeTimer();
expect(document.body.querySelector('.ant-btn-primary')!.textContent).toEqual('确 定');
fireEvent.click(document.body.querySelector('.ant-btn-primary')!);
await waitFakeTimer();
}
rerender(<Demo count={0} />);
await waitFakeTimer(); await waitFakeTimer();
expect(document.body.querySelector('.ant-btn-primary')!.textContent).toEqual('OK'); expect(document.body.querySelector('.ant-btn-primary')!.textContent).toEqual('OK');
@ -488,7 +472,7 @@ describe('Modal.hook', () => {
})(); })();
}, []); }, []);
return contextHolder; return <ConfigWarp>{contextHolder}</ConfigWarp>;
}; };
render(<Demo />); render(<Demo />);
@ -526,7 +510,7 @@ describe('Modal.hook', () => {
})(); })();
}, []); }, []);
return contextHolder; return <ConfigWarp>{contextHolder}</ConfigWarp>;
}; };
render(<Demo />); render(<Demo />);