mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-07 17:44:35 +08:00
feat: refactor useNotification (#35423)
* more refactor * chore: motion support * chore: tmp test * test: Hooks * chore: static function * tmp of it * all of it * mv prefix * chore: clean up * chore: clean up * more test case * test: all base test * test: all test case * init * refactor: rm notification.open instance related code * follow up * refactor: singlton * test: notification test case * refactor to destroy * refactor: message base * test: part test case * test: more * test: more * test: all test * chore: clean up * docs: reorder * chore: fix lint * test: fix test case * chore: add act * chore: back * chore: fix style * test: notification test * test: more and more * test: fix more test * test: index * test: more & more * test: fix placement * test: fix coverage * chore: clean up * chore: bundle size * fix: 17 * chore: more * test: message * test: more test * fix: lint * test: rm class in static * chore: clean up * test: coverage * chore: fix lint
This commit is contained in:
parent
d326765a6b
commit
2341a25d91
@ -123,22 +123,12 @@ export const globalConfig = () => ({
|
||||
return suffixCls ? `${getGlobalPrefixCls()}-${suffixCls}` : getGlobalPrefixCls();
|
||||
},
|
||||
getIconPrefixCls: getGlobalIconPrefixCls,
|
||||
getRootPrefixCls: (rootPrefixCls?: string, customizePrefixCls?: string) => {
|
||||
// Customize rootPrefixCls is first priority
|
||||
if (rootPrefixCls) {
|
||||
return rootPrefixCls;
|
||||
}
|
||||
|
||||
getRootPrefixCls: () => {
|
||||
// If Global prefixCls provided, use this
|
||||
if (globalPrefixCls) {
|
||||
return globalPrefixCls;
|
||||
}
|
||||
|
||||
// [Legacy] If customize prefixCls provided, we cut it to get the prefixCls
|
||||
if (customizePrefixCls && customizePrefixCls.includes('-')) {
|
||||
return customizePrefixCls.replace(/^(.*)-[^-]*$/, '$1');
|
||||
}
|
||||
|
||||
// Fallback to default prefixCls
|
||||
return getGlobalPrefixCls();
|
||||
},
|
||||
|
@ -1,207 +0,0 @@
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { sleep } from '../../../tests/utils';
|
||||
import message, { getInstance } from '..';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
|
||||
describe('message.config', () => {
|
||||
// Mock for rc-util raf
|
||||
window.requestAnimationFrame = callback => window.setTimeout(callback, 16);
|
||||
window.cancelAnimationFrame = id => {
|
||||
window.clearTimeout(id);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
|
||||
act(() => {
|
||||
message.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to config top', () => {
|
||||
message.config({
|
||||
top: 100,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
message.info('whatever');
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-message')[0].style.top).toBe('100px');
|
||||
});
|
||||
|
||||
it('should be able to config rtl', () => {
|
||||
message.config({
|
||||
rtl: true,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
message.info('whatever');
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-rtl').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should be able to config getContainer', () => {
|
||||
message.config({
|
||||
getContainer: () => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'custom-container';
|
||||
document.body.appendChild(div);
|
||||
return div;
|
||||
},
|
||||
});
|
||||
|
||||
act(() => {
|
||||
message.info('whatever');
|
||||
});
|
||||
expect(document.querySelectorAll('.custom-container').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should be able to config maxCount', () => {
|
||||
message.config({
|
||||
maxCount: 5,
|
||||
});
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
act(() => {
|
||||
message.info('test');
|
||||
});
|
||||
}
|
||||
|
||||
act(() => {
|
||||
message.info('last');
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(5);
|
||||
expect(document.querySelectorAll('.ant-message-notice')[4].textContent).toBe('last');
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(getInstance().component.state.notices).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be able to config duration', async () => {
|
||||
jest.useRealTimers();
|
||||
message.config({
|
||||
duration: 0.5,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
message.info('last');
|
||||
});
|
||||
expect(getInstance().component.state.notices).toHaveLength(1);
|
||||
|
||||
await sleep(1000);
|
||||
expect(getInstance().component.state.notices).toHaveLength(0);
|
||||
message.config({
|
||||
duration: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('customize prefix should auto get transition prefixCls', () => {
|
||||
message.config({
|
||||
prefixCls: 'light-message',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
message.info('bamboo');
|
||||
});
|
||||
|
||||
expect(getInstance().config).toEqual(
|
||||
expect.objectContaining({
|
||||
transitionName: 'light-move-up',
|
||||
}),
|
||||
);
|
||||
|
||||
message.config({
|
||||
prefixCls: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to global config rootPrefixCls', () => {
|
||||
ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
|
||||
|
||||
act(() => {
|
||||
message.info('last');
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.prefix-test-message-notice')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.bamboo-info-circle')).toHaveLength(1);
|
||||
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null });
|
||||
});
|
||||
it('should be able to config prefixCls', () => {
|
||||
message.config({
|
||||
prefixCls: 'prefix-test',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
message.info('last');
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
|
||||
message.config({
|
||||
prefixCls: '', // can be set to empty, ant default value is set in ConfigProvider
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to config transitionName', () => {
|
||||
message.config({
|
||||
transitionName: '',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
message.info('last');
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-move-up-enter')).toHaveLength(0);
|
||||
message.config({
|
||||
transitionName: 'ant-move-up',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to config getContainer, although messageInstance already exists', () => {
|
||||
function createContainer() {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
return [
|
||||
container,
|
||||
() => {
|
||||
document.body.removeChild(container);
|
||||
},
|
||||
];
|
||||
}
|
||||
const [container1, removeContainer1] = createContainer();
|
||||
const [container2, removeContainer2] = createContainer();
|
||||
expect(container1.querySelector('.ant-message-notice')).toBeFalsy();
|
||||
expect(container2.querySelector('.ant-message-notice')).toBeFalsy();
|
||||
message.config({
|
||||
getContainer: () => container1,
|
||||
});
|
||||
const messageText1 = 'mounted in container1';
|
||||
|
||||
act(() => {
|
||||
message.info(messageText1);
|
||||
});
|
||||
|
||||
expect(container1.querySelector('.ant-message-notice').textContent).toEqual(messageText1);
|
||||
message.config({
|
||||
getContainer: () => container2,
|
||||
});
|
||||
const messageText2 = 'mounted in container2';
|
||||
|
||||
act(() => {
|
||||
message.info(messageText2);
|
||||
});
|
||||
expect(container2.querySelector('.ant-message-notice').textContent).toEqual(messageText2);
|
||||
removeContainer1();
|
||||
removeContainer2();
|
||||
});
|
||||
});
|
212
components/message/__tests__/config.test.tsx
Normal file
212
components/message/__tests__/config.test.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
import { act } from '../../../tests/utils';
|
||||
import message, { actWrapper } from '..';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import { awaitPromise, triggerMotionEnd } from './util';
|
||||
|
||||
describe('message.config', () => {
|
||||
beforeAll(() => {
|
||||
actWrapper(act);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
message.destroy();
|
||||
await triggerMotionEnd();
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
await awaitPromise();
|
||||
});
|
||||
|
||||
it('should be able to config top', async () => {
|
||||
message.config({
|
||||
top: 100,
|
||||
});
|
||||
|
||||
message.info('whatever');
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.ant-message')).toHaveStyle({
|
||||
top: '100px',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to config rtl', async () => {
|
||||
message.config({
|
||||
rtl: true,
|
||||
});
|
||||
|
||||
message.info('whatever');
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.ant-message-rtl')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be able to config getContainer', async () => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'custom-container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
message.config({
|
||||
getContainer: () => div,
|
||||
});
|
||||
|
||||
message.info('whatever');
|
||||
await awaitPromise();
|
||||
|
||||
expect(div.querySelector('.ant-message')).toBeTruthy();
|
||||
|
||||
message.config({
|
||||
getContainer: null,
|
||||
});
|
||||
|
||||
document.body.removeChild(div);
|
||||
});
|
||||
|
||||
it('should be able to config maxCount', async () => {
|
||||
message.config({
|
||||
maxCount: 5,
|
||||
});
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
message.info('test');
|
||||
}
|
||||
|
||||
message.info('last');
|
||||
await awaitPromise();
|
||||
|
||||
const noticeWithoutLeaving = Array.from(
|
||||
document.querySelectorAll('.ant-message-notice'),
|
||||
).filter(ele => !ele.classList.contains('ant-message-move-up-leave'));
|
||||
|
||||
expect(noticeWithoutLeaving).toHaveLength(5);
|
||||
expect(noticeWithoutLeaving[4].textContent).toEqual('last');
|
||||
|
||||
await triggerMotionEnd();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
|
||||
message.config({
|
||||
maxCount: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to config duration', async () => {
|
||||
message.config({
|
||||
duration: 0.5,
|
||||
});
|
||||
|
||||
message.info('last');
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
|
||||
message.config({
|
||||
duration: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('customize prefix should auto get transition prefixCls', async () => {
|
||||
message.config({
|
||||
prefixCls: 'light-message',
|
||||
});
|
||||
|
||||
message.info('bamboo');
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.light-message-move-up')).toBeTruthy();
|
||||
|
||||
message.config({
|
||||
prefixCls: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to global config rootPrefixCls', async () => {
|
||||
ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
|
||||
|
||||
message.info('last');
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.prefix-test-message-notice')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.bamboo-info-circle')).toHaveLength(1);
|
||||
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null! });
|
||||
});
|
||||
|
||||
it('should be able to config prefixCls', async () => {
|
||||
message.config({
|
||||
prefixCls: 'prefix-test',
|
||||
});
|
||||
|
||||
message.info('last');
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
|
||||
message.config({
|
||||
prefixCls: '', // can be set to empty, ant default value is set in ConfigProvider
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to config transitionName', async () => {
|
||||
message.config({
|
||||
transitionName: '',
|
||||
});
|
||||
|
||||
message.info('last');
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.ant-message-notice')).toBeTruthy();
|
||||
expect(document.querySelectorAll('.ant-move-up-enter')).toHaveLength(0);
|
||||
message.config({
|
||||
transitionName: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to config getContainer, although messageInstance already exists', async () => {
|
||||
function createContainer(): [HTMLElement, VoidFunction] {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
return [
|
||||
container,
|
||||
() => {
|
||||
document.body.removeChild(container);
|
||||
},
|
||||
];
|
||||
}
|
||||
const [container1, removeContainer1] = createContainer();
|
||||
const [container2, removeContainer2] = createContainer();
|
||||
expect(container1.querySelector('.ant-message-notice')).toBeFalsy();
|
||||
expect(container2.querySelector('.ant-message-notice')).toBeFalsy();
|
||||
|
||||
message.config({
|
||||
getContainer: () => container1,
|
||||
});
|
||||
const messageText1 = 'mounted in container1';
|
||||
|
||||
message.info(messageText1);
|
||||
await awaitPromise();
|
||||
expect(container1.querySelector('.ant-message-notice')!.textContent).toEqual(messageText1);
|
||||
|
||||
// Config will directly change container
|
||||
message.config({
|
||||
getContainer: () => container2,
|
||||
});
|
||||
const messageText2 = 'mounted in container2';
|
||||
|
||||
message.info(messageText2);
|
||||
expect(container2.querySelectorAll('.ant-message-notice')[1]!.textContent).toEqual(
|
||||
messageText2,
|
||||
);
|
||||
|
||||
removeContainer1();
|
||||
removeContainer2();
|
||||
});
|
||||
});
|
@ -1,21 +1,18 @@
|
||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import message, { getInstance } from '..';
|
||||
import message from '..';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import { render, fireEvent } from '../../../tests/utils';
|
||||
import { triggerMotionEnd } from './util';
|
||||
|
||||
describe('message.hooks', () => {
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
message.destroy();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
@ -46,10 +43,11 @@ describe('message.hooks', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const wrapper = mount(<Demo />);
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
||||
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
||||
const { container } = render(<Demo />);
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
|
||||
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
|
||||
});
|
||||
|
||||
it('should work with success', () => {
|
||||
@ -80,16 +78,15 @@ describe('message.hooks', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const wrapper = mount(<Demo />);
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
||||
expect(document.querySelectorAll('.anticon-check-circle').length).toBe(1);
|
||||
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
||||
const { container } = render(<Demo />);
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.anticon-check-circle')).toHaveLength(1);
|
||||
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
|
||||
});
|
||||
|
||||
it('should work with onClose', done => {
|
||||
// if not use real timer, done won't be called
|
||||
jest.useRealTimers();
|
||||
const Demo = () => {
|
||||
const [api, holder] = message.useMessage();
|
||||
return (
|
||||
@ -111,14 +108,13 @@ describe('message.hooks', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const wrapper = mount(<Demo />);
|
||||
wrapper.find('button').simulate('click');
|
||||
jest.useFakeTimers();
|
||||
const { container } = render(<Demo />);
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
triggerMotionEnd();
|
||||
});
|
||||
|
||||
it('should work with close promise', done => {
|
||||
// if not use real timer, done won't be called
|
||||
jest.useRealTimers();
|
||||
const Demo = () => {
|
||||
const [api, holder] = message.useMessage();
|
||||
return (
|
||||
@ -141,13 +137,14 @@ describe('message.hooks', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const wrapper = mount(<Demo />);
|
||||
wrapper.find('button').simulate('click');
|
||||
jest.useFakeTimers();
|
||||
const { container } = render(<Demo />);
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
triggerMotionEnd();
|
||||
});
|
||||
|
||||
it('should work with hide', () => {
|
||||
let hide;
|
||||
it('should work with hide', async () => {
|
||||
let hide: VoidFunction;
|
||||
const Demo = () => {
|
||||
const [api, holder] = message.useMessage();
|
||||
return (
|
||||
@ -166,48 +163,50 @@ describe('message.hooks', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const wrapper = mount(<Demo />);
|
||||
wrapper.find('button').simulate('click');
|
||||
const { container } = render(<Demo />);
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
hide!();
|
||||
});
|
||||
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
||||
await triggerMotionEnd('.my-test-message-move-up-leave');
|
||||
|
||||
act(() => {
|
||||
hide();
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(getInstance().component.state.notices).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be same hook', () => {
|
||||
let count = 0;
|
||||
let cacheAPI: any;
|
||||
|
||||
const Demo = () => {
|
||||
const [, forceUpdate] = React.useState({});
|
||||
const [api] = message.useMessage();
|
||||
|
||||
React.useEffect(() => {
|
||||
count += 1;
|
||||
expect(count).toEqual(1);
|
||||
forceUpdate();
|
||||
if (!cacheAPI) {
|
||||
cacheAPI = api;
|
||||
} else {
|
||||
expect(cacheAPI).toBe(api);
|
||||
}
|
||||
|
||||
forceUpdate({});
|
||||
}, [api]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
mount(<Demo />);
|
||||
render(<Demo />);
|
||||
});
|
||||
|
||||
it("should use ConfigProvider's getPopupContainer as message container", () => {
|
||||
const containerId = 'container';
|
||||
const getPopupContainer = () => {
|
||||
const div = document.createElement('div');
|
||||
div.id = containerId;
|
||||
document.body.appendChild(div);
|
||||
return div;
|
||||
};
|
||||
|
||||
const getPopupContainer = () => div;
|
||||
|
||||
const Demo = () => {
|
||||
const [api, holder] = message.useMessage();
|
||||
return (
|
||||
@ -226,13 +225,39 @@ describe('message.hooks', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const wrapper = mount(<Demo />);
|
||||
const { container } = render(<Demo />);
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
||||
expect(document.querySelectorAll('.anticon-check-circle').length).toBe(1);
|
||||
expect(document.querySelector('.hook-content').innerHTML).toEqual('happy');
|
||||
expect(document.querySelectorAll(`#${containerId}`).length).toBe(1);
|
||||
expect(wrapper.find(`#${containerId}`).children.length).toBe(1);
|
||||
expect(div.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
|
||||
expect(div.querySelectorAll('.anticon-check-circle')).toHaveLength(1);
|
||||
expect(div.querySelector('.hook-content')!.textContent).toEqual('happy');
|
||||
expect(document.querySelectorAll(`#${containerId}`)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('warning if user call update in render', () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const Demo = () => {
|
||||
const [api, holder] = message.useMessage();
|
||||
const calledRef = React.useRef(false);
|
||||
|
||||
if (!calledRef.current) {
|
||||
api.info({
|
||||
content: <div className="bamboo" />,
|
||||
});
|
||||
calledRef.current = true;
|
||||
}
|
||||
|
||||
return holder;
|
||||
};
|
||||
|
||||
render(<Demo />);
|
||||
|
||||
expect(document.querySelector('.bamboo')).toBeFalsy();
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Message] You are calling notice in render which will break in React 18 concurrent mode. Please trigger in effect instead.',
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
59
components/message/__tests__/immediately.test.tsx
Normal file
59
components/message/__tests__/immediately.test.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import message, { actWrapper, actDestroy } from '..';
|
||||
import { act } from '../../../tests/utils';
|
||||
import { awaitPromise, triggerMotionEnd } from './util';
|
||||
|
||||
describe('call close immediately', () => {
|
||||
beforeAll(() => {
|
||||
actWrapper(act);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
actDestroy();
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
message.destroy();
|
||||
await triggerMotionEnd();
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
await awaitPromise();
|
||||
});
|
||||
|
||||
it('open', async () => {
|
||||
const closeFn = message.open({
|
||||
content: '',
|
||||
});
|
||||
closeFn();
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
|
||||
// Created close
|
||||
const closeFn2 = message.open({
|
||||
content: 'showed',
|
||||
});
|
||||
await awaitPromise();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
|
||||
closeFn2();
|
||||
await triggerMotionEnd();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('info', async () => {
|
||||
const closeFn = message.info('Message1', 0);
|
||||
closeFn();
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
});
|
||||
});
|
@ -1,255 +0,0 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { SmileOutlined } from '@ant-design/icons';
|
||||
import message, { getInstance } from '..';
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
describe('message', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
|
||||
act(() => {
|
||||
message.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to hide manually', async () => {
|
||||
let hide1;
|
||||
let hide2;
|
||||
|
||||
act(() => {
|
||||
hide1 = message.info('whatever', 0);
|
||||
});
|
||||
act(() => {
|
||||
hide2 = message.info('whatever', 0);
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
||||
|
||||
hide1();
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(getInstance().component.state.notices).toHaveLength(1);
|
||||
|
||||
hide2();
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(getInstance().component.state.notices).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be able to remove manually with a unique key', () => {
|
||||
const key1 = 'key1';
|
||||
const key2 = 'key2';
|
||||
|
||||
act(() => {
|
||||
message.info({ content: 'Message1', key: 'key1', duration: 0 });
|
||||
});
|
||||
act(() => {
|
||||
message.info({ content: 'Message2', key: 'key2', duration: 0 });
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
||||
|
||||
act(() => {
|
||||
message.destroy(key1);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(getInstance().component.state.notices).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
message.destroy(key2);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(getInstance().component.state.notices).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be able to destroy globally', () => {
|
||||
act(() => {
|
||||
message.info('whatever', 0);
|
||||
});
|
||||
act(() => {
|
||||
message.info('whatever', 0);
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-message').length).toBe(1);
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
||||
|
||||
act(() => {
|
||||
message.destroy();
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(document.querySelectorAll('.ant-message').length).toBe(0);
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not need to use duration argument when using the onClose arguments', () => {
|
||||
message.info('whatever', () => {});
|
||||
});
|
||||
|
||||
it('should have the default duration when using the onClose arguments', done => {
|
||||
jest.useRealTimers();
|
||||
const defaultDuration = 3;
|
||||
const now = Date.now();
|
||||
message.info('whatever', () => {
|
||||
// calculate the approximately duration value
|
||||
const aboutDuration = parseInt((Date.now() - now) / 1000, 10);
|
||||
expect(aboutDuration).toBe(defaultDuration);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('trigger onClick method', () => {
|
||||
const onClick = jest.fn();
|
||||
class Test extends React.Component {
|
||||
componentDidMount() {
|
||||
message.info({
|
||||
onClick,
|
||||
duration: 0,
|
||||
content: 'message info',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>test message onClick method</div>;
|
||||
}
|
||||
}
|
||||
mount(<Test />);
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
|
||||
document.querySelectorAll('.ant-message-notice')[0].click();
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should be called like promise', done => {
|
||||
jest.useRealTimers();
|
||||
const defaultDuration = 3;
|
||||
const now = Date.now();
|
||||
message.info('whatever').then(() => {
|
||||
// calculate the approximately duration value
|
||||
const aboutDuration = parseInt((Date.now() - now) / 1000, 10);
|
||||
expect(aboutDuration).toBe(defaultDuration);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/8201
|
||||
it('should hide message correctly', () => {
|
||||
let hide;
|
||||
class Test extends React.Component {
|
||||
componentDidMount() {
|
||||
act(() => {
|
||||
hide = message.loading('Action in progress..', 0);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>test</div>;
|
||||
}
|
||||
}
|
||||
mount(<Test />);
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
|
||||
|
||||
act(() => {
|
||||
hide();
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(getInstance().component.state.notices).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should allow custom icon', () => {
|
||||
act(() => {
|
||||
message.open({ content: 'Message', icon: <SmileOutlined /> });
|
||||
});
|
||||
expect(document.querySelectorAll('.anticon-smile').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should have no icon', () => {
|
||||
message.open({ content: 'Message', icon: <span /> });
|
||||
expect(document.querySelectorAll('.ant-message-notice .anticon').length).toBe(0);
|
||||
});
|
||||
it('should have no icon when not pass icon props', () => {
|
||||
message.open({ content: 'Message' });
|
||||
expect(document.querySelectorAll('.ant-message-notice .anticon').length).toBe(0);
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/8201
|
||||
it('should destroy messages correctly', () => {
|
||||
class Test extends React.Component {
|
||||
componentDidMount() {
|
||||
message.loading('Action in progress1..', 0);
|
||||
message.loading('Action in progress2..', 0);
|
||||
setTimeout(() => message.destroy(), 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>test</div>;
|
||||
}
|
||||
}
|
||||
mount(<Test />);
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
||||
jest.runAllTimers();
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should support update message content with a unique key', () => {
|
||||
const key = 'updatable';
|
||||
class Test extends React.Component {
|
||||
componentDidMount() {
|
||||
message.loading({ content: 'Loading...', key });
|
||||
// Testing that content of the message should be updated.
|
||||
setTimeout(() => message.success({ content: 'Loaded', key }), 1000);
|
||||
setTimeout(() => message.destroy(), 3000);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>test</div>;
|
||||
}
|
||||
}
|
||||
|
||||
mount(<Test />);
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
|
||||
jest.advanceTimersByTime(1500);
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
|
||||
jest.runAllTimers();
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
||||
});
|
||||
|
||||
it('update message content with a unique key and cancel manually', () => {
|
||||
const key = 'updatable';
|
||||
class Test extends React.Component {
|
||||
componentDidMount() {
|
||||
let hideLoading;
|
||||
act(() => {
|
||||
hideLoading = message.loading({ content: 'Loading...', key, duration: 0 });
|
||||
});
|
||||
// Testing that content of the message should be cancel manually.
|
||||
setTimeout(() => {
|
||||
act(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>test</div>;
|
||||
}
|
||||
}
|
||||
|
||||
mount(<Test />);
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
|
||||
|
||||
jest.advanceTimersByTime(1500);
|
||||
expect(getInstance().component.state.notices).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not throw error when pass null', () => {
|
||||
message.error(null);
|
||||
});
|
||||
});
|
237
components/message/__tests__/index.test.tsx
Normal file
237
components/message/__tests__/index.test.tsx
Normal file
@ -0,0 +1,237 @@
|
||||
import React from 'react';
|
||||
import { SmileOutlined } from '@ant-design/icons';
|
||||
import message, { actWrapper } from '..';
|
||||
import { act, fireEvent, sleep } from '../../../tests/utils';
|
||||
import { awaitPromise, triggerMotionEnd } from './util';
|
||||
|
||||
describe('message', () => {
|
||||
beforeAll(() => {
|
||||
actWrapper(act);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
message.destroy();
|
||||
await triggerMotionEnd();
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
await awaitPromise();
|
||||
});
|
||||
|
||||
it('should be able to hide manually', async () => {
|
||||
const hide1 = message.info('whatever', 0);
|
||||
const hide2 = message.info('whatever', 0);
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
|
||||
|
||||
hide1();
|
||||
await triggerMotionEnd();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
|
||||
hide2();
|
||||
await triggerMotionEnd();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be able to remove manually with a unique key', async () => {
|
||||
const key1 = 'key1';
|
||||
const key2 = 'key2';
|
||||
|
||||
message.info({ content: 'Message1', key: 'key1', duration: 0 });
|
||||
message.info({ content: 'Message2', key: 'key2', duration: 0 });
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
|
||||
|
||||
message.destroy(key1);
|
||||
await triggerMotionEnd();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
|
||||
message.destroy(key2);
|
||||
await triggerMotionEnd();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be able to destroy globally', async () => {
|
||||
message.info('whatever', 0);
|
||||
message.info('whatever', 0);
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
|
||||
|
||||
message.destroy();
|
||||
await triggerMotionEnd();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not need to use duration argument when using the onClose arguments', async () => {
|
||||
const onClose = jest.fn();
|
||||
const close = message.info('whatever', onClose);
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
close();
|
||||
await triggerMotionEnd();
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have the default duration when using the onClose arguments', async () => {
|
||||
const onClose = jest.fn();
|
||||
|
||||
message.info('whatever', onClose);
|
||||
await awaitPromise();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(2500);
|
||||
});
|
||||
|
||||
expect(document.querySelector('.ant-message-move-up-leave')).toBeFalsy();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(document.querySelector('.ant-message-move-up-leave')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('trigger onClick method', async () => {
|
||||
const onClick = jest.fn();
|
||||
message.info({
|
||||
onClick,
|
||||
duration: 0,
|
||||
content: 'message info',
|
||||
});
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
fireEvent.click(document.querySelector('.ant-message-notice')!);
|
||||
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should be called like promise', async () => {
|
||||
const onClose = jest.fn();
|
||||
message.info('whatever').then(onClose);
|
||||
await awaitPromise();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(2500);
|
||||
});
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
await sleep(); // Wait to let event loop run
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/8201
|
||||
it('should hide message correctly', async () => {
|
||||
const hide = message.loading('Action in progress..', 0);
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
|
||||
hide!();
|
||||
await triggerMotionEnd();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should allow custom icon', async () => {
|
||||
message.open({ content: 'Message', icon: <SmileOutlined /> });
|
||||
|
||||
await awaitPromise();
|
||||
expect(document.querySelector('.anticon-smile')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have no icon', async () => {
|
||||
message.open({ content: 'Message', icon: <span /> });
|
||||
|
||||
await awaitPromise();
|
||||
expect(document.querySelector('.ant-message-notice .anticon')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should have no icon when not pass icon props', async () => {
|
||||
message.open({ content: 'Message' });
|
||||
|
||||
await awaitPromise();
|
||||
expect(document.querySelector('.ant-message-notice .anticon')).toBeFalsy();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/8201
|
||||
it('should destroy messages correctly', async () => {
|
||||
message.loading('Action in progress1..', 0);
|
||||
message.loading('Action in progress2..', 0);
|
||||
setTimeout(() => message.destroy(), 1000);
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
|
||||
|
||||
await triggerMotionEnd();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should support update message content with a unique key', async () => {
|
||||
const key = 'updatable';
|
||||
|
||||
message.loading({ content: 'Loading...', key });
|
||||
// Testing that content of the message should be updated.
|
||||
setTimeout(() => message.success({ content: 'Loaded', key }), 1000);
|
||||
setTimeout(() => message.destroy(), 3000);
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1500);
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
expect(document.querySelector('.ant-message-move-up-leave')).toBeFalsy();
|
||||
|
||||
await triggerMotionEnd();
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('update message content with a unique key and cancel manually', async () => {
|
||||
const key = 'updatable';
|
||||
|
||||
const hideLoading = message.loading({ content: 'Loading...', key, duration: 0 });
|
||||
await awaitPromise();
|
||||
|
||||
setTimeout(() => {
|
||||
act(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1500);
|
||||
});
|
||||
expect(document.querySelectorAll('.ant-message-move-up-leave')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not throw error when pass null', async () => {
|
||||
message.error(null);
|
||||
await awaitPromise();
|
||||
});
|
||||
});
|
@ -1,31 +1,56 @@
|
||||
import message from '..';
|
||||
import message, { actWrapper } from '..';
|
||||
import { act } from '../../../tests/utils';
|
||||
import { awaitPromise, triggerMotionEnd } from './util';
|
||||
|
||||
describe('message.typescript', () => {
|
||||
it('promise without auguments', () => {
|
||||
beforeAll(() => {
|
||||
actWrapper(act);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
message.destroy();
|
||||
await triggerMotionEnd();
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
await awaitPromise();
|
||||
});
|
||||
|
||||
it('promise without arguments', async () => {
|
||||
message.success('yes!!!', 0);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
it('promise with one augument', done => {
|
||||
message.success('yes!!!').then(filled => {
|
||||
expect(filled).toBe(true);
|
||||
done();
|
||||
});
|
||||
it('promise with one arguments', async () => {
|
||||
const filled = jest.fn();
|
||||
|
||||
message.success('yes!!!').then(filled);
|
||||
|
||||
await triggerMotionEnd();
|
||||
|
||||
expect(filled).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('promise two auguments', done => {
|
||||
message.success('yes!!!').then(
|
||||
filled => {
|
||||
expect(filled).toBe(true);
|
||||
done();
|
||||
},
|
||||
rejected => {
|
||||
expect(rejected).toBe(false);
|
||||
},
|
||||
);
|
||||
it('promise two arguments', async () => {
|
||||
const filled = jest.fn();
|
||||
const rejected = jest.fn();
|
||||
|
||||
message.success('yes!!!').then(filled, rejected);
|
||||
|
||||
await triggerMotionEnd();
|
||||
|
||||
expect(filled).toHaveBeenCalledWith(true);
|
||||
expect(rejected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hide', () => {
|
||||
it('hide', async () => {
|
||||
const hide = message.loading('doing...');
|
||||
await Promise.resolve();
|
||||
hide();
|
||||
});
|
||||
});
|
||||
|
25
components/message/__tests__/util.ts
Normal file
25
components/message/__tests__/util.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { act, fireEvent } from '../../../tests/utils';
|
||||
|
||||
export async function awaitPromise() {
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export async function triggerMotionEnd(selector: string = '.ant-message-move-up-leave') {
|
||||
await awaitPromise();
|
||||
|
||||
// Flush css motion state update
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(selector).forEach(ele => {
|
||||
fireEvent.animationEnd(ele);
|
||||
});
|
||||
|
||||
await awaitPromise();
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
---
|
||||
order: 10
|
||||
order: -1
|
||||
title:
|
||||
zh-CN: 通过 Hooks 获取上下文(4.5.0+)
|
||||
en-US: Get context with hooks (4.5.0+)
|
||||
zh-CN: Hooks 调用(推荐)
|
||||
en-US: Hooks usage (recommended)
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。
|
||||
通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
|
||||
|
||||
## en-US
|
||||
|
||||
Use `message.useMessage` to get `contextHolder` with context accessible issue.
|
||||
Use `message.useMessage` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `message` static method, because static method cannot consume context, and ConfigProvider data will not work.
|
||||
|
||||
```jsx
|
||||
import { message, Button } from 'antd';
|
||||
|
@ -1,90 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import useRCNotification from 'rc-notification/lib/useNotification';
|
||||
import type {
|
||||
NotificationInstance as RCNotificationInstance,
|
||||
NoticeContent as RCNoticeContent,
|
||||
HolderReadyCallback as RCHolderReadyCallback,
|
||||
} from 'rc-notification/lib/Notification';
|
||||
import type { ConfigConsumerProps } from '../../config-provider';
|
||||
import { ConfigConsumer } from '../../config-provider';
|
||||
import type { MessageInstance, ArgsProps, ThenableArgument } from '..';
|
||||
import { attachTypeApi, getKeyThenIncreaseKey, typeList } from '..';
|
||||
|
||||
export default function createUseMessage(
|
||||
getRcNotificationInstance: (
|
||||
args: ArgsProps,
|
||||
callback: (info: { prefixCls: string; instance: RCNotificationInstance }) => void,
|
||||
) => void,
|
||||
getRCNoticeProps: (args: ArgsProps, prefixCls: string) => RCNoticeContent,
|
||||
) {
|
||||
const useMessage = (): [MessageInstance, React.ReactElement] => {
|
||||
// We can only get content by render
|
||||
let getPrefixCls: ConfigConsumerProps['getPrefixCls'];
|
||||
let getPopupContainer: ConfigConsumerProps['getPopupContainer'];
|
||||
|
||||
// We create a proxy to handle delay created instance
|
||||
let innerInstance: RCNotificationInstance | null = null;
|
||||
const proxy = {
|
||||
add: (noticeProps: RCNoticeContent, holderCallback?: RCHolderReadyCallback) => {
|
||||
innerInstance?.component.add(noticeProps, holderCallback);
|
||||
},
|
||||
} as any;
|
||||
|
||||
const [hookNotify, holder] = useRCNotification(proxy);
|
||||
|
||||
function notify(args: ArgsProps) {
|
||||
const { prefixCls: customizePrefixCls } = args;
|
||||
const mergedPrefixCls = getPrefixCls('message', customizePrefixCls);
|
||||
const rootPrefixCls = getPrefixCls();
|
||||
const target = args.key || getKeyThenIncreaseKey();
|
||||
const closePromise = new Promise(resolve => {
|
||||
const callback = () => {
|
||||
if (typeof args.onClose === 'function') {
|
||||
args.onClose();
|
||||
}
|
||||
return resolve(true);
|
||||
};
|
||||
getRcNotificationInstance(
|
||||
{
|
||||
...args,
|
||||
prefixCls: mergedPrefixCls,
|
||||
rootPrefixCls,
|
||||
getPopupContainer,
|
||||
},
|
||||
({ prefixCls, instance }) => {
|
||||
innerInstance = instance;
|
||||
hookNotify(getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls));
|
||||
},
|
||||
);
|
||||
});
|
||||
const result: any = () => {
|
||||
if (innerInstance) {
|
||||
innerInstance.removeNotice(target);
|
||||
}
|
||||
};
|
||||
result.then = (filled: ThenableArgument, rejected: ThenableArgument) =>
|
||||
closePromise.then(filled, rejected);
|
||||
result.promise = closePromise;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fill functions
|
||||
const hookApiRef = React.useRef<any>({});
|
||||
|
||||
hookApiRef.current.open = notify;
|
||||
|
||||
typeList.forEach(type => attachTypeApi(hookApiRef.current, type));
|
||||
|
||||
return [
|
||||
hookApiRef.current,
|
||||
<ConfigConsumer key="holder">
|
||||
{(context: ConfigConsumerProps) => {
|
||||
({ getPrefixCls, getPopupContainer } = context);
|
||||
return holder;
|
||||
}}
|
||||
</ConfigConsumer>,
|
||||
];
|
||||
};
|
||||
|
||||
return useMessage;
|
||||
}
|
@ -1,277 +1,356 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import RCNotification from 'rc-notification';
|
||||
import { render } from 'rc-util/lib/React/render';
|
||||
import useMessage, { useInternalMessage } from './useMessage';
|
||||
import type {
|
||||
NotificationInstance as RCNotificationInstance,
|
||||
NoticeContent,
|
||||
} from 'rc-notification/lib/Notification';
|
||||
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
|
||||
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
|
||||
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
||||
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
|
||||
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
|
||||
import createUseMessage from './hooks/useMessage';
|
||||
ArgsProps,
|
||||
MessageInstance,
|
||||
ConfigOptions,
|
||||
NoticeType,
|
||||
TypeOpen,
|
||||
MessageType,
|
||||
} from './interface';
|
||||
import ConfigProvider, { globalConfig } from '../config-provider';
|
||||
import { wrapPromiseFn } from './util';
|
||||
|
||||
let messageInstance: RCNotificationInstance | null;
|
||||
let defaultDuration = 3;
|
||||
let defaultTop: number;
|
||||
let key = 1;
|
||||
let localPrefixCls = '';
|
||||
let transitionName = 'move-up';
|
||||
let hasTransitionName = false;
|
||||
let getContainer: () => HTMLElement;
|
||||
let maxCount: number;
|
||||
let rtl = false;
|
||||
export { ArgsProps };
|
||||
|
||||
export function getKeyThenIncreaseKey() {
|
||||
return key++;
|
||||
const methods: NoticeType[] = ['success', 'info', 'warning', 'error', 'loading'];
|
||||
|
||||
let message: GlobalMessage | null = null;
|
||||
|
||||
let act: (callback: VoidFunction) => Promise<void> | void = (callback: VoidFunction) => callback();
|
||||
|
||||
interface GlobalMessage {
|
||||
fragment: DocumentFragment;
|
||||
instance?: MessageInstance | null;
|
||||
sync?: VoidFunction;
|
||||
}
|
||||
|
||||
export interface ConfigOptions {
|
||||
top?: number;
|
||||
duration?: number;
|
||||
prefixCls?: string;
|
||||
getContainer?: () => HTMLElement;
|
||||
transitionName?: string;
|
||||
maxCount?: number;
|
||||
rtl?: boolean;
|
||||
interface OpenTask {
|
||||
type: 'open';
|
||||
config: ArgsProps;
|
||||
resolve: VoidFunction;
|
||||
setCloseFn: (closeFn: VoidFunction) => void;
|
||||
skipped?: boolean;
|
||||
}
|
||||
|
||||
function setMessageConfig(options: ConfigOptions) {
|
||||
if (options.top !== undefined) {
|
||||
defaultTop = options.top;
|
||||
messageInstance = null; // delete messageInstance for new defaultTop
|
||||
}
|
||||
if (options.duration !== undefined) {
|
||||
defaultDuration = options.duration;
|
||||
}
|
||||
|
||||
if (options.prefixCls !== undefined) {
|
||||
localPrefixCls = options.prefixCls;
|
||||
}
|
||||
if (options.getContainer !== undefined) {
|
||||
getContainer = options.getContainer;
|
||||
messageInstance = null; // delete messageInstance for new getContainer
|
||||
}
|
||||
if (options.transitionName !== undefined) {
|
||||
transitionName = options.transitionName;
|
||||
messageInstance = null; // delete messageInstance for new transitionName
|
||||
hasTransitionName = true;
|
||||
}
|
||||
if (options.maxCount !== undefined) {
|
||||
maxCount = options.maxCount;
|
||||
messageInstance = null;
|
||||
}
|
||||
if (options.rtl !== undefined) {
|
||||
rtl = options.rtl;
|
||||
}
|
||||
interface TypeTask {
|
||||
type: NoticeType;
|
||||
args: Parameters<TypeOpen>;
|
||||
resolve: VoidFunction;
|
||||
setCloseFn: (closeFn: VoidFunction) => void;
|
||||
skipped?: boolean;
|
||||
}
|
||||
|
||||
function getRCNotificationInstance(
|
||||
args: ArgsProps,
|
||||
callback: (info: {
|
||||
prefixCls: string;
|
||||
rootPrefixCls: string;
|
||||
iconPrefixCls: string;
|
||||
instance: RCNotificationInstance;
|
||||
}) => void,
|
||||
) {
|
||||
const { prefixCls: customizePrefixCls, getPopupContainer: getContextPopupContainer } = args;
|
||||
const { getPrefixCls, getRootPrefixCls, getIconPrefixCls } = globalConfig();
|
||||
const prefixCls = getPrefixCls('message', customizePrefixCls || localPrefixCls);
|
||||
const rootPrefixCls = getRootPrefixCls(args.rootPrefixCls, prefixCls);
|
||||
const iconPrefixCls = getIconPrefixCls();
|
||||
type Task =
|
||||
| OpenTask
|
||||
| TypeTask
|
||||
| {
|
||||
type: 'destroy';
|
||||
key: React.Key;
|
||||
skipped?: boolean;
|
||||
};
|
||||
|
||||
if (messageInstance) {
|
||||
callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance: messageInstance });
|
||||
return;
|
||||
}
|
||||
let taskQueue: Task[] = [];
|
||||
|
||||
const instanceConfig = {
|
||||
prefixCls,
|
||||
transitionName: hasTransitionName ? transitionName : `${rootPrefixCls}-${transitionName}`,
|
||||
style: { top: defaultTop }, // 覆盖原来的样式
|
||||
getContainer: getContainer || getContextPopupContainer,
|
||||
let defaultGlobalConfig: ConfigOptions = {};
|
||||
|
||||
function getGlobalContext() {
|
||||
const {
|
||||
prefixCls: globalPrefixCls,
|
||||
getContainer: globalGetContainer,
|
||||
rtl,
|
||||
maxCount,
|
||||
};
|
||||
top,
|
||||
} = defaultGlobalConfig;
|
||||
const mergedPrefixCls = globalPrefixCls ?? globalConfig().getPrefixCls('message');
|
||||
const mergedContainer = globalGetContainer?.() || document.body;
|
||||
|
||||
RCNotification.newInstance(instanceConfig, (instance: any) => {
|
||||
if (messageInstance) {
|
||||
callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance: messageInstance });
|
||||
return;
|
||||
}
|
||||
messageInstance = instance;
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
(messageInstance as any).config = instanceConfig;
|
||||
}
|
||||
|
||||
callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance });
|
||||
});
|
||||
}
|
||||
|
||||
export interface ThenableArgument {
|
||||
(val: any): void;
|
||||
}
|
||||
|
||||
export interface MessageType extends PromiseLike<any> {
|
||||
(): void;
|
||||
}
|
||||
|
||||
const typeToIcon = {
|
||||
info: InfoCircleFilled,
|
||||
success: CheckCircleFilled,
|
||||
error: CloseCircleFilled,
|
||||
warning: ExclamationCircleFilled,
|
||||
loading: LoadingOutlined,
|
||||
};
|
||||
|
||||
export type NoticeType = keyof typeof typeToIcon;
|
||||
|
||||
export const typeList = Object.keys(typeToIcon) as NoticeType[];
|
||||
|
||||
export interface ArgsProps {
|
||||
content: any;
|
||||
duration?: number;
|
||||
type?: NoticeType;
|
||||
prefixCls?: string;
|
||||
rootPrefixCls?: string;
|
||||
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
||||
onClose?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
key?: string | number;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
function getRCNoticeProps(
|
||||
args: ArgsProps,
|
||||
prefixCls: string,
|
||||
iconPrefixCls?: string,
|
||||
): NoticeContent {
|
||||
const duration = args.duration !== undefined ? args.duration : defaultDuration;
|
||||
const IconComponent = typeToIcon[args.type!];
|
||||
const messageClass = classNames(`${prefixCls}-custom-content`, {
|
||||
[`${prefixCls}-${args.type}`]: args.type,
|
||||
[`${prefixCls}-rtl`]: rtl === true,
|
||||
});
|
||||
return {
|
||||
key: args.key,
|
||||
duration,
|
||||
style: args.style || {},
|
||||
className: args.className,
|
||||
content: (
|
||||
<ConfigProvider iconPrefixCls={iconPrefixCls}>
|
||||
<div className={messageClass}>
|
||||
{args.icon || (IconComponent && <IconComponent />)}
|
||||
<span>{args.content}</span>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
),
|
||||
onClose: args.onClose,
|
||||
onClick: args.onClick,
|
||||
prefixCls: mergedPrefixCls,
|
||||
container: mergedContainer,
|
||||
rtl,
|
||||
maxCount,
|
||||
top,
|
||||
};
|
||||
}
|
||||
|
||||
function notice(args: ArgsProps): MessageType {
|
||||
const target = args.key || getKeyThenIncreaseKey();
|
||||
const closePromise = new Promise(resolve => {
|
||||
const callback = () => {
|
||||
if (typeof args.onClose === 'function') {
|
||||
args.onClose();
|
||||
}
|
||||
return resolve(true);
|
||||
interface GlobalHolderRef {
|
||||
instance: MessageInstance;
|
||||
sync: () => void;
|
||||
}
|
||||
|
||||
const GlobalHolder = React.forwardRef<GlobalHolderRef, {}>((_, ref) => {
|
||||
const [prefixCls, setPrefixCls] = React.useState<string>();
|
||||
const [container, setContainer] = React.useState<HTMLElement>();
|
||||
const [maxCount, setMaxCount] = React.useState<number | undefined>();
|
||||
const [rtl, setRTL] = React.useState<boolean | undefined>();
|
||||
const [top, setTop] = React.useState<number | undefined>();
|
||||
|
||||
const [api, holder] = useInternalMessage({
|
||||
prefixCls,
|
||||
getContainer: () => container!,
|
||||
maxCount,
|
||||
rtl,
|
||||
top,
|
||||
});
|
||||
|
||||
const global = globalConfig();
|
||||
const rootPrefixCls = global.getRootPrefixCls();
|
||||
const rootIconPrefixCls = global.getIconPrefixCls();
|
||||
|
||||
const sync = () => {
|
||||
const {
|
||||
prefixCls: nextGlobalPrefixCls,
|
||||
container: nextGlobalContainer,
|
||||
maxCount: nextGlobalMaxCount,
|
||||
rtl: nextGlobalRTL,
|
||||
top: nextTop,
|
||||
} = getGlobalContext();
|
||||
|
||||
setPrefixCls(nextGlobalPrefixCls);
|
||||
setContainer(nextGlobalContainer);
|
||||
setMaxCount(nextGlobalMaxCount);
|
||||
setRTL(nextGlobalRTL);
|
||||
setTop(nextTop);
|
||||
};
|
||||
|
||||
getRCNotificationInstance(args, ({ prefixCls, iconPrefixCls, instance }) => {
|
||||
instance.notice(
|
||||
getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls, iconPrefixCls),
|
||||
React.useEffect(sync, []);
|
||||
|
||||
React.useImperativeHandle(ref, () => {
|
||||
const instance: any = { ...api };
|
||||
|
||||
Object.keys(instance).forEach(method => {
|
||||
instance[method] = (...args: any[]) => {
|
||||
sync();
|
||||
return (api as any)[method](...args);
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
instance,
|
||||
sync,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider prefixCls={rootPrefixCls} iconPrefixCls={rootIconPrefixCls}>
|
||||
{holder}
|
||||
</ConfigProvider>
|
||||
);
|
||||
});
|
||||
|
||||
function flushNotice() {
|
||||
if (!message) {
|
||||
const holderFragment = document.createDocumentFragment();
|
||||
|
||||
const newMessage: GlobalMessage = {
|
||||
fragment: holderFragment,
|
||||
};
|
||||
|
||||
message = newMessage;
|
||||
|
||||
// Delay render to avoid sync issue
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalHolder
|
||||
ref={node => {
|
||||
const { instance, sync } = node || {};
|
||||
|
||||
// React 18 test env will throw if call immediately in ref
|
||||
Promise.resolve().then(() => {
|
||||
if (!newMessage.instance && instance) {
|
||||
newMessage.instance = instance;
|
||||
newMessage.sync = sync;
|
||||
flushNotice();
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
holderFragment,
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Notification not ready
|
||||
if (message && !message.instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// >>> Execute task
|
||||
taskQueue.forEach(task => {
|
||||
const { type, skipped } = task;
|
||||
|
||||
// Only `skipped` when user call notice but cancel it immediately
|
||||
// and instance not ready
|
||||
if (!skipped) {
|
||||
switch (type) {
|
||||
case 'open': {
|
||||
act(() => {
|
||||
const closeFn = message!.instance!.open({
|
||||
...defaultGlobalConfig,
|
||||
...task.config,
|
||||
});
|
||||
const result: any = () => {
|
||||
if (messageInstance) {
|
||||
messageInstance.removeNotice(target);
|
||||
|
||||
closeFn?.then(task.resolve);
|
||||
task.setCloseFn(closeFn);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'destroy':
|
||||
act(() => {
|
||||
message?.instance!.destroy(task.key);
|
||||
});
|
||||
break;
|
||||
|
||||
// Other type open
|
||||
default: {
|
||||
act(() => {
|
||||
const closeFn = message!.instance;
|
||||
|
||||
closeFn?.then(task.resolve);
|
||||
task.setCloseFn(closeFn);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up
|
||||
taskQueue = [];
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// == Export ==
|
||||
// ==============================================================================
|
||||
type MethodType = typeof methods[number];
|
||||
|
||||
function setMessageGlobalConfig(config: ConfigOptions) {
|
||||
defaultGlobalConfig = {
|
||||
...defaultGlobalConfig,
|
||||
...config,
|
||||
};
|
||||
|
||||
// Trigger sync for it
|
||||
act(() => {
|
||||
message?.sync?.();
|
||||
});
|
||||
}
|
||||
|
||||
function open(config: ArgsProps): MessageType {
|
||||
const result = wrapPromiseFn(resolve => {
|
||||
let closeFn: VoidFunction;
|
||||
|
||||
const task: OpenTask = {
|
||||
type: 'open',
|
||||
config,
|
||||
resolve,
|
||||
setCloseFn: fn => {
|
||||
closeFn = fn;
|
||||
},
|
||||
};
|
||||
taskQueue.push(task);
|
||||
|
||||
return () => {
|
||||
if (closeFn) {
|
||||
act(() => {
|
||||
closeFn();
|
||||
});
|
||||
} else {
|
||||
task.skipped = true;
|
||||
}
|
||||
};
|
||||
result.then = (filled: ThenableArgument, rejected: ThenableArgument) =>
|
||||
closePromise.then(filled, rejected);
|
||||
result.promise = closePromise;
|
||||
});
|
||||
|
||||
flushNotice();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
type ConfigContent = React.ReactNode;
|
||||
type ConfigDuration = number | (() => void);
|
||||
type JointContent = ConfigContent | ArgsProps;
|
||||
export type ConfigOnClose = () => void;
|
||||
function typeOpen(type: NoticeType, args: Parameters<TypeOpen>): MessageType {
|
||||
const result = wrapPromiseFn(resolve => {
|
||||
let closeFn: VoidFunction;
|
||||
|
||||
function isArgsProps(content: JointContent): content is ArgsProps {
|
||||
return (
|
||||
Object.prototype.toString.call(content) === '[object Object]' &&
|
||||
!!(content as ArgsProps).content
|
||||
);
|
||||
const task: TypeTask = {
|
||||
type,
|
||||
args,
|
||||
resolve,
|
||||
setCloseFn: fn => {
|
||||
closeFn = fn;
|
||||
},
|
||||
};
|
||||
|
||||
taskQueue.push(task);
|
||||
|
||||
return () => {
|
||||
if (closeFn) {
|
||||
act(() => {
|
||||
closeFn();
|
||||
});
|
||||
} else {
|
||||
task.skipped = true;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
flushNotice();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const api: any = {
|
||||
open: notice,
|
||||
config: setMessageConfig,
|
||||
destroy(messageKey?: React.Key) {
|
||||
if (messageInstance) {
|
||||
if (messageKey) {
|
||||
const { removeNotice } = messageInstance;
|
||||
removeNotice(messageKey);
|
||||
} else {
|
||||
const { destroy } = messageInstance;
|
||||
destroy();
|
||||
messageInstance = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
function destroy(key: React.Key) {
|
||||
taskQueue.push({
|
||||
type: 'destroy',
|
||||
key,
|
||||
});
|
||||
flushNotice();
|
||||
}
|
||||
|
||||
const baseStaticMethods: {
|
||||
open: (config: ArgsProps) => MessageType;
|
||||
destroy: (key?: React.Key) => void;
|
||||
config: any;
|
||||
useMessage: typeof useMessage;
|
||||
} = {
|
||||
open,
|
||||
destroy,
|
||||
config: setMessageGlobalConfig,
|
||||
useMessage,
|
||||
};
|
||||
|
||||
export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
|
||||
originalApi[type] = (
|
||||
content: JointContent,
|
||||
duration?: ConfigDuration,
|
||||
onClose?: ConfigOnClose,
|
||||
) => {
|
||||
if (isArgsProps(content)) {
|
||||
return originalApi.open({ ...content, type });
|
||||
}
|
||||
const staticMethods: typeof baseStaticMethods & Record<MethodType, TypeOpen> =
|
||||
baseStaticMethods as any;
|
||||
|
||||
if (typeof duration === 'function') {
|
||||
onClose = duration;
|
||||
duration = undefined;
|
||||
}
|
||||
methods.forEach(type => {
|
||||
staticMethods[type] = (...args: Parameters<TypeOpen>) => typeOpen(type, args);
|
||||
});
|
||||
|
||||
return originalApi.open({ content, duration, type, onClose });
|
||||
// ==============================================================================
|
||||
// == Test ==
|
||||
// ==============================================================================
|
||||
const noop = () => {};
|
||||
|
||||
/** @private Only Work in test env */
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let actWrapper: (wrapper: any) => void = noop;
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
actWrapper = wrapper => {
|
||||
act = wrapper;
|
||||
};
|
||||
}
|
||||
|
||||
typeList.forEach(type => attachTypeApi(api, type));
|
||||
/** @private Only Work in test env */
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let actDestroy = noop;
|
||||
|
||||
api.warn = api.warning;
|
||||
api.useMessage = createUseMessage(getRCNotificationInstance, getRCNoticeProps);
|
||||
|
||||
export interface MessageInstance {
|
||||
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||
error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||
open(args: ArgsProps): MessageType;
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
actDestroy = () => {
|
||||
message = null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageApi extends MessageInstance {
|
||||
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||
config(options: ConfigOptions): void;
|
||||
destroy(messageKey?: React.Key): void;
|
||||
useMessage(): [MessageInstance, React.ReactElement];
|
||||
}
|
||||
|
||||
/** @private test Only function. Not work on production */
|
||||
export const getInstance = () => (process.env.NODE_ENV === 'test' ? messageInstance : null);
|
||||
|
||||
export default api as MessageApi;
|
||||
export default staticMethods;
|
||||
|
47
components/message/interface.ts
Normal file
47
components/message/interface.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type * as React from 'react';
|
||||
|
||||
export type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
|
||||
|
||||
export interface ConfigOptions {
|
||||
top?: number;
|
||||
duration?: number;
|
||||
prefixCls?: string;
|
||||
getContainer?: () => HTMLElement;
|
||||
transitionName?: string;
|
||||
maxCount?: number;
|
||||
rtl?: boolean;
|
||||
}
|
||||
|
||||
export interface ArgsProps {
|
||||
content: React.ReactNode;
|
||||
duration?: number;
|
||||
type?: NoticeType;
|
||||
onClose?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
key?: string | number;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
export type JointContent = React.ReactNode | ArgsProps;
|
||||
|
||||
export interface MessageType extends PromiseLike<boolean> {
|
||||
(): void;
|
||||
}
|
||||
|
||||
export type TypeOpen = (
|
||||
content: JointContent,
|
||||
duration?: number | VoidFunction, // Also can use onClose directly
|
||||
onClose?: VoidFunction,
|
||||
) => MessageType;
|
||||
|
||||
export interface MessageInstance {
|
||||
info: TypeOpen;
|
||||
success: TypeOpen;
|
||||
error: TypeOpen;
|
||||
warning: TypeOpen;
|
||||
loading: TypeOpen;
|
||||
open(args: ArgsProps): MessageType;
|
||||
destroy(key?: React.Key): void;
|
||||
}
|
@ -51,9 +51,46 @@
|
||||
font-size: @font-size-lg;
|
||||
}
|
||||
|
||||
&-notice.@{ant-prefix}-move-up-leave.@{ant-prefix}-move-up-leave-active {
|
||||
&-move-up {
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&-move-up-appear,
|
||||
&-move-up-enter {
|
||||
animation-name: MessageMoveIn;
|
||||
animation-duration: 0.3s;
|
||||
animation-play-state: paused;
|
||||
animation-timing-function: @ease-out-circ;
|
||||
}
|
||||
|
||||
&-move-up-appear&-move-up-appear-active,
|
||||
&-move-up-enter&-move-up-enter-active {
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&-move-up-leave {
|
||||
animation-name: MessageMoveOut;
|
||||
animation-duration: 0.3s;
|
||||
animation-play-state: paused;
|
||||
animation-timing-function: @ease-out-circ;
|
||||
}
|
||||
|
||||
&-move-up-leave&-move-up-leave-active {
|
||||
animation-play-state: running;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes MessageMoveIn {
|
||||
0% {
|
||||
padding: 0;
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
padding: 8px;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
228
components/message/useMessage.tsx
Normal file
228
components/message/useMessage.tsx
Normal file
@ -0,0 +1,228 @@
|
||||
import * as React from 'react';
|
||||
import { useNotification as useRcNotification } from 'rc-notification/lib';
|
||||
import type { NotificationAPI } from 'rc-notification/lib';
|
||||
import classNames from 'classnames';
|
||||
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
|
||||
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
|
||||
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
||||
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
|
||||
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
|
||||
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import type {
|
||||
MessageInstance,
|
||||
ArgsProps,
|
||||
MessageType,
|
||||
ConfigOptions,
|
||||
NoticeType,
|
||||
TypeOpen,
|
||||
} from './interface';
|
||||
import { getMotion, wrapPromiseFn } from './util';
|
||||
import devWarning from '../_util/devWarning';
|
||||
|
||||
const TypeIcon = {
|
||||
info: <InfoCircleFilled />,
|
||||
success: <CheckCircleFilled />,
|
||||
error: <CloseCircleFilled />,
|
||||
warning: <ExclamationCircleFilled />,
|
||||
loading: <LoadingOutlined />,
|
||||
};
|
||||
|
||||
const DEFAULT_OFFSET = 8;
|
||||
const DEFAULT_DURATION = 3;
|
||||
|
||||
// ==============================================================================
|
||||
// == Holder ==
|
||||
// ==============================================================================
|
||||
type HolderProps = ConfigOptions & {
|
||||
onAllRemoved?: VoidFunction;
|
||||
};
|
||||
|
||||
interface HolderRef extends NotificationAPI {
|
||||
prefixCls: string;
|
||||
}
|
||||
|
||||
const Holder = React.forwardRef<HolderRef, HolderProps>((props, ref) => {
|
||||
const {
|
||||
top,
|
||||
prefixCls: staticPrefixCls,
|
||||
getContainer: staticGetContainer,
|
||||
maxCount,
|
||||
rtl,
|
||||
transitionName,
|
||||
onAllRemoved,
|
||||
} = props;
|
||||
const { getPrefixCls, getPopupContainer } = React.useContext(ConfigContext);
|
||||
|
||||
const prefixCls = staticPrefixCls || getPrefixCls('message');
|
||||
|
||||
// =============================== Style ===============================
|
||||
const getStyle = () => ({
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
top: top ?? DEFAULT_OFFSET,
|
||||
});
|
||||
|
||||
const getClassName = () => (rtl ? `${prefixCls}-rtl` : '');
|
||||
|
||||
// ============================== Motion ===============================
|
||||
const getNotificationMotion = () => getMotion(prefixCls, transitionName);
|
||||
|
||||
// ============================ Close Icon =============================
|
||||
const mergedCloseIcon = (
|
||||
<span className={`${prefixCls}-close-x`}>
|
||||
<CloseOutlined className={`${prefixCls}-close-icon`} />
|
||||
</span>
|
||||
);
|
||||
|
||||
// ============================== Origin ===============================
|
||||
const [api, holder] = useRcNotification({
|
||||
prefixCls,
|
||||
style: getStyle,
|
||||
className: getClassName,
|
||||
motion: getNotificationMotion,
|
||||
closable: false,
|
||||
closeIcon: mergedCloseIcon,
|
||||
duration: DEFAULT_DURATION,
|
||||
getContainer: () => staticGetContainer?.() || getPopupContainer?.() || document.body,
|
||||
maxCount,
|
||||
onAllRemoved,
|
||||
});
|
||||
|
||||
// ================================ Ref ================================
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
...api,
|
||||
prefixCls,
|
||||
}));
|
||||
|
||||
return holder;
|
||||
});
|
||||
|
||||
// ==============================================================================
|
||||
// == Hook ==
|
||||
// ==============================================================================
|
||||
let keyIndex = 0;
|
||||
|
||||
export function useInternalMessage(
|
||||
notificationConfig?: HolderProps,
|
||||
): [MessageInstance, React.ReactElement] {
|
||||
const holderRef = React.useRef<HolderRef>(null);
|
||||
|
||||
// ================================ API ================================
|
||||
const wrapAPI = React.useMemo<MessageInstance>(() => {
|
||||
// Wrap with notification content
|
||||
|
||||
// >>> close
|
||||
const close = (key: React.Key) => {
|
||||
holderRef.current?.close(key);
|
||||
};
|
||||
|
||||
// >>> Open
|
||||
const open = (config: ArgsProps): MessageType => {
|
||||
if (!holderRef.current) {
|
||||
devWarning(
|
||||
false,
|
||||
'Message',
|
||||
'You are calling notice in render which will break in React 18 concurrent mode. Please trigger in effect instead.',
|
||||
);
|
||||
|
||||
const fakeResult: any = () => {};
|
||||
fakeResult.then = () => {};
|
||||
return fakeResult;
|
||||
}
|
||||
|
||||
const { open: originOpen, prefixCls } = holderRef.current;
|
||||
const noticePrefixCls = `${prefixCls}-notice`;
|
||||
|
||||
const { content, icon, type, key, className, onClose, ...restConfig } = config;
|
||||
|
||||
let mergedKey: React.Key = key!;
|
||||
if (mergedKey === undefined || mergedKey === null) {
|
||||
keyIndex += 1;
|
||||
mergedKey = `antd-message-${keyIndex}`;
|
||||
}
|
||||
|
||||
return wrapPromiseFn(resolve => {
|
||||
originOpen({
|
||||
...restConfig,
|
||||
key: mergedKey,
|
||||
content: (
|
||||
<div className={classNames(`${prefixCls}-custom-content`, `${prefixCls}-${type}`)}>
|
||||
{icon || TypeIcon[type!]}
|
||||
<span>{content}</span>
|
||||
</div>
|
||||
),
|
||||
placement: 'top',
|
||||
className: classNames(type && `${noticePrefixCls}-${type}`, className),
|
||||
onClose: () => {
|
||||
onClose?.();
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
|
||||
// Return close function
|
||||
return () => {
|
||||
close(mergedKey);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// >>> destroy
|
||||
const destroy = (key?: React.Key) => {
|
||||
if (key !== undefined) {
|
||||
close(key);
|
||||
} else {
|
||||
holderRef.current?.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const clone = {
|
||||
open,
|
||||
destroy,
|
||||
} as MessageInstance;
|
||||
|
||||
const keys: NoticeType[] = ['info', 'success', 'warning', 'error', 'loading'];
|
||||
keys.forEach(type => {
|
||||
const typeOpen: TypeOpen = (jointContent, duration, onClose) => {
|
||||
let config: ArgsProps;
|
||||
if (jointContent && typeof jointContent === 'object' && 'content' in jointContent) {
|
||||
config = jointContent;
|
||||
} else {
|
||||
config = {
|
||||
content: jointContent,
|
||||
};
|
||||
}
|
||||
|
||||
// Params
|
||||
let mergedDuration: number | undefined;
|
||||
let mergedOnClose: VoidFunction | undefined;
|
||||
if (typeof duration === 'function') {
|
||||
mergedOnClose = duration;
|
||||
} else {
|
||||
mergedDuration = duration;
|
||||
mergedOnClose = onClose;
|
||||
}
|
||||
|
||||
const mergedConfig = {
|
||||
onClose: mergedOnClose,
|
||||
duration: mergedDuration,
|
||||
...config,
|
||||
type,
|
||||
};
|
||||
|
||||
return open(mergedConfig);
|
||||
};
|
||||
|
||||
clone[type] = typeOpen;
|
||||
});
|
||||
|
||||
return clone;
|
||||
}, []);
|
||||
|
||||
// ============================== Return ===============================
|
||||
return [wrapAPI, <Holder key="holder" {...notificationConfig} ref={holderRef} />];
|
||||
}
|
||||
|
||||
export default function useMessage(notificationConfig?: ConfigOptions) {
|
||||
return useInternalMessage(notificationConfig);
|
||||
}
|
28
components/message/util.ts
Normal file
28
components/message/util.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { CSSMotionProps } from 'rc-motion';
|
||||
|
||||
export function getMotion(prefixCls: string, transitionName?: string): CSSMotionProps {
|
||||
return {
|
||||
motionName: transitionName ?? `${prefixCls}-move-up`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Wrap message open with promise like function */
|
||||
export function wrapPromiseFn(openFn: (resolve: VoidFunction) => VoidFunction) {
|
||||
let closeFn: VoidFunction;
|
||||
|
||||
const closePromise = new Promise<boolean>(resolve => {
|
||||
closeFn = openFn(() => {
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
const result: any = () => {
|
||||
closeFn?.();
|
||||
};
|
||||
|
||||
result.then = (filled: VoidFunction, rejected: VoidFunction) =>
|
||||
closePromise.then(filled, rejected);
|
||||
result.promise = closePromise;
|
||||
|
||||
return result;
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import notification, { getInstance } from '..';
|
||||
import { sleep } from '../../../tests/utils';
|
||||
|
||||
describe('notification.config', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
notification.destroy();
|
||||
});
|
||||
|
||||
it('should be able to config maxCount', async () => {
|
||||
notification.config({
|
||||
maxCount: 5,
|
||||
duration: 0.5,
|
||||
});
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification message',
|
||||
key: i,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification last',
|
||||
key: '11',
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(5);
|
||||
expect(document.querySelectorAll('.ant-notification-notice')[4].textContent).toBe(
|
||||
'Notification last',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await sleep(500);
|
||||
});
|
||||
expect((await getInstance('ant-notification-topRight')).component.state.notices).toHaveLength(
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
87
components/notification/__tests__/config.test.tsx
Normal file
87
components/notification/__tests__/config.test.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import notification, { actWrapper } from '..';
|
||||
import { act } from '../../../tests/utils';
|
||||
import { awaitPromise, triggerMotionEnd } from './util';
|
||||
|
||||
describe('notification.config', () => {
|
||||
beforeAll(() => {
|
||||
actWrapper(act);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
notification.destroy();
|
||||
await triggerMotionEnd();
|
||||
|
||||
notification.config({
|
||||
prefixCls: null,
|
||||
getContainer: null,
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
await awaitPromise();
|
||||
});
|
||||
|
||||
it('should be able to config maxCount', async () => {
|
||||
notification.config({
|
||||
maxCount: 5,
|
||||
duration: 0.5,
|
||||
});
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
notification.open({
|
||||
message: 'Notification message',
|
||||
key: i,
|
||||
duration: 999,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await awaitPromise();
|
||||
|
||||
act(() => {
|
||||
// One frame is 16ms
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await triggerMotionEnd(false);
|
||||
|
||||
const count = document.querySelectorAll('.ant-notification-notice').length;
|
||||
expect(count).toBeLessThanOrEqual(5);
|
||||
}
|
||||
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification last',
|
||||
key: '11',
|
||||
duration: 999,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
// One frame is 16ms
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
await triggerMotionEnd(false);
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(5);
|
||||
expect(document.querySelectorAll('.ant-notification-notice')[4].textContent).toBe(
|
||||
'Notification last',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
await triggerMotionEnd(false);
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
|
||||
});
|
||||
});
|
@ -3,18 +3,15 @@ import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import notification from '..';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import { render, fireEvent } from '../../../tests/utils';
|
||||
|
||||
describe('notification.hooks', () => {
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
notification.destroy();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
@ -30,6 +27,7 @@ describe('notification.hooks', () => {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
api.open({
|
||||
message: null,
|
||||
description: (
|
||||
<Context.Consumer>
|
||||
{name => <span className="hook-test-result">{name}</span>}
|
||||
@ -45,10 +43,12 @@ describe('notification.hooks', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const wrapper = mount(<Demo />);
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(document.querySelectorAll('.my-test-notification-notice').length).toBe(1);
|
||||
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
||||
const { container } = render(<Demo />);
|
||||
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
expect(document.querySelectorAll('.my-test-notification-notice')).toHaveLength(1);
|
||||
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
|
||||
});
|
||||
|
||||
it('should work with success', () => {
|
||||
@ -64,6 +64,7 @@ describe('notification.hooks', () => {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
api.success({
|
||||
message: null,
|
||||
description: (
|
||||
<Context.Consumer>
|
||||
{name => <span className="hook-test-result">{name}</span>}
|
||||
@ -79,11 +80,12 @@ describe('notification.hooks', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const wrapper = mount(<Demo />);
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(document.querySelectorAll('.my-test-notification-notice').length).toBe(1);
|
||||
expect(document.querySelectorAll('.anticon-check-circle').length).toBe(1);
|
||||
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
||||
const { container } = render(<Demo />);
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
expect(document.querySelectorAll('.my-test-notification-notice')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.anticon-check-circle')).toHaveLength(1);
|
||||
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
|
||||
});
|
||||
|
||||
it('should be same hook', () => {
|
||||
@ -96,7 +98,7 @@ describe('notification.hooks', () => {
|
||||
React.useEffect(() => {
|
||||
count += 1;
|
||||
expect(count).toEqual(1);
|
||||
forceUpdate();
|
||||
forceUpdate({});
|
||||
}, [api]);
|
||||
|
||||
return null;
|
||||
@ -104,4 +106,53 @@ describe('notification.hooks', () => {
|
||||
|
||||
mount(<Demo />);
|
||||
});
|
||||
|
||||
describe('not break in effect', () => {
|
||||
it('basic', () => {
|
||||
const Demo = () => {
|
||||
const [api, holder] = notification.useNotification();
|
||||
|
||||
React.useEffect(() => {
|
||||
api.info({
|
||||
message: null,
|
||||
description: <div className="bamboo" />,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return holder;
|
||||
};
|
||||
|
||||
render(<Demo />);
|
||||
|
||||
expect(document.querySelector('.bamboo')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('warning if user call update in render', () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const Demo = () => {
|
||||
const [api, holder] = notification.useNotification();
|
||||
const calledRef = React.useRef(false);
|
||||
|
||||
if (!calledRef.current) {
|
||||
api.info({
|
||||
message: null,
|
||||
description: <div className="bamboo" />,
|
||||
});
|
||||
calledRef.current = true;
|
||||
}
|
||||
|
||||
return holder;
|
||||
};
|
||||
|
||||
render(<Demo />);
|
||||
|
||||
expect(document.querySelector('.bamboo')).toBeFalsy();
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Notification] You are calling notice in render which will break in React 18 concurrent mode. Please trigger in effect instead.',
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,333 +0,0 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import notification, { getInstance } from '..';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import { sleep } from '../../../tests/utils';
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
describe('notification', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runAllTimers();
|
||||
jest.useRealTimers();
|
||||
|
||||
act(() => {
|
||||
notification.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
it('not duplicate create holder', async () => {
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
prefixCls: 'additional-holder',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await sleep();
|
||||
|
||||
const count = document.querySelectorAll('.additional-holder').length;
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should be able to hide manually', async () => {
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title 1',
|
||||
duration: 0,
|
||||
key: '1',
|
||||
});
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title 2',
|
||||
duration: 0,
|
||||
key: '2',
|
||||
});
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(2);
|
||||
|
||||
act(() => {
|
||||
notification.close('1');
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect((await getInstance('ant-notification-topRight')).component.state.notices).toHaveLength(
|
||||
1,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
notification.close('2');
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect((await getInstance('ant-notification-topRight')).component.state.notices).toHaveLength(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to destroy globally', async () => {
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification').length).toBe(1);
|
||||
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(2);
|
||||
|
||||
act(() => {
|
||||
notification.destroy();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification').length).toBe(0);
|
||||
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should be able to destroy after config', () => {
|
||||
act(() => {
|
||||
notification.config({
|
||||
bottom: 100,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
notification.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to config rtl', () => {
|
||||
act(() => {
|
||||
notification.config({
|
||||
rtl: true,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'whatever',
|
||||
});
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-rtl').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should be able to global config rootPrefixCls', () => {
|
||||
ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
|
||||
|
||||
act(() => {
|
||||
notification.success({ message: 'Notification Title', duration: 0 });
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.prefix-test-notification-notice')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.bamboo-check-circle')).toHaveLength(1);
|
||||
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null });
|
||||
});
|
||||
|
||||
it('should be able to config prefixCls', () => {
|
||||
notification.config({
|
||||
prefixCls: 'prefix-test',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
});
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
|
||||
|
||||
notification.config({
|
||||
prefixCls: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to open with icon', async () => {
|
||||
const iconPrefix = '.ant-notification-notice-icon';
|
||||
|
||||
const openNotificationWithIcon = async type => {
|
||||
act(() => {
|
||||
notification[type]({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
description: 'This is the content of the notification.',
|
||||
});
|
||||
jest.runAllTimers();
|
||||
});
|
||||
};
|
||||
|
||||
const list = ['success', 'info', 'warning', 'error'];
|
||||
|
||||
const promises = list.map(type => openNotificationWithIcon(type));
|
||||
|
||||
await act(async () => {
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
list.forEach(type => {
|
||||
expect(document.querySelectorAll(`${iconPrefix}-${type}`).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to add parent class for different notification types', async () => {
|
||||
const openNotificationWithIcon = async type => {
|
||||
act(() => {
|
||||
notification[type]({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
description: 'This is the content of the notification.',
|
||||
});
|
||||
jest.runAllTimers();
|
||||
});
|
||||
};
|
||||
|
||||
const list = ['success', 'info', 'warning', 'error'];
|
||||
const promises = list.map(type => openNotificationWithIcon(type));
|
||||
|
||||
await act(async () => {
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
list.forEach(type => {
|
||||
expect(document.querySelectorAll(`.ant-notification-notice-${type}`).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('trigger onClick', () => {
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
});
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification').length).toBe(1);
|
||||
});
|
||||
|
||||
it('support closeIcon', () => {
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
closeIcon: <span className="test-customize-icon" />,
|
||||
});
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.test-customize-icon').length).toBe(1);
|
||||
});
|
||||
|
||||
it('support config closeIcon', () => {
|
||||
notification.config({
|
||||
closeIcon: <span className="test-customize-icon" />,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
closeIcon: <span className="test-customize-icon" />,
|
||||
});
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.test-customize-icon').length).toBe(1);
|
||||
});
|
||||
|
||||
it('closeIcon should be update', async () => {
|
||||
const openNotificationWithCloseIcon = async type => {
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
closeIcon: <span className={`test-customize-icon-${type}`} />,
|
||||
});
|
||||
jest.runAllTimers();
|
||||
});
|
||||
};
|
||||
|
||||
const list = ['1', '2'];
|
||||
const promises = list.map(type => openNotificationWithCloseIcon(type));
|
||||
|
||||
await act(async () => {
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
list.forEach(type => {
|
||||
expect(document.querySelectorAll(`.test-customize-icon-${type}`).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('support config duration', () => {
|
||||
notification.config({
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'whatever',
|
||||
});
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification').length).toBe(1);
|
||||
});
|
||||
|
||||
it('support icon', () => {
|
||||
act(() => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
icon: <UserOutlined />,
|
||||
});
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.anticon-user').length).toBe(1);
|
||||
});
|
||||
});
|
292
components/notification/__tests__/index.test.tsx
Normal file
292
components/notification/__tests__/index.test.tsx
Normal file
@ -0,0 +1,292 @@
|
||||
import React from 'react';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import notification, { actWrapper } from '..';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import { act, fireEvent } from '../../../tests/utils';
|
||||
import { awaitPromise, triggerMotionEnd } from './util';
|
||||
|
||||
describe('notification', () => {
|
||||
beforeAll(() => {
|
||||
actWrapper(act);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
notification.destroy();
|
||||
await triggerMotionEnd();
|
||||
|
||||
notification.config({
|
||||
prefixCls: null,
|
||||
getContainer: null,
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
await awaitPromise();
|
||||
});
|
||||
|
||||
it('not duplicate create holder', async () => {
|
||||
notification.config({
|
||||
prefixCls: 'additional-holder',
|
||||
});
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.additional-holder')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should be able to hide manually', async () => {
|
||||
notification.open({
|
||||
message: 'Notification Title 1',
|
||||
duration: 0,
|
||||
key: '1',
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
notification.open({
|
||||
message: 'Notification Title 2',
|
||||
duration: 0,
|
||||
key: '2',
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(2);
|
||||
|
||||
// Close 1
|
||||
notification.destroy('1');
|
||||
|
||||
await triggerMotionEnd();
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(1);
|
||||
|
||||
// Close 2
|
||||
notification.destroy('2');
|
||||
|
||||
await triggerMotionEnd();
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be able to destroy globally', async () => {
|
||||
notification.open({
|
||||
message: 'Notification Title 1',
|
||||
duration: 0,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
notification.open({
|
||||
message: 'Notification Title 2',
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(2);
|
||||
|
||||
notification.destroy();
|
||||
|
||||
await triggerMotionEnd();
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be able to destroy after config', () => {
|
||||
notification.config({
|
||||
bottom: 100,
|
||||
});
|
||||
|
||||
notification.destroy();
|
||||
});
|
||||
|
||||
it('should be able to config rtl', async () => {
|
||||
notification.config({
|
||||
rtl: true,
|
||||
});
|
||||
|
||||
notification.open({
|
||||
message: 'whatever',
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-rtl')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should be able to global config rootPrefixCls', async () => {
|
||||
ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
|
||||
|
||||
notification.success({ message: 'Notification Title', duration: 0 });
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.prefix-test-notification-notice')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.bamboo-check-circle')).toHaveLength(1);
|
||||
|
||||
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null! });
|
||||
});
|
||||
|
||||
it('should be able to config prefixCls', async () => {
|
||||
notification.config({
|
||||
prefixCls: 'prefix-test',
|
||||
});
|
||||
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
|
||||
expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
|
||||
|
||||
notification.config({
|
||||
prefixCls: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to open with icon', async () => {
|
||||
const iconPrefix = '.ant-notification-notice-icon';
|
||||
|
||||
const list = ['success', 'info', 'warning', 'error'] as const;
|
||||
|
||||
list.forEach(type => {
|
||||
notification[type]({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
description: 'This is the content of the notification.',
|
||||
});
|
||||
});
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
list.forEach(type => {
|
||||
expect(document.querySelectorAll(`${iconPrefix}-${type}`)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to add parent class for different notification types', async () => {
|
||||
const list = ['success', 'info', 'warning', 'error'] as const;
|
||||
list.forEach(type => {
|
||||
notification[type]({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
description: 'This is the content of the notification.',
|
||||
});
|
||||
});
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
list.forEach(type => {
|
||||
expect(document.querySelectorAll(`.ant-notification-notice-${type}`)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('trigger onClick', async () => {
|
||||
const onClick = jest.fn();
|
||||
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
onClick,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.ant-notification')).toHaveLength(1);
|
||||
|
||||
fireEvent.click(document.querySelector('.ant-notification-notice')!);
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('support closeIcon', async () => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
closeIcon: <span className="test-customize-icon" />,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelectorAll('.test-customize-icon')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('support config closeIcon', async () => {
|
||||
notification.config({
|
||||
closeIcon: <span className="test-customize-icon" />,
|
||||
});
|
||||
|
||||
// Global Icon
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.test-customize-icon')).toBeTruthy();
|
||||
|
||||
// Notice Icon
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
closeIcon: <span className="replace-icon" />,
|
||||
});
|
||||
|
||||
expect(document.querySelector('.replace-icon')).toBeTruthy();
|
||||
|
||||
notification.config({
|
||||
closeIcon: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('closeIcon should be update', async () => {
|
||||
const list = ['1', '2'];
|
||||
list.forEach(type => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
closeIcon: <span className={`test-customize-icon-${type}`} />,
|
||||
duration: 0,
|
||||
});
|
||||
});
|
||||
|
||||
await awaitPromise();
|
||||
|
||||
list.forEach(type => {
|
||||
expect(document.querySelector(`.test-customize-icon-${type}`)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('support config duration', async () => {
|
||||
notification.config({
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
notification.open({
|
||||
message: 'whatever',
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.ant-notification')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('support icon', async () => {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
icon: <UserOutlined />,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.anticon-user')).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,250 +0,0 @@
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import notification from '..';
|
||||
|
||||
describe('Notification.placement', () => {
|
||||
afterEach(() => notification.destroy());
|
||||
|
||||
function $$(className) {
|
||||
return document.body.querySelectorAll(className);
|
||||
}
|
||||
|
||||
function getStyle(el, prop) {
|
||||
const style = window.getComputedStyle ? window.getComputedStyle(el) : el.currentStyle;
|
||||
// If a css property's value is `auto`, it will return an empty string.
|
||||
return prop ? style[prop] : style;
|
||||
}
|
||||
|
||||
function open(args) {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
description: 'This is the content of the notification.',
|
||||
...args,
|
||||
});
|
||||
}
|
||||
|
||||
function config(args) {
|
||||
notification.config({
|
||||
...args,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
open();
|
||||
});
|
||||
}
|
||||
|
||||
describe('placement', () => {
|
||||
it('can be configured per notification using the `open` method', () => {
|
||||
const defaultTop = '24px';
|
||||
const defaultBottom = '24px';
|
||||
let style;
|
||||
|
||||
// top
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'top',
|
||||
top: 50,
|
||||
});
|
||||
});
|
||||
|
||||
style = getStyle($$('.ant-notification-top')[0]);
|
||||
expect(style.top).toBe('50px');
|
||||
expect(style.left).toBe('50%');
|
||||
expect(style.transform).toBe('translateX(-50%)');
|
||||
expect(style.right).toBe('');
|
||||
expect(style.bottom).toBe('');
|
||||
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'top',
|
||||
});
|
||||
});
|
||||
expect($$('.ant-notification-top').length).toBe(1);
|
||||
|
||||
// topLeft
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'topLeft',
|
||||
top: 50,
|
||||
});
|
||||
});
|
||||
style = getStyle($$('.ant-notification-topLeft')[0]);
|
||||
expect(style.top).toBe('50px');
|
||||
expect(style.left).toBe('0px');
|
||||
expect(style.bottom).toBe('');
|
||||
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'topLeft',
|
||||
});
|
||||
});
|
||||
expect($$('.ant-notification-topLeft').length).toBe(1);
|
||||
|
||||
// topRight
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'topRight',
|
||||
});
|
||||
});
|
||||
style = getStyle($$('.ant-notification-topRight')[0]);
|
||||
expect(style.top).toBe(defaultTop);
|
||||
expect(style.right).toBe('0px');
|
||||
expect(style.bottom).toBe('');
|
||||
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'topRight',
|
||||
});
|
||||
});
|
||||
expect($$('.ant-notification-topRight').length).toBe(1);
|
||||
|
||||
// bottom
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'bottom',
|
||||
bottom: 100,
|
||||
});
|
||||
});
|
||||
style = getStyle($$('.ant-notification-bottom')[0]);
|
||||
expect(style.top).toBe('');
|
||||
expect(style.left).toBe('50%');
|
||||
expect(style.transform).toBe('translateX(-50%)');
|
||||
expect(style.right).toBe('');
|
||||
expect(style.bottom).toBe('100px');
|
||||
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'bottom',
|
||||
});
|
||||
});
|
||||
expect($$('.ant-notification-bottom').length).toBe(1);
|
||||
|
||||
// bottomRight
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'bottomRight',
|
||||
bottom: 100,
|
||||
});
|
||||
});
|
||||
style = getStyle($$('.ant-notification-bottomRight')[0]);
|
||||
expect(style.top).toBe('');
|
||||
expect(style.right).toBe('0px');
|
||||
expect(style.bottom).toBe('100px');
|
||||
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
});
|
||||
expect($$('.ant-notification-bottomRight').length).toBe(1);
|
||||
|
||||
// bottomLeft
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'bottomLeft',
|
||||
});
|
||||
});
|
||||
style = getStyle($$('.ant-notification-bottomLeft')[0]);
|
||||
expect(style.top).toBe('');
|
||||
expect(style.left).toBe('0px');
|
||||
expect(style.bottom).toBe(defaultBottom);
|
||||
|
||||
act(() => {
|
||||
open({
|
||||
placement: 'bottomLeft',
|
||||
});
|
||||
});
|
||||
expect($$('.ant-notification-bottomLeft').length).toBe(1);
|
||||
});
|
||||
|
||||
it('can be configured globally using the `config` method', () => {
|
||||
let style;
|
||||
|
||||
// topLeft
|
||||
config({
|
||||
placement: 'topLeft',
|
||||
top: 50,
|
||||
bottom: 50,
|
||||
});
|
||||
style = getStyle($$('.ant-notification-topLeft')[0]);
|
||||
expect(style.top).toBe('50px');
|
||||
expect(style.left).toBe('0px');
|
||||
expect(style.bottom).toBe('');
|
||||
|
||||
// topRight
|
||||
config({
|
||||
placement: 'topRight',
|
||||
top: 100,
|
||||
bottom: 50,
|
||||
});
|
||||
style = getStyle($$('.ant-notification-topRight')[0]);
|
||||
expect(style.top).toBe('100px');
|
||||
expect(style.right).toBe('0px');
|
||||
expect(style.bottom).toBe('');
|
||||
|
||||
// bottomRight
|
||||
config({
|
||||
placement: 'bottomRight',
|
||||
top: 50,
|
||||
bottom: 100,
|
||||
});
|
||||
style = getStyle($$('.ant-notification-bottomRight')[0]);
|
||||
expect(style.top).toBe('');
|
||||
expect(style.right).toBe('0px');
|
||||
expect(style.bottom).toBe('100px');
|
||||
|
||||
// bottomLeft
|
||||
config({
|
||||
placement: 'bottomLeft',
|
||||
top: 100,
|
||||
bottom: 50,
|
||||
});
|
||||
style = getStyle($$('.ant-notification-bottomLeft')[0]);
|
||||
expect(style.top).toBe('');
|
||||
expect(style.left).toBe('0px');
|
||||
expect(style.bottom).toBe('50px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mountNode', () => {
|
||||
const $container = document.createElement('div');
|
||||
beforeEach(() => {
|
||||
document.body.appendChild($container);
|
||||
});
|
||||
afterEach(() => {
|
||||
$container.remove();
|
||||
});
|
||||
|
||||
it('can be configured per notification using the `open` method', () => {
|
||||
act(() => {
|
||||
open({
|
||||
getContainer: () => $container,
|
||||
});
|
||||
});
|
||||
|
||||
expect($container.querySelector('.ant-notification')).not.toBe(null);
|
||||
notification.destroy();
|
||||
setTimeout(() => {
|
||||
// Upcoming notifications still use their default mountNode and not $container
|
||||
act(() => {
|
||||
open();
|
||||
});
|
||||
expect($container.querySelector('.ant-notification')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be configured globally using the `config` method', () => {
|
||||
config({
|
||||
getContainer: () => $container,
|
||||
});
|
||||
expect($container.querySelector('.ant-notification')).not.toBe(null);
|
||||
notification.destroy();
|
||||
setTimeout(() => {
|
||||
// Upcoming notifications are mounted in $container
|
||||
act(() => {
|
||||
open();
|
||||
});
|
||||
expect($container.querySelector('.ant-notification')).not.toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
167
components/notification/__tests__/placement.test.tsx
Normal file
167
components/notification/__tests__/placement.test.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import notification, { actWrapper } from '..';
|
||||
import { act, fireEvent } from '../../../tests/utils';
|
||||
import type { ArgsProps, GlobalConfigProps } from '../interface';
|
||||
import { awaitPromise, triggerMotionEnd } from './util';
|
||||
|
||||
describe('Notification.placement', () => {
|
||||
function open(args?: Partial<ArgsProps>) {
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
description: 'This is the content of the notification.',
|
||||
...args,
|
||||
});
|
||||
}
|
||||
|
||||
function config(args: Partial<GlobalConfigProps>) {
|
||||
notification.config({
|
||||
...args,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
open();
|
||||
});
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
actWrapper(act);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
notification.destroy();
|
||||
await triggerMotionEnd();
|
||||
|
||||
notification.config({
|
||||
prefixCls: null,
|
||||
getContainer: null,
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
await awaitPromise();
|
||||
});
|
||||
|
||||
describe('placement', () => {
|
||||
it('can be configured globally using the `config` method', async () => {
|
||||
// topLeft
|
||||
config({
|
||||
placement: 'topLeft',
|
||||
top: 50,
|
||||
bottom: 50,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.ant-notification-topLeft')).toHaveStyle({
|
||||
top: '50px',
|
||||
left: '0px',
|
||||
bottom: '',
|
||||
});
|
||||
|
||||
// topRight
|
||||
config({
|
||||
placement: 'topRight',
|
||||
top: 100,
|
||||
bottom: 50,
|
||||
});
|
||||
|
||||
expect(document.querySelector('.ant-notification-topRight')).toHaveStyle({
|
||||
top: '100px',
|
||||
right: '0px',
|
||||
bottom: '',
|
||||
});
|
||||
|
||||
// bottomRight
|
||||
config({
|
||||
placement: 'bottomRight',
|
||||
top: 50,
|
||||
bottom: 100,
|
||||
});
|
||||
|
||||
expect(document.querySelector('.ant-notification-bottomRight')).toHaveStyle({
|
||||
top: '',
|
||||
right: '0px',
|
||||
bottom: '100px',
|
||||
});
|
||||
|
||||
// bottomLeft
|
||||
config({
|
||||
placement: 'bottomLeft',
|
||||
top: 100,
|
||||
bottom: 50,
|
||||
});
|
||||
|
||||
expect(document.querySelector('.ant-notification-bottomLeft')).toHaveStyle({
|
||||
top: '',
|
||||
left: '0px',
|
||||
bottom: '50px',
|
||||
});
|
||||
|
||||
// top
|
||||
config({
|
||||
placement: 'top',
|
||||
top: 50,
|
||||
bottom: 60,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.ant-notification-top')).toHaveStyle({
|
||||
top: '50px',
|
||||
left: '50%',
|
||||
bottom: '',
|
||||
});
|
||||
|
||||
// bottom
|
||||
config({
|
||||
placement: 'bottom',
|
||||
top: 50,
|
||||
bottom: 60,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.ant-notification-bottom')).toHaveStyle({
|
||||
top: '',
|
||||
left: '50%',
|
||||
bottom: '60px',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mountNode', () => {
|
||||
const $container = document.createElement('div');
|
||||
beforeEach(() => {
|
||||
document.body.appendChild($container);
|
||||
});
|
||||
afterEach(() => {
|
||||
$container.remove();
|
||||
});
|
||||
|
||||
it('can be configured globally using the `config` method', async () => {
|
||||
config({
|
||||
getContainer: () => $container,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect($container.querySelector('.ant-notification')).toBeTruthy();
|
||||
notification.destroy();
|
||||
|
||||
// Leave motion
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
document.querySelectorAll('.ant-notification-notice').forEach(ele => {
|
||||
fireEvent.animationEnd(ele);
|
||||
});
|
||||
expect($container.querySelector('.ant-notification')).toBeFalsy();
|
||||
|
||||
// Upcoming notifications are mounted in $container
|
||||
act(() => {
|
||||
open();
|
||||
});
|
||||
expect($container.querySelector('.ant-notification')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
30
components/notification/__tests__/util.ts
Normal file
30
components/notification/__tests__/util.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { act, fireEvent } from '../../../tests/utils';
|
||||
|
||||
export async function awaitPromise() {
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export async function triggerMotionEnd(runAllTimers: boolean = true) {
|
||||
await awaitPromise();
|
||||
|
||||
if (runAllTimers) {
|
||||
// Flush css motion state update
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// document.querySelectorAll('.ant-notification-fade-leave').forEach(ele => {
|
||||
// fireEvent.animationEnd(ele);
|
||||
// });
|
||||
document.querySelectorAll('[role="alert"]').forEach(ele => {
|
||||
fireEvent.animationEnd(ele.parentNode?.parentNode!);
|
||||
});
|
||||
|
||||
await awaitPromise();
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
---
|
||||
order: 8
|
||||
order: -1
|
||||
title:
|
||||
zh-CN: 通过 Hooks 获取上下文
|
||||
en-US: Get context with hooks
|
||||
zh-CN: Hooks 调用(推荐)
|
||||
en-US: Hooks usage (recommended)
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
通过 `notification.useNotification` 创建支持读取 context 的 `contextHolder`。
|
||||
通过 `notification.useNotification` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
|
||||
|
||||
## en-US
|
||||
|
||||
Use `notification.useNotification` to get `contextHolder` with context accessible issue.
|
||||
Use `notification.useNotification` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `notification` static method, because static method cannot consume context, and ConfigProvider data will not work.
|
||||
|
||||
```jsx
|
||||
import { Button, notification, Divider, Space } from 'antd';
|
||||
|
@ -14,7 +14,7 @@ title:
|
||||
To customize the style or font of the close button.
|
||||
|
||||
```jsx
|
||||
import { Button, notification } from 'antd';
|
||||
import { Button, notification, Space } from 'antd';
|
||||
|
||||
const close = () => {
|
||||
console.log(
|
||||
@ -25,9 +25,14 @@ const close = () => {
|
||||
const openNotification = () => {
|
||||
const key = `open${Date.now()}`;
|
||||
const btn = (
|
||||
<Button type="primary" size="small" onClick={() => notification.close(key)}>
|
||||
<Space>
|
||||
<Button type="link" size="small" onClick={() => notification.destroy()}>
|
||||
Destroy All
|
||||
</Button>
|
||||
<Button type="primary" size="small" onClick={() => notification.destroy(key)}>
|
||||
Confirm
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
|
@ -1,74 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import useRCNotification from 'rc-notification/lib/useNotification';
|
||||
import type {
|
||||
NotificationInstance as RCNotificationInstance,
|
||||
NoticeContent as RCNoticeContent,
|
||||
HolderReadyCallback as RCHolderReadyCallback,
|
||||
} from 'rc-notification/lib/Notification';
|
||||
import type { ConfigConsumerProps } from '../../config-provider';
|
||||
import { ConfigConsumer } from '../../config-provider';
|
||||
import type { NotificationInstance, ArgsProps } from '..';
|
||||
|
||||
export default function createUseNotification(
|
||||
getNotificationInstance: (
|
||||
args: ArgsProps,
|
||||
callback: (info: { prefixCls: string; instance: RCNotificationInstance }) => void,
|
||||
) => void,
|
||||
getRCNoticeProps: (args: ArgsProps, prefixCls: string) => RCNoticeContent,
|
||||
) {
|
||||
const useNotification = (): [NotificationInstance, React.ReactElement] => {
|
||||
// We can only get content by render
|
||||
let getPrefixCls: ConfigConsumerProps['getPrefixCls'];
|
||||
|
||||
// We create a proxy to handle delay created instance
|
||||
let innerInstance: RCNotificationInstance | null = null;
|
||||
const proxy = {
|
||||
add: (noticeProps: RCNoticeContent, holderCallback?: RCHolderReadyCallback) => {
|
||||
innerInstance?.component.add(noticeProps, holderCallback);
|
||||
},
|
||||
} as any;
|
||||
|
||||
const [hookNotify, holder] = useRCNotification(proxy);
|
||||
|
||||
function notify(args: ArgsProps) {
|
||||
const { prefixCls: customizePrefixCls } = args;
|
||||
const mergedPrefixCls = getPrefixCls('notification', customizePrefixCls);
|
||||
|
||||
getNotificationInstance(
|
||||
{
|
||||
...args,
|
||||
prefixCls: mergedPrefixCls,
|
||||
},
|
||||
({ prefixCls, instance }) => {
|
||||
innerInstance = instance;
|
||||
hookNotify(getRCNoticeProps(args, prefixCls));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Fill functions
|
||||
const hookApiRef = React.useRef<any>({});
|
||||
|
||||
hookApiRef.current.open = notify;
|
||||
|
||||
['success', 'info', 'warning', 'error'].forEach(type => {
|
||||
hookApiRef.current[type] = (args: ArgsProps) =>
|
||||
hookApiRef.current.open({
|
||||
...args,
|
||||
type,
|
||||
});
|
||||
});
|
||||
|
||||
return [
|
||||
hookApiRef.current,
|
||||
<ConfigConsumer key="holder">
|
||||
{(context: ConfigConsumerProps) => {
|
||||
({ getPrefixCls } = context);
|
||||
return holder;
|
||||
}}
|
||||
</ConfigConsumer>,
|
||||
];
|
||||
};
|
||||
|
||||
return useNotification;
|
||||
}
|
@ -24,8 +24,7 @@ To display a notification message at any of the four corners of the viewport. Ty
|
||||
- `notification.warning(config)`
|
||||
- `notification.warn(config)`
|
||||
- `notification.open(config)`
|
||||
- `notification.close(key: String)`
|
||||
- `notification.destroy()`
|
||||
- `notification.destroy(key?: String)`
|
||||
|
||||
The properties of config are as follows:
|
||||
|
||||
|
@ -1,345 +1,258 @@
|
||||
import * as React from 'react';
|
||||
import Notification from 'rc-notification';
|
||||
import type { NotificationInstance as RCNotificationInstance } from 'rc-notification/lib/Notification';
|
||||
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
||||
import classNames from 'classnames';
|
||||
import CheckCircleOutlined from '@ant-design/icons/CheckCircleOutlined';
|
||||
import CloseCircleOutlined from '@ant-design/icons/CloseCircleOutlined';
|
||||
import ExclamationCircleOutlined from '@ant-design/icons/ExclamationCircleOutlined';
|
||||
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
|
||||
import createUseNotification from './hooks/useNotification';
|
||||
import { render } from 'rc-util/lib/React/render';
|
||||
import useNotification, { useInternalNotification } from './useNotification';
|
||||
import type { ArgsProps, NotificationInstance, GlobalConfigProps } from './interface';
|
||||
import ConfigProvider, { globalConfig } from '../config-provider';
|
||||
|
||||
export type NotificationPlacement =
|
||||
| 'top'
|
||||
| 'topLeft'
|
||||
| 'topRight'
|
||||
| 'bottom'
|
||||
| 'bottomLeft'
|
||||
| 'bottomRight';
|
||||
let notification: GlobalNotification | null = null;
|
||||
|
||||
export type IconType = 'success' | 'info' | 'error' | 'warning';
|
||||
let act: (callback: VoidFunction) => Promise<void> | void = (callback: VoidFunction) => callback();
|
||||
|
||||
const notificationInstance: {
|
||||
[key: string]: Promise<RCNotificationInstance>;
|
||||
} = {};
|
||||
let defaultDuration = 4.5;
|
||||
let defaultTop = 24;
|
||||
let defaultBottom = 24;
|
||||
let defaultPrefixCls = '';
|
||||
let defaultPlacement: NotificationPlacement = 'topRight';
|
||||
let defaultGetContainer: () => HTMLElement;
|
||||
let defaultCloseIcon: React.ReactNode;
|
||||
let rtl = false;
|
||||
let maxCount: number;
|
||||
|
||||
export interface ConfigProps {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
duration?: number;
|
||||
prefixCls?: string;
|
||||
placement?: NotificationPlacement;
|
||||
getContainer?: () => HTMLElement;
|
||||
closeIcon?: React.ReactNode;
|
||||
rtl?: boolean;
|
||||
maxCount?: number;
|
||||
interface GlobalNotification {
|
||||
fragment: DocumentFragment;
|
||||
instance?: NotificationInstance | null;
|
||||
sync?: VoidFunction;
|
||||
}
|
||||
|
||||
function setNotificationConfig(options: ConfigProps) {
|
||||
const { duration, placement, bottom, top, getContainer, closeIcon, prefixCls } = options;
|
||||
if (prefixCls !== undefined) {
|
||||
defaultPrefixCls = prefixCls;
|
||||
type Task =
|
||||
| {
|
||||
type: 'open';
|
||||
config: ArgsProps;
|
||||
}
|
||||
if (duration !== undefined) {
|
||||
defaultDuration = duration;
|
||||
}
|
||||
if (placement !== undefined) {
|
||||
defaultPlacement = placement;
|
||||
} else if (options.rtl) {
|
||||
defaultPlacement = 'topLeft';
|
||||
}
|
||||
if (bottom !== undefined) {
|
||||
defaultBottom = bottom;
|
||||
}
|
||||
if (top !== undefined) {
|
||||
defaultTop = top;
|
||||
}
|
||||
if (getContainer !== undefined) {
|
||||
defaultGetContainer = getContainer;
|
||||
}
|
||||
if (closeIcon !== undefined) {
|
||||
defaultCloseIcon = closeIcon;
|
||||
}
|
||||
if (options.rtl !== undefined) {
|
||||
rtl = options.rtl;
|
||||
}
|
||||
if (options.maxCount !== undefined) {
|
||||
maxCount = options.maxCount;
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'destroy';
|
||||
key: React.Key;
|
||||
};
|
||||
|
||||
function getPlacementStyle(
|
||||
placement: NotificationPlacement,
|
||||
top: number = defaultTop,
|
||||
bottom: number = defaultBottom,
|
||||
) {
|
||||
let style;
|
||||
switch (placement) {
|
||||
case 'top':
|
||||
style = {
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
right: 'auto',
|
||||
top,
|
||||
bottom: 'auto',
|
||||
};
|
||||
break;
|
||||
case 'topLeft':
|
||||
style = {
|
||||
left: 0,
|
||||
top,
|
||||
bottom: 'auto',
|
||||
};
|
||||
break;
|
||||
case 'topRight':
|
||||
style = {
|
||||
right: 0,
|
||||
top,
|
||||
bottom: 'auto',
|
||||
};
|
||||
break;
|
||||
case 'bottom':
|
||||
style = {
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
right: 'auto',
|
||||
top: 'auto',
|
||||
bottom,
|
||||
};
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
style = {
|
||||
left: 0,
|
||||
top: 'auto',
|
||||
bottom,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
style = {
|
||||
right: 0,
|
||||
top: 'auto',
|
||||
bottom,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
let taskQueue: Task[] = [];
|
||||
|
||||
function getNotificationInstance(
|
||||
args: ArgsProps,
|
||||
callback: (info: {
|
||||
prefixCls: string;
|
||||
iconPrefixCls: string;
|
||||
instance: RCNotificationInstance;
|
||||
}) => void,
|
||||
) {
|
||||
let defaultGlobalConfig: GlobalConfigProps = {};
|
||||
|
||||
function getGlobalContext() {
|
||||
const {
|
||||
placement = defaultPlacement,
|
||||
prefixCls: globalPrefixCls,
|
||||
getContainer: globalGetContainer,
|
||||
rtl,
|
||||
maxCount,
|
||||
top,
|
||||
bottom,
|
||||
getContainer = defaultGetContainer,
|
||||
prefixCls: customizePrefixCls,
|
||||
} = args;
|
||||
const { getPrefixCls, getIconPrefixCls } = globalConfig();
|
||||
const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls);
|
||||
const iconPrefixCls = getIconPrefixCls();
|
||||
} = defaultGlobalConfig;
|
||||
const mergedPrefixCls = globalPrefixCls ?? globalConfig().getPrefixCls('notification');
|
||||
const mergedContainer = globalGetContainer?.() || document.body;
|
||||
|
||||
const cacheKey = `${prefixCls}-${placement}`;
|
||||
const cacheInstance = notificationInstance[cacheKey];
|
||||
return {
|
||||
prefixCls: mergedPrefixCls,
|
||||
container: mergedContainer,
|
||||
rtl,
|
||||
maxCount,
|
||||
top,
|
||||
bottom,
|
||||
};
|
||||
}
|
||||
|
||||
if (cacheInstance) {
|
||||
Promise.resolve(cacheInstance).then(instance => {
|
||||
callback({ prefixCls: `${prefixCls}-notice`, iconPrefixCls, instance });
|
||||
interface GlobalHolderRef {
|
||||
instance: NotificationInstance;
|
||||
sync: () => void;
|
||||
}
|
||||
|
||||
const GlobalHolder = React.forwardRef<GlobalHolderRef, {}>((_, ref) => {
|
||||
const [prefixCls, setPrefixCls] = React.useState<string>();
|
||||
const [container, setContainer] = React.useState<HTMLElement>();
|
||||
const [maxCount, setMaxCount] = React.useState<number | undefined>();
|
||||
const [rtl, setRTL] = React.useState<boolean | undefined>();
|
||||
const [top, setTop] = React.useState<number | undefined>();
|
||||
const [bottom, setBottom] = React.useState<number | undefined>();
|
||||
|
||||
const [api, holder] = useInternalNotification({
|
||||
prefixCls,
|
||||
getContainer: () => container!,
|
||||
maxCount,
|
||||
rtl,
|
||||
top,
|
||||
bottom,
|
||||
});
|
||||
|
||||
const global = globalConfig();
|
||||
const rootPrefixCls = global.getRootPrefixCls();
|
||||
const rootIconPrefixCls = global.getIconPrefixCls();
|
||||
|
||||
const sync = () => {
|
||||
const {
|
||||
prefixCls: nextGlobalPrefixCls,
|
||||
container: nextGlobalContainer,
|
||||
maxCount: nextGlobalMaxCount,
|
||||
rtl: nextGlobalRTL,
|
||||
top: nextTop,
|
||||
bottom: nextBottom,
|
||||
} = getGlobalContext();
|
||||
|
||||
setPrefixCls(nextGlobalPrefixCls);
|
||||
setContainer(nextGlobalContainer);
|
||||
setMaxCount(nextGlobalMaxCount);
|
||||
setRTL(nextGlobalRTL);
|
||||
setTop(nextTop);
|
||||
setBottom(nextBottom);
|
||||
};
|
||||
|
||||
React.useEffect(sync, []);
|
||||
|
||||
React.useImperativeHandle(ref, () => {
|
||||
const instance: any = { ...api };
|
||||
|
||||
Object.keys(instance).forEach(method => {
|
||||
instance[method] = (...args: any[]) => {
|
||||
sync();
|
||||
return (api as any)[method](...args);
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
instance,
|
||||
sync,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider prefixCls={rootPrefixCls} iconPrefixCls={rootIconPrefixCls}>
|
||||
{holder}
|
||||
</ConfigProvider>
|
||||
);
|
||||
});
|
||||
|
||||
function flushNotice() {
|
||||
if (!notification) {
|
||||
const holderFragment = document.createDocumentFragment();
|
||||
|
||||
const newNotification: GlobalNotification = {
|
||||
fragment: holderFragment,
|
||||
};
|
||||
|
||||
notification = newNotification;
|
||||
|
||||
// Delay render to avoid sync issue
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalHolder
|
||||
ref={node => {
|
||||
const { instance, sync } = node || {};
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
if (!newNotification.instance && instance) {
|
||||
newNotification.instance = instance;
|
||||
newNotification.sync = sync;
|
||||
flushNotice();
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
holderFragment,
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationClass = classNames(`${prefixCls}-${placement}`, {
|
||||
[`${prefixCls}-rtl`]: rtl === true,
|
||||
});
|
||||
|
||||
notificationInstance[cacheKey] = new Promise(resolve => {
|
||||
Notification.newInstance(
|
||||
{
|
||||
prefixCls,
|
||||
className: notificationClass,
|
||||
style: getPlacementStyle(placement, top, bottom),
|
||||
getContainer,
|
||||
maxCount,
|
||||
},
|
||||
notification => {
|
||||
resolve(notification);
|
||||
callback({
|
||||
prefixCls: `${prefixCls}-notice`,
|
||||
iconPrefixCls,
|
||||
instance: notification,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const typeToIcon = {
|
||||
success: CheckCircleOutlined,
|
||||
info: InfoCircleOutlined,
|
||||
error: CloseCircleOutlined,
|
||||
warning: ExclamationCircleOutlined,
|
||||
};
|
||||
|
||||
export interface ArgsProps {
|
||||
message: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
btn?: React.ReactNode;
|
||||
key?: string;
|
||||
onClose?: () => void;
|
||||
duration?: number | null;
|
||||
icon?: React.ReactNode;
|
||||
placement?: NotificationPlacement;
|
||||
maxCount?: number;
|
||||
style?: React.CSSProperties;
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
readonly type?: IconType;
|
||||
onClick?: () => void;
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
getContainer?: () => HTMLElement;
|
||||
closeIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
function getRCNoticeProps(args: ArgsProps, prefixCls: string, iconPrefixCls?: string) {
|
||||
const {
|
||||
duration: durationArg,
|
||||
icon,
|
||||
type,
|
||||
description,
|
||||
message,
|
||||
btn,
|
||||
onClose,
|
||||
onClick,
|
||||
key,
|
||||
style,
|
||||
className,
|
||||
closeIcon = defaultCloseIcon,
|
||||
} = args;
|
||||
|
||||
const duration = durationArg === undefined ? defaultDuration : durationArg;
|
||||
|
||||
let iconNode: React.ReactNode = null;
|
||||
if (icon) {
|
||||
iconNode = <span className={`${prefixCls}-icon`}>{args.icon}</span>;
|
||||
} else if (type) {
|
||||
iconNode = React.createElement(typeToIcon[type] || null, {
|
||||
className: `${prefixCls}-icon ${prefixCls}-icon-${type}`,
|
||||
});
|
||||
// Notification not ready
|
||||
if (notification && !notification.instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closeIconToRender = (
|
||||
<span className={`${prefixCls}-close-x`}>
|
||||
{closeIcon || <CloseOutlined className={`${prefixCls}-close-icon`} />}
|
||||
</span>
|
||||
);
|
||||
// >>> Execute task
|
||||
taskQueue.forEach(task => {
|
||||
// eslint-disable-next-line default-case
|
||||
switch (task.type) {
|
||||
case 'open': {
|
||||
act(() => {
|
||||
notification!.instance!.open({
|
||||
...defaultGlobalConfig,
|
||||
...task.config,
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
const autoMarginTag =
|
||||
!description && iconNode ? (
|
||||
<span className={`${prefixCls}-message-single-line-auto-margin`} />
|
||||
) : null;
|
||||
case 'destroy':
|
||||
act(() => {
|
||||
notification?.instance!.destroy(task.key);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
content: (
|
||||
<ConfigProvider iconPrefixCls={iconPrefixCls}>
|
||||
<div className={iconNode ? `${prefixCls}-with-icon` : ''} role="alert">
|
||||
{iconNode}
|
||||
<div className={`${prefixCls}-message`}>
|
||||
{autoMarginTag}
|
||||
{message}
|
||||
</div>
|
||||
<div className={`${prefixCls}-description`}>{description}</div>
|
||||
{btn ? <span className={`${prefixCls}-btn`}>{btn}</span> : null}
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
),
|
||||
duration,
|
||||
closable: true,
|
||||
closeIcon: closeIconToRender,
|
||||
onClose,
|
||||
onClick,
|
||||
key,
|
||||
style: style || {},
|
||||
className: classNames(className, {
|
||||
[`${prefixCls}-${type}`]: !!type,
|
||||
}),
|
||||
// Clean up
|
||||
taskQueue = [];
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// == Export ==
|
||||
// ==============================================================================
|
||||
const methods = ['success', 'info', 'warning', 'error'] as const;
|
||||
type MethodType = typeof methods[number];
|
||||
|
||||
function setNotificationGlobalConfig(config: GlobalConfigProps) {
|
||||
defaultGlobalConfig = {
|
||||
...defaultGlobalConfig,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
function notice(args: ArgsProps) {
|
||||
getNotificationInstance(args, ({ prefixCls, iconPrefixCls, instance }) => {
|
||||
instance.notice(getRCNoticeProps(args, prefixCls, iconPrefixCls));
|
||||
// Trigger sync for it
|
||||
act(() => {
|
||||
notification?.sync?.();
|
||||
});
|
||||
}
|
||||
|
||||
const api: any = {
|
||||
open: notice,
|
||||
close(key: string) {
|
||||
Object.keys(notificationInstance).forEach(cacheKey =>
|
||||
Promise.resolve(notificationInstance[cacheKey]).then(instance => {
|
||||
instance.removeNotice(key);
|
||||
}),
|
||||
);
|
||||
},
|
||||
config: setNotificationConfig,
|
||||
destroy() {
|
||||
Object.keys(notificationInstance).forEach(cacheKey => {
|
||||
Promise.resolve(notificationInstance[cacheKey]).then(instance => {
|
||||
instance.destroy();
|
||||
function open(config: ArgsProps) {
|
||||
taskQueue.push({
|
||||
type: 'open',
|
||||
config,
|
||||
});
|
||||
delete notificationInstance[cacheKey]; // lgtm[js/missing-await]
|
||||
flushNotice();
|
||||
}
|
||||
|
||||
function destroy(key: React.Key) {
|
||||
taskQueue.push({
|
||||
type: 'destroy',
|
||||
key,
|
||||
});
|
||||
},
|
||||
flushNotice();
|
||||
}
|
||||
|
||||
const baseStaticMethods: {
|
||||
open: (config: ArgsProps) => void;
|
||||
destroy: (key?: React.Key) => void;
|
||||
config: any;
|
||||
useNotification: typeof useNotification;
|
||||
} = {
|
||||
open,
|
||||
destroy,
|
||||
config: setNotificationGlobalConfig,
|
||||
useNotification,
|
||||
};
|
||||
|
||||
['success', 'info', 'warning', 'error'].forEach(type => {
|
||||
api[type] = (args: ArgsProps) =>
|
||||
api.open({
|
||||
...args,
|
||||
const staticMethods: typeof baseStaticMethods & Record<MethodType, (config: ArgsProps) => void> =
|
||||
baseStaticMethods as any;
|
||||
|
||||
methods.forEach(type => {
|
||||
staticMethods[type] = config =>
|
||||
open({
|
||||
...config,
|
||||
type,
|
||||
});
|
||||
});
|
||||
|
||||
api.warn = api.warning;
|
||||
api.useNotification = createUseNotification(getNotificationInstance, getRCNoticeProps);
|
||||
// ==============================================================================
|
||||
// == Test ==
|
||||
// ==============================================================================
|
||||
const noop = () => {};
|
||||
|
||||
export interface NotificationInstance {
|
||||
success(args: ArgsProps): void;
|
||||
error(args: ArgsProps): void;
|
||||
info(args: ArgsProps): void;
|
||||
warning(args: ArgsProps): void;
|
||||
open(args: ArgsProps): void;
|
||||
/** @private Only Work in test env */
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let actWrapper: (wrapper: any) => void = noop;
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
actWrapper = wrapper => {
|
||||
act = wrapper;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NotificationApi extends NotificationInstance {
|
||||
warn(args: ArgsProps): void;
|
||||
close(key: string): void;
|
||||
config(options: ConfigProps): void;
|
||||
destroy(): void;
|
||||
|
||||
// Hooks
|
||||
useNotification: () => [NotificationInstance, React.ReactElement];
|
||||
}
|
||||
|
||||
/** @private test Only function. Not work on production */
|
||||
export const getInstance = async (cacheKey: string) =>
|
||||
process.env.NODE_ENV === 'test' ? notificationInstance[cacheKey] : null;
|
||||
|
||||
export default api as NotificationApi;
|
||||
export default staticMethods;
|
||||
|
@ -25,8 +25,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Jxm5nw61w/Notification.svg
|
||||
- `notification.warning(config)`
|
||||
- `notification.warn(config)`
|
||||
- `notification.open(config)`
|
||||
- `notification.close(key: String)`
|
||||
- `notification.destroy()`
|
||||
- `notification.destroy(key?: String)`
|
||||
|
||||
config 参数如下:
|
||||
|
||||
|
57
components/notification/interface.ts
Normal file
57
components/notification/interface.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import type * as React from 'react';
|
||||
|
||||
export type NotificationPlacement =
|
||||
| 'top'
|
||||
| 'topLeft'
|
||||
| 'topRight'
|
||||
| 'bottom'
|
||||
| 'bottomLeft'
|
||||
| 'bottomRight';
|
||||
|
||||
export type IconType = 'success' | 'info' | 'error' | 'warning';
|
||||
|
||||
export interface ArgsProps {
|
||||
message: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
btn?: React.ReactNode;
|
||||
key?: React.Key;
|
||||
onClose?: () => void;
|
||||
duration?: number | null;
|
||||
icon?: React.ReactNode;
|
||||
placement?: NotificationPlacement;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
readonly type?: IconType;
|
||||
onClick?: () => void;
|
||||
closeIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface NotificationInstance {
|
||||
success(args: ArgsProps): void;
|
||||
error(args: ArgsProps): void;
|
||||
info(args: ArgsProps): void;
|
||||
warning(args: ArgsProps): void;
|
||||
open(args: ArgsProps): void;
|
||||
destroy(key?: React.Key): void;
|
||||
}
|
||||
|
||||
export interface GlobalConfigProps {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
duration?: number;
|
||||
prefixCls?: string;
|
||||
getContainer?: () => HTMLElement;
|
||||
placement?: NotificationPlacement;
|
||||
closeIcon?: React.ReactNode;
|
||||
rtl?: boolean;
|
||||
maxCount?: number;
|
||||
}
|
||||
|
||||
export interface NotificationConfig {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
prefixCls?: string;
|
||||
getContainer?: () => HTMLElement;
|
||||
maxCount?: number;
|
||||
rtl?: boolean;
|
||||
}
|
192
components/notification/useNotification.tsx
Normal file
192
components/notification/useNotification.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
import * as React from 'react';
|
||||
import { useNotification as useRcNotification } from 'rc-notification/lib';
|
||||
import type { NotificationAPI } from 'rc-notification/lib';
|
||||
import classNames from 'classnames';
|
||||
import CheckCircleOutlined from '@ant-design/icons/CheckCircleOutlined';
|
||||
import CloseCircleOutlined from '@ant-design/icons/CloseCircleOutlined';
|
||||
import ExclamationCircleOutlined from '@ant-design/icons/ExclamationCircleOutlined';
|
||||
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
|
||||
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import type {
|
||||
NotificationInstance,
|
||||
ArgsProps,
|
||||
NotificationPlacement,
|
||||
NotificationConfig,
|
||||
} from './interface';
|
||||
import { getPlacementStyle, getMotion } from './util';
|
||||
import devWarning from '../_util/devWarning';
|
||||
|
||||
const typeToIcon = {
|
||||
success: CheckCircleOutlined,
|
||||
info: InfoCircleOutlined,
|
||||
error: CloseCircleOutlined,
|
||||
warning: ExclamationCircleOutlined,
|
||||
};
|
||||
|
||||
const DEFAULT_OFFSET = 24;
|
||||
const DEFAULT_DURATION = 4.5;
|
||||
|
||||
// ==============================================================================
|
||||
// == Holder ==
|
||||
// ==============================================================================
|
||||
type HolderProps = NotificationConfig & {
|
||||
onAllRemoved?: VoidFunction;
|
||||
};
|
||||
|
||||
interface HolderRef extends NotificationAPI {
|
||||
prefixCls: string;
|
||||
}
|
||||
|
||||
const Holder = React.forwardRef<HolderRef, HolderProps>((props, ref) => {
|
||||
const {
|
||||
top,
|
||||
bottom,
|
||||
prefixCls: staticPrefixCls,
|
||||
getContainer: staticGetContainer,
|
||||
maxCount,
|
||||
rtl,
|
||||
onAllRemoved,
|
||||
} = props;
|
||||
const { getPrefixCls, getPopupContainer } = React.useContext(ConfigContext);
|
||||
|
||||
const prefixCls = staticPrefixCls || getPrefixCls('notification');
|
||||
|
||||
// =============================== Style ===============================
|
||||
const getStyle = (placement: NotificationPlacement) =>
|
||||
getPlacementStyle(placement, top ?? DEFAULT_OFFSET, bottom ?? DEFAULT_OFFSET);
|
||||
|
||||
const getClassName = () => (rtl ? `${prefixCls}-rtl` : '');
|
||||
|
||||
// ============================== Motion ===============================
|
||||
const getNotificationMotion = () => getMotion(prefixCls);
|
||||
|
||||
// ============================ Close Icon =============================
|
||||
const mergedCloseIcon = (
|
||||
<span className={`${prefixCls}-close-x`}>
|
||||
<CloseOutlined className={`${prefixCls}-close-icon`} />
|
||||
</span>
|
||||
);
|
||||
|
||||
// ============================== Origin ===============================
|
||||
const [api, holder] = useRcNotification({
|
||||
prefixCls,
|
||||
style: getStyle,
|
||||
className: getClassName,
|
||||
motion: getNotificationMotion,
|
||||
closable: true,
|
||||
closeIcon: mergedCloseIcon,
|
||||
duration: DEFAULT_DURATION,
|
||||
getContainer: () => staticGetContainer?.() || getPopupContainer?.() || document.body,
|
||||
maxCount,
|
||||
onAllRemoved,
|
||||
});
|
||||
|
||||
// ================================ Ref ================================
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
...api,
|
||||
prefixCls,
|
||||
}));
|
||||
|
||||
return holder;
|
||||
});
|
||||
|
||||
// ==============================================================================
|
||||
// == Hook ==
|
||||
// ==============================================================================
|
||||
export function useInternalNotification(
|
||||
notificationConfig?: HolderProps,
|
||||
): [NotificationInstance, React.ReactElement] {
|
||||
const holderRef = React.useRef<HolderRef>(null);
|
||||
|
||||
// ================================ API ================================
|
||||
const wrapAPI = React.useMemo<NotificationInstance>(() => {
|
||||
// Wrap with notification content
|
||||
|
||||
// >>> Open
|
||||
const open = (config: ArgsProps) => {
|
||||
if (!holderRef.current) {
|
||||
devWarning(
|
||||
false,
|
||||
'Notification',
|
||||
'You are calling notice in render which will break in React 18 concurrent mode. Please trigger in effect instead.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { open: originOpen, prefixCls } = holderRef.current;
|
||||
const noticePrefixCls = `${prefixCls}-notice`;
|
||||
|
||||
const {
|
||||
message,
|
||||
description,
|
||||
icon,
|
||||
type,
|
||||
placement = 'topRight',
|
||||
btn,
|
||||
className,
|
||||
...restConfig
|
||||
} = config;
|
||||
|
||||
let iconNode: React.ReactNode = null;
|
||||
if (icon) {
|
||||
iconNode = <span className={`${noticePrefixCls}-icon`}>{icon}</span>;
|
||||
} else if (type) {
|
||||
iconNode = React.createElement(typeToIcon[type] || null, {
|
||||
className: classNames(`${noticePrefixCls}-icon`, `${noticePrefixCls}-icon-${type}`),
|
||||
});
|
||||
}
|
||||
|
||||
return originOpen({
|
||||
...restConfig,
|
||||
content: (
|
||||
<div
|
||||
className={classNames({
|
||||
[`${noticePrefixCls}-with-icon`]: iconNode,
|
||||
})}
|
||||
role="alert"
|
||||
>
|
||||
{iconNode}
|
||||
<div className={`${noticePrefixCls}-message`}>{message}</div>
|
||||
<div className={`${noticePrefixCls}-description`}>{description}</div>
|
||||
{btn && <div className={`${noticePrefixCls}-btn`}>{btn}</div>}
|
||||
</div>
|
||||
),
|
||||
placement,
|
||||
className: classNames(type && `${noticePrefixCls}-${type}`, className),
|
||||
});
|
||||
};
|
||||
|
||||
// >>> destroy
|
||||
const destroy = (key?: React.Key) => {
|
||||
if (key !== undefined) {
|
||||
holderRef.current?.close(key);
|
||||
} else {
|
||||
holderRef.current?.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const clone = {
|
||||
open,
|
||||
destroy,
|
||||
} as NotificationInstance;
|
||||
|
||||
const keys = ['success', 'info', 'warning', 'error'] as const;
|
||||
keys.forEach(type => {
|
||||
clone[type] = config =>
|
||||
open({
|
||||
...config,
|
||||
type,
|
||||
});
|
||||
});
|
||||
|
||||
return clone;
|
||||
}, []);
|
||||
|
||||
// ============================== Return ===============================
|
||||
return [wrapAPI, <Holder key="holder" {...notificationConfig} ref={holderRef} />];
|
||||
}
|
||||
|
||||
export default function useNotification(notificationConfig?: NotificationConfig) {
|
||||
return useInternalNotification(notificationConfig);
|
||||
}
|
68
components/notification/util.ts
Normal file
68
components/notification/util.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import type * as React from 'react';
|
||||
import type { CSSMotionProps } from 'rc-motion';
|
||||
import type { NotificationPlacement } from './interface';
|
||||
|
||||
export function getPlacementStyle(placement: NotificationPlacement, top: number, bottom: number) {
|
||||
let style: React.CSSProperties;
|
||||
|
||||
switch (placement) {
|
||||
case 'top':
|
||||
style = {
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
right: 'auto',
|
||||
top,
|
||||
bottom: 'auto',
|
||||
};
|
||||
break;
|
||||
|
||||
case 'topLeft':
|
||||
style = {
|
||||
left: 0,
|
||||
top,
|
||||
bottom: 'auto',
|
||||
};
|
||||
break;
|
||||
|
||||
case 'topRight':
|
||||
style = {
|
||||
right: 0,
|
||||
top,
|
||||
bottom: 'auto',
|
||||
};
|
||||
break;
|
||||
|
||||
case 'bottom':
|
||||
style = {
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
right: 'auto',
|
||||
top: 'auto',
|
||||
bottom,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'bottomLeft':
|
||||
style = {
|
||||
left: 0,
|
||||
top: 'auto',
|
||||
bottom,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
style = {
|
||||
right: 0,
|
||||
top: 'auto',
|
||||
bottom,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
export function getMotion(prefixCls: string): CSSMotionProps {
|
||||
return {
|
||||
motionName: `${prefixCls}-fade`,
|
||||
};
|
||||
}
|
@ -138,7 +138,7 @@
|
||||
"rc-mentions": "~1.8.0",
|
||||
"rc-menu": "~9.6.0",
|
||||
"rc-motion": "^2.5.1",
|
||||
"rc-notification": "~4.6.0",
|
||||
"rc-notification": "~5.0.0-alpha.8",
|
||||
"rc-pagination": "~3.1.9",
|
||||
"rc-picker": "~2.6.4",
|
||||
"rc-progress": "~3.2.1",
|
||||
@ -157,7 +157,7 @@
|
||||
"rc-tree-select": "~5.3.0",
|
||||
"rc-trigger": "^5.2.10",
|
||||
"rc-upload": "~4.3.0",
|
||||
"rc-util": "^5.20.0",
|
||||
"rc-util": "^5.21.3",
|
||||
"scroll-into-view-if-needed": "^2.2.25",
|
||||
"shallowequal": "^1.1.0"
|
||||
},
|
||||
@ -306,7 +306,7 @@
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/antd.min.js",
|
||||
"maxSize": "356 kB"
|
||||
"maxSize": "358 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/antd.min.css",
|
||||
|
@ -327,7 +327,7 @@ class Footer extends React.Component<WrappedComponentProps & { location: any }>
|
||||
intl: { messages },
|
||||
} = this.props;
|
||||
message.loading({
|
||||
content: messages['app.footer.primary-color-changing'],
|
||||
content: messages['app.footer.primary-color-changing'] as string,
|
||||
key: 'change-primary-color',
|
||||
});
|
||||
const changeColor = () => {
|
||||
@ -337,7 +337,7 @@ class Footer extends React.Component<WrappedComponentProps & { location: any }>
|
||||
})
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: messages['app.footer.primary-color-changed'],
|
||||
content: messages['app.footer.primary-color-changed'] as string,
|
||||
key: 'change-primary-color',
|
||||
});
|
||||
this.setState({ color });
|
||||
|
@ -1,6 +1,10 @@
|
||||
# V5 breaking change 记录
|
||||
|
||||
- getPopupContainer: 所有的 getPopupContainer 都需要保证返回的是唯一的 div。React 18 concurrent 下会反复调用该方法。
|
||||
- Dropdown
|
||||
- 魔改包裹元素样式移除,请使用 Space 组件
|
||||
- DropdownButton 的 prefixCls 改为 `dropdown`
|
||||
- Upload List 结构变化
|
||||
- Notification
|
||||
- 静态方法不在允许在 `open` 中动态设置 `prefixCls` `maxCount` `top` `bottom` `getContainer`,Notification 静态方法现在将只有一个实例。如果需要不同配置,请使用 `useNotification`。
|
||||
- close 改名为 destroy 和 message 保持一致
|
||||
|
Loading…
Reference in New Issue
Block a user