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();
|
return suffixCls ? `${getGlobalPrefixCls()}-${suffixCls}` : getGlobalPrefixCls();
|
||||||
},
|
},
|
||||||
getIconPrefixCls: getGlobalIconPrefixCls,
|
getIconPrefixCls: getGlobalIconPrefixCls,
|
||||||
getRootPrefixCls: (rootPrefixCls?: string, customizePrefixCls?: string) => {
|
getRootPrefixCls: () => {
|
||||||
// Customize rootPrefixCls is first priority
|
|
||||||
if (rootPrefixCls) {
|
|
||||||
return rootPrefixCls;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Global prefixCls provided, use this
|
// If Global prefixCls provided, use this
|
||||||
if (globalPrefixCls) {
|
if (globalPrefixCls) {
|
||||||
return 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
|
// Fallback to default prefixCls
|
||||||
return getGlobalPrefixCls();
|
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 */
|
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import message, { getInstance } from '..';
|
import message from '..';
|
||||||
import ConfigProvider from '../../config-provider';
|
import ConfigProvider from '../../config-provider';
|
||||||
|
import { render, fireEvent } from '../../../tests/utils';
|
||||||
|
import { triggerMotionEnd } from './util';
|
||||||
|
|
||||||
describe('message.hooks', () => {
|
describe('message.hooks', () => {
|
||||||
beforeAll(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
message.destroy();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@ -46,10 +43,11 @@ describe('message.hooks', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = mount(<Demo />);
|
const { container } = render(<Demo />);
|
||||||
wrapper.find('button').simulate('click');
|
fireEvent.click(container.querySelector('button')!);
|
||||||
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
|
||||||
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
|
||||||
|
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with success', () => {
|
it('should work with success', () => {
|
||||||
@ -80,16 +78,15 @@ describe('message.hooks', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = mount(<Demo />);
|
const { container } = render(<Demo />);
|
||||||
wrapper.find('button').simulate('click');
|
fireEvent.click(container.querySelector('button')!);
|
||||||
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
|
||||||
expect(document.querySelectorAll('.anticon-check-circle').length).toBe(1);
|
expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
|
||||||
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
expect(document.querySelectorAll('.anticon-check-circle')).toHaveLength(1);
|
||||||
|
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with onClose', done => {
|
it('should work with onClose', done => {
|
||||||
// if not use real timer, done won't be called
|
|
||||||
jest.useRealTimers();
|
|
||||||
const Demo = () => {
|
const Demo = () => {
|
||||||
const [api, holder] = message.useMessage();
|
const [api, holder] = message.useMessage();
|
||||||
return (
|
return (
|
||||||
@ -111,14 +108,13 @@ describe('message.hooks', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = mount(<Demo />);
|
const { container } = render(<Demo />);
|
||||||
wrapper.find('button').simulate('click');
|
fireEvent.click(container.querySelector('button')!);
|
||||||
jest.useFakeTimers();
|
|
||||||
|
triggerMotionEnd();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with close promise', done => {
|
it('should work with close promise', done => {
|
||||||
// if not use real timer, done won't be called
|
|
||||||
jest.useRealTimers();
|
|
||||||
const Demo = () => {
|
const Demo = () => {
|
||||||
const [api, holder] = message.useMessage();
|
const [api, holder] = message.useMessage();
|
||||||
return (
|
return (
|
||||||
@ -141,13 +137,14 @@ describe('message.hooks', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = mount(<Demo />);
|
const { container } = render(<Demo />);
|
||||||
wrapper.find('button').simulate('click');
|
fireEvent.click(container.querySelector('button')!);
|
||||||
jest.useFakeTimers();
|
|
||||||
|
triggerMotionEnd();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with hide', () => {
|
it('should work with hide', async () => {
|
||||||
let hide;
|
let hide: VoidFunction;
|
||||||
const Demo = () => {
|
const Demo = () => {
|
||||||
const [api, holder] = message.useMessage();
|
const [api, holder] = message.useMessage();
|
||||||
return (
|
return (
|
||||||
@ -166,48 +163,50 @@ describe('message.hooks', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = mount(<Demo />);
|
const { container } = render(<Demo />);
|
||||||
wrapper.find('button').simulate('click');
|
fireEvent.click(container.querySelector('button')!);
|
||||||
|
|
||||||
|
expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
jest.runAllTimers();
|
hide!();
|
||||||
});
|
});
|
||||||
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
await triggerMotionEnd('.my-test-message-move-up-leave');
|
||||||
|
|
||||||
act(() => {
|
expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(0);
|
||||||
hide();
|
|
||||||
jest.runAllTimers();
|
|
||||||
});
|
|
||||||
expect(getInstance().component.state.notices).toHaveLength(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be same hook', () => {
|
it('should be same hook', () => {
|
||||||
let count = 0;
|
let cacheAPI: any;
|
||||||
|
|
||||||
const Demo = () => {
|
const Demo = () => {
|
||||||
const [, forceUpdate] = React.useState({});
|
const [, forceUpdate] = React.useState({});
|
||||||
const [api] = message.useMessage();
|
const [api] = message.useMessage();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
count += 1;
|
if (!cacheAPI) {
|
||||||
expect(count).toEqual(1);
|
cacheAPI = api;
|
||||||
forceUpdate();
|
} else {
|
||||||
|
expect(cacheAPI).toBe(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
forceUpdate({});
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
mount(<Demo />);
|
render(<Demo />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use ConfigProvider's getPopupContainer as message container", () => {
|
it("should use ConfigProvider's getPopupContainer as message container", () => {
|
||||||
const containerId = 'container';
|
const containerId = 'container';
|
||||||
const getPopupContainer = () => {
|
const div = document.createElement('div');
|
||||||
const div = document.createElement('div');
|
div.id = containerId;
|
||||||
div.id = containerId;
|
document.body.appendChild(div);
|
||||||
document.body.appendChild(div);
|
|
||||||
return div;
|
const getPopupContainer = () => div;
|
||||||
};
|
|
||||||
const Demo = () => {
|
const Demo = () => {
|
||||||
const [api, holder] = message.useMessage();
|
const [api, holder] = message.useMessage();
|
||||||
return (
|
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(div.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
|
||||||
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
expect(div.querySelectorAll('.anticon-check-circle')).toHaveLength(1);
|
||||||
expect(document.querySelectorAll('.anticon-check-circle').length).toBe(1);
|
expect(div.querySelector('.hook-content')!.textContent).toEqual('happy');
|
||||||
expect(document.querySelector('.hook-content').innerHTML).toEqual('happy');
|
expect(document.querySelectorAll(`#${containerId}`)).toHaveLength(1);
|
||||||
expect(document.querySelectorAll(`#${containerId}`).length).toBe(1);
|
});
|
||||||
expect(wrapper.find(`#${containerId}`).children.length).toBe(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', () => {
|
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);
|
message.success('yes!!!', 0);
|
||||||
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('promise with one augument', done => {
|
it('promise with one arguments', async () => {
|
||||||
message.success('yes!!!').then(filled => {
|
const filled = jest.fn();
|
||||||
expect(filled).toBe(true);
|
|
||||||
done();
|
message.success('yes!!!').then(filled);
|
||||||
});
|
|
||||||
|
await triggerMotionEnd();
|
||||||
|
|
||||||
|
expect(filled).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('promise two auguments', done => {
|
it('promise two arguments', async () => {
|
||||||
message.success('yes!!!').then(
|
const filled = jest.fn();
|
||||||
filled => {
|
const rejected = jest.fn();
|
||||||
expect(filled).toBe(true);
|
|
||||||
done();
|
message.success('yes!!!').then(filled, rejected);
|
||||||
},
|
|
||||||
rejected => {
|
await triggerMotionEnd();
|
||||||
expect(rejected).toBe(false);
|
|
||||||
},
|
expect(filled).toHaveBeenCalledWith(true);
|
||||||
);
|
expect(rejected).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hide', () => {
|
it('hide', async () => {
|
||||||
const hide = message.loading('doing...');
|
const hide = message.loading('doing...');
|
||||||
|
await Promise.resolve();
|
||||||
hide();
|
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:
|
title:
|
||||||
zh-CN: 通过 Hooks 获取上下文(4.5.0+)
|
zh-CN: Hooks 调用(推荐)
|
||||||
en-US: Get context with hooks (4.5.0+)
|
en-US: Hooks usage (recommended)
|
||||||
---
|
---
|
||||||
|
|
||||||
## zh-CN
|
## zh-CN
|
||||||
|
|
||||||
通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。
|
通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
|
||||||
|
|
||||||
## en-US
|
## 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
|
```jsx
|
||||||
import { message, Button } from 'antd';
|
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 * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import { render } from 'rc-util/lib/React/render';
|
||||||
import RCNotification from 'rc-notification';
|
import useMessage, { useInternalMessage } from './useMessage';
|
||||||
import type {
|
import type {
|
||||||
NotificationInstance as RCNotificationInstance,
|
ArgsProps,
|
||||||
NoticeContent,
|
MessageInstance,
|
||||||
} from 'rc-notification/lib/Notification';
|
ConfigOptions,
|
||||||
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
|
NoticeType,
|
||||||
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
|
TypeOpen,
|
||||||
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
MessageType,
|
||||||
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
|
} from './interface';
|
||||||
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
|
|
||||||
import createUseMessage from './hooks/useMessage';
|
|
||||||
import ConfigProvider, { globalConfig } from '../config-provider';
|
import ConfigProvider, { globalConfig } from '../config-provider';
|
||||||
|
import { wrapPromiseFn } from './util';
|
||||||
|
|
||||||
let messageInstance: RCNotificationInstance | null;
|
export { ArgsProps };
|
||||||
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 function getKeyThenIncreaseKey() {
|
const methods: NoticeType[] = ['success', 'info', 'warning', 'error', 'loading'];
|
||||||
return key++;
|
|
||||||
|
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 {
|
interface OpenTask {
|
||||||
top?: number;
|
type: 'open';
|
||||||
duration?: number;
|
config: ArgsProps;
|
||||||
prefixCls?: string;
|
resolve: VoidFunction;
|
||||||
getContainer?: () => HTMLElement;
|
setCloseFn: (closeFn: VoidFunction) => void;
|
||||||
transitionName?: string;
|
skipped?: boolean;
|
||||||
maxCount?: number;
|
|
||||||
rtl?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMessageConfig(options: ConfigOptions) {
|
interface TypeTask {
|
||||||
if (options.top !== undefined) {
|
type: NoticeType;
|
||||||
defaultTop = options.top;
|
args: Parameters<TypeOpen>;
|
||||||
messageInstance = null; // delete messageInstance for new defaultTop
|
resolve: VoidFunction;
|
||||||
}
|
setCloseFn: (closeFn: VoidFunction) => void;
|
||||||
if (options.duration !== undefined) {
|
skipped?: boolean;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRCNotificationInstance(
|
type Task =
|
||||||
args: ArgsProps,
|
| OpenTask
|
||||||
callback: (info: {
|
| TypeTask
|
||||||
prefixCls: string;
|
| {
|
||||||
rootPrefixCls: string;
|
type: 'destroy';
|
||||||
iconPrefixCls: string;
|
key: React.Key;
|
||||||
instance: RCNotificationInstance;
|
skipped?: boolean;
|
||||||
}) => void,
|
};
|
||||||
) {
|
|
||||||
const { prefixCls: customizePrefixCls, getPopupContainer: getContextPopupContainer } = args;
|
let taskQueue: Task[] = [];
|
||||||
const { getPrefixCls, getRootPrefixCls, getIconPrefixCls } = globalConfig();
|
|
||||||
const prefixCls = getPrefixCls('message', customizePrefixCls || localPrefixCls);
|
let defaultGlobalConfig: ConfigOptions = {};
|
||||||
const rootPrefixCls = getRootPrefixCls(args.rootPrefixCls, prefixCls);
|
|
||||||
const iconPrefixCls = getIconPrefixCls();
|
function getGlobalContext() {
|
||||||
|
const {
|
||||||
|
prefixCls: globalPrefixCls,
|
||||||
|
getContainer: globalGetContainer,
|
||||||
|
rtl,
|
||||||
|
maxCount,
|
||||||
|
top,
|
||||||
|
} = defaultGlobalConfig;
|
||||||
|
const mergedPrefixCls = globalPrefixCls ?? globalConfig().getPrefixCls('message');
|
||||||
|
const mergedContainer = globalGetContainer?.() || document.body;
|
||||||
|
|
||||||
|
return {
|
||||||
|
prefixCls: mergedPrefixCls,
|
||||||
|
container: mergedContainer,
|
||||||
|
rtl,
|
||||||
|
maxCount,
|
||||||
|
top,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (messageInstance) {
|
|
||||||
callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance: messageInstance });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceConfig = {
|
// Notification not ready
|
||||||
prefixCls,
|
if (message && !message.instance) {
|
||||||
transitionName: hasTransitionName ? transitionName : `${rootPrefixCls}-${transitionName}`,
|
return;
|
||||||
style: { top: defaultTop }, // 覆盖原来的样式
|
}
|
||||||
getContainer: getContainer || getContextPopupContainer,
|
|
||||||
maxCount,
|
|
||||||
};
|
|
||||||
|
|
||||||
RCNotification.newInstance(instanceConfig, (instance: any) => {
|
// >>> Execute task
|
||||||
if (messageInstance) {
|
taskQueue.forEach(task => {
|
||||||
callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance: messageInstance });
|
const { type, skipped } = task;
|
||||||
return;
|
|
||||||
}
|
|
||||||
messageInstance = instance;
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
// Only `skipped` when user call notice but cancel it immediately
|
||||||
(messageInstance as any).config = instanceConfig;
|
// and instance not ready
|
||||||
}
|
if (!skipped) {
|
||||||
|
switch (type) {
|
||||||
|
case 'open': {
|
||||||
|
act(() => {
|
||||||
|
const closeFn = message!.instance!.open({
|
||||||
|
...defaultGlobalConfig,
|
||||||
|
...task.config,
|
||||||
|
});
|
||||||
|
|
||||||
callback({ prefixCls, rootPrefixCls, iconPrefixCls, instance });
|
closeFn?.then(task.resolve);
|
||||||
});
|
task.setCloseFn(closeFn);
|
||||||
}
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ThenableArgument {
|
case 'destroy':
|
||||||
(val: any): void;
|
act(() => {
|
||||||
}
|
message?.instance!.destroy(task.key);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
export interface MessageType extends PromiseLike<any> {
|
// Other type open
|
||||||
(): void;
|
default: {
|
||||||
}
|
act(() => {
|
||||||
|
const closeFn = message!.instance;
|
||||||
|
|
||||||
const typeToIcon = {
|
closeFn?.then(task.resolve);
|
||||||
info: InfoCircleFilled,
|
task.setCloseFn(closeFn);
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
getRCNotificationInstance(args, ({ prefixCls, iconPrefixCls, instance }) => {
|
|
||||||
instance.notice(
|
|
||||||
getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls, iconPrefixCls),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const result: any = () => {
|
|
||||||
if (messageInstance) {
|
|
||||||
messageInstance.removeNotice(target);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
taskQueue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// == Export ==
|
||||||
|
// ==============================================================================
|
||||||
|
type MethodType = typeof methods[number];
|
||||||
|
|
||||||
|
function setMessageGlobalConfig(config: ConfigOptions) {
|
||||||
|
defaultGlobalConfig = {
|
||||||
|
...defaultGlobalConfig,
|
||||||
|
...config,
|
||||||
};
|
};
|
||||||
result.then = (filled: ThenableArgument, rejected: ThenableArgument) =>
|
|
||||||
closePromise.then(filled, rejected);
|
// Trigger sync for it
|
||||||
result.promise = closePromise;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
flushNotice();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigContent = React.ReactNode;
|
function typeOpen(type: NoticeType, args: Parameters<TypeOpen>): MessageType {
|
||||||
type ConfigDuration = number | (() => void);
|
const result = wrapPromiseFn(resolve => {
|
||||||
type JointContent = ConfigContent | ArgsProps;
|
let closeFn: VoidFunction;
|
||||||
export type ConfigOnClose = () => void;
|
|
||||||
|
|
||||||
function isArgsProps(content: JointContent): content is ArgsProps {
|
const task: TypeTask = {
|
||||||
return (
|
type,
|
||||||
Object.prototype.toString.call(content) === '[object Object]' &&
|
args,
|
||||||
!!(content as ArgsProps).content
|
resolve,
|
||||||
);
|
setCloseFn: fn => {
|
||||||
|
closeFn = fn;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
taskQueue.push(task);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (closeFn) {
|
||||||
|
act(() => {
|
||||||
|
closeFn();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
task.skipped = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
flushNotice();
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const api: any = {
|
function destroy(key: React.Key) {
|
||||||
open: notice,
|
taskQueue.push({
|
||||||
config: setMessageConfig,
|
type: 'destroy',
|
||||||
destroy(messageKey?: React.Key) {
|
key,
|
||||||
if (messageInstance) {
|
});
|
||||||
if (messageKey) {
|
flushNotice();
|
||||||
const { removeNotice } = messageInstance;
|
}
|
||||||
removeNotice(messageKey);
|
|
||||||
} else {
|
const baseStaticMethods: {
|
||||||
const { destroy } = messageInstance;
|
open: (config: ArgsProps) => MessageType;
|
||||||
destroy();
|
destroy: (key?: React.Key) => void;
|
||||||
messageInstance = null;
|
config: any;
|
||||||
}
|
useMessage: typeof useMessage;
|
||||||
}
|
} = {
|
||||||
},
|
open,
|
||||||
|
destroy,
|
||||||
|
config: setMessageGlobalConfig,
|
||||||
|
useMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
|
const staticMethods: typeof baseStaticMethods & Record<MethodType, TypeOpen> =
|
||||||
originalApi[type] = (
|
baseStaticMethods as any;
|
||||||
content: JointContent,
|
|
||||||
duration?: ConfigDuration,
|
|
||||||
onClose?: ConfigOnClose,
|
|
||||||
) => {
|
|
||||||
if (isArgsProps(content)) {
|
|
||||||
return originalApi.open({ ...content, type });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof duration === 'function') {
|
methods.forEach(type => {
|
||||||
onClose = duration;
|
staticMethods[type] = (...args: Parameters<TypeOpen>) => typeOpen(type, args);
|
||||||
duration = undefined;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
if (process.env.NODE_ENV === 'test') {
|
||||||
api.useMessage = createUseMessage(getRCNotificationInstance, getRCNoticeProps);
|
actDestroy = () => {
|
||||||
|
message = null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageApi extends MessageInstance {
|
export default staticMethods;
|
||||||
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;
|
|
||||||
|
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;
|
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-name: MessageMoveOut;
|
||||||
animation-duration: 0.3s;
|
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 { mount } from 'enzyme';
|
||||||
import notification from '..';
|
import notification from '..';
|
||||||
import ConfigProvider from '../../config-provider';
|
import ConfigProvider from '../../config-provider';
|
||||||
|
import { render, fireEvent } from '../../../tests/utils';
|
||||||
|
|
||||||
describe('notification.hooks', () => {
|
describe('notification.hooks', () => {
|
||||||
beforeAll(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
notification.destroy();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@ -30,6 +27,7 @@ describe('notification.hooks', () => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
api.open({
|
api.open({
|
||||||
|
message: null,
|
||||||
description: (
|
description: (
|
||||||
<Context.Consumer>
|
<Context.Consumer>
|
||||||
{name => <span className="hook-test-result">{name}</span>}
|
{name => <span className="hook-test-result">{name}</span>}
|
||||||
@ -45,10 +43,12 @@ describe('notification.hooks', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = mount(<Demo />);
|
const { container } = render(<Demo />);
|
||||||
wrapper.find('button').simulate('click');
|
|
||||||
expect(document.querySelectorAll('.my-test-notification-notice').length).toBe(1);
|
fireEvent.click(container.querySelector('button')!);
|
||||||
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
|
||||||
|
expect(document.querySelectorAll('.my-test-notification-notice')).toHaveLength(1);
|
||||||
|
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with success', () => {
|
it('should work with success', () => {
|
||||||
@ -64,6 +64,7 @@ describe('notification.hooks', () => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
api.success({
|
api.success({
|
||||||
|
message: null,
|
||||||
description: (
|
description: (
|
||||||
<Context.Consumer>
|
<Context.Consumer>
|
||||||
{name => <span className="hook-test-result">{name}</span>}
|
{name => <span className="hook-test-result">{name}</span>}
|
||||||
@ -79,11 +80,12 @@ describe('notification.hooks', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = mount(<Demo />);
|
const { container } = render(<Demo />);
|
||||||
wrapper.find('button').simulate('click');
|
fireEvent.click(container.querySelector('button')!);
|
||||||
expect(document.querySelectorAll('.my-test-notification-notice').length).toBe(1);
|
|
||||||
expect(document.querySelectorAll('.anticon-check-circle').length).toBe(1);
|
expect(document.querySelectorAll('.my-test-notification-notice')).toHaveLength(1);
|
||||||
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
expect(document.querySelectorAll('.anticon-check-circle')).toHaveLength(1);
|
||||||
|
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be same hook', () => {
|
it('should be same hook', () => {
|
||||||
@ -96,7 +98,7 @@ describe('notification.hooks', () => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
count += 1;
|
count += 1;
|
||||||
expect(count).toEqual(1);
|
expect(count).toEqual(1);
|
||||||
forceUpdate();
|
forceUpdate({});
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -104,4 +106,53 @@ describe('notification.hooks', () => {
|
|||||||
|
|
||||||
mount(<Demo />);
|
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:
|
title:
|
||||||
zh-CN: 通过 Hooks 获取上下文
|
zh-CN: Hooks 调用(推荐)
|
||||||
en-US: Get context with hooks
|
en-US: Hooks usage (recommended)
|
||||||
---
|
---
|
||||||
|
|
||||||
## zh-CN
|
## zh-CN
|
||||||
|
|
||||||
通过 `notification.useNotification` 创建支持读取 context 的 `contextHolder`。
|
通过 `notification.useNotification` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
|
||||||
|
|
||||||
## en-US
|
## 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
|
```jsx
|
||||||
import { Button, notification, Divider, Space } from 'antd';
|
import { Button, notification, Divider, Space } from 'antd';
|
||||||
|
@ -14,7 +14,7 @@ title:
|
|||||||
To customize the style or font of the close button.
|
To customize the style or font of the close button.
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import { Button, notification } from 'antd';
|
import { Button, notification, Space } from 'antd';
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
console.log(
|
console.log(
|
||||||
@ -25,9 +25,14 @@ const close = () => {
|
|||||||
const openNotification = () => {
|
const openNotification = () => {
|
||||||
const key = `open${Date.now()}`;
|
const key = `open${Date.now()}`;
|
||||||
const btn = (
|
const btn = (
|
||||||
<Button type="primary" size="small" onClick={() => notification.close(key)}>
|
<Space>
|
||||||
Confirm
|
<Button type="link" size="small" onClick={() => notification.destroy()}>
|
||||||
</Button>
|
Destroy All
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" size="small" onClick={() => notification.destroy(key)}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
);
|
);
|
||||||
notification.open({
|
notification.open({
|
||||||
message: 'Notification Title',
|
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.warning(config)`
|
||||||
- `notification.warn(config)`
|
- `notification.warn(config)`
|
||||||
- `notification.open(config)`
|
- `notification.open(config)`
|
||||||
- `notification.close(key: String)`
|
- `notification.destroy(key?: String)`
|
||||||
- `notification.destroy()`
|
|
||||||
|
|
||||||
The properties of config are as follows:
|
The properties of config are as follows:
|
||||||
|
|
||||||
|
@ -1,345 +1,258 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Notification from 'rc-notification';
|
import { render } from 'rc-util/lib/React/render';
|
||||||
import type { NotificationInstance as RCNotificationInstance } from 'rc-notification/lib/Notification';
|
import useNotification, { useInternalNotification } from './useNotification';
|
||||||
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
import type { ArgsProps, NotificationInstance, GlobalConfigProps } from './interface';
|
||||||
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 ConfigProvider, { globalConfig } from '../config-provider';
|
import ConfigProvider, { globalConfig } from '../config-provider';
|
||||||
|
|
||||||
export type NotificationPlacement =
|
let notification: GlobalNotification | null = null;
|
||||||
| 'top'
|
|
||||||
| 'topLeft'
|
|
||||||
| 'topRight'
|
|
||||||
| 'bottom'
|
|
||||||
| 'bottomLeft'
|
|
||||||
| 'bottomRight';
|
|
||||||
|
|
||||||
export type IconType = 'success' | 'info' | 'error' | 'warning';
|
let act: (callback: VoidFunction) => Promise<void> | void = (callback: VoidFunction) => callback();
|
||||||
|
|
||||||
const notificationInstance: {
|
interface GlobalNotification {
|
||||||
[key: string]: Promise<RCNotificationInstance>;
|
fragment: DocumentFragment;
|
||||||
} = {};
|
instance?: NotificationInstance | null;
|
||||||
let defaultDuration = 4.5;
|
sync?: VoidFunction;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setNotificationConfig(options: ConfigProps) {
|
type Task =
|
||||||
const { duration, placement, bottom, top, getContainer, closeIcon, prefixCls } = options;
|
| {
|
||||||
if (prefixCls !== undefined) {
|
type: 'open';
|
||||||
defaultPrefixCls = prefixCls;
|
config: ArgsProps;
|
||||||
}
|
}
|
||||||
if (duration !== undefined) {
|
| {
|
||||||
defaultDuration = duration;
|
type: 'destroy';
|
||||||
}
|
key: React.Key;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlacementStyle(
|
let taskQueue: Task[] = [];
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNotificationInstance(
|
let defaultGlobalConfig: GlobalConfigProps = {};
|
||||||
args: ArgsProps,
|
|
||||||
callback: (info: {
|
function getGlobalContext() {
|
||||||
prefixCls: string;
|
|
||||||
iconPrefixCls: string;
|
|
||||||
instance: RCNotificationInstance;
|
|
||||||
}) => void,
|
|
||||||
) {
|
|
||||||
const {
|
const {
|
||||||
placement = defaultPlacement,
|
prefixCls: globalPrefixCls,
|
||||||
|
getContainer: globalGetContainer,
|
||||||
|
rtl,
|
||||||
|
maxCount,
|
||||||
top,
|
top,
|
||||||
bottom,
|
bottom,
|
||||||
getContainer = defaultGetContainer,
|
} = defaultGlobalConfig;
|
||||||
prefixCls: customizePrefixCls,
|
const mergedPrefixCls = globalPrefixCls ?? globalConfig().getPrefixCls('notification');
|
||||||
} = args;
|
const mergedContainer = globalGetContainer?.() || document.body;
|
||||||
const { getPrefixCls, getIconPrefixCls } = globalConfig();
|
|
||||||
const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls);
|
|
||||||
const iconPrefixCls = getIconPrefixCls();
|
|
||||||
|
|
||||||
const cacheKey = `${prefixCls}-${placement}`;
|
return {
|
||||||
const cacheInstance = notificationInstance[cacheKey];
|
prefixCls: mergedPrefixCls,
|
||||||
|
container: mergedContainer,
|
||||||
|
rtl,
|
||||||
|
maxCount,
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (cacheInstance) {
|
interface GlobalHolderRef {
|
||||||
Promise.resolve(cacheInstance).then(instance => {
|
instance: NotificationInstance;
|
||||||
callback({ prefixCls: `${prefixCls}-notice`, iconPrefixCls, instance });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationClass = classNames(`${prefixCls}-${placement}`, {
|
// Notification not ready
|
||||||
[`${prefixCls}-rtl`]: rtl === true,
|
if (notification && !notification.instance) {
|
||||||
});
|
return;
|
||||||
|
|
||||||
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}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeIconToRender = (
|
// >>> Execute task
|
||||||
<span className={`${prefixCls}-close-x`}>
|
taskQueue.forEach(task => {
|
||||||
{closeIcon || <CloseOutlined className={`${prefixCls}-close-icon`} />}
|
// eslint-disable-next-line default-case
|
||||||
</span>
|
switch (task.type) {
|
||||||
);
|
case 'open': {
|
||||||
|
act(() => {
|
||||||
|
notification!.instance!.open({
|
||||||
|
...defaultGlobalConfig,
|
||||||
|
...task.config,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const autoMarginTag =
|
case 'destroy':
|
||||||
!description && iconNode ? (
|
act(() => {
|
||||||
<span className={`${prefixCls}-message-single-line-auto-margin`} />
|
notification?.instance!.destroy(task.key);
|
||||||
) : null;
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
// Clean up
|
||||||
content: (
|
taskQueue = [];
|
||||||
<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,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function notice(args: ArgsProps) {
|
// ==============================================================================
|
||||||
getNotificationInstance(args, ({ prefixCls, iconPrefixCls, instance }) => {
|
// == Export ==
|
||||||
instance.notice(getRCNoticeProps(args, prefixCls, iconPrefixCls));
|
// ==============================================================================
|
||||||
|
const methods = ['success', 'info', 'warning', 'error'] as const;
|
||||||
|
type MethodType = typeof methods[number];
|
||||||
|
|
||||||
|
function setNotificationGlobalConfig(config: GlobalConfigProps) {
|
||||||
|
defaultGlobalConfig = {
|
||||||
|
...defaultGlobalConfig,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger sync for it
|
||||||
|
act(() => {
|
||||||
|
notification?.sync?.();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const api: any = {
|
function open(config: ArgsProps) {
|
||||||
open: notice,
|
taskQueue.push({
|
||||||
close(key: string) {
|
type: 'open',
|
||||||
Object.keys(notificationInstance).forEach(cacheKey =>
|
config,
|
||||||
Promise.resolve(notificationInstance[cacheKey]).then(instance => {
|
});
|
||||||
instance.removeNotice(key);
|
flushNotice();
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
},
|
function destroy(key: React.Key) {
|
||||||
config: setNotificationConfig,
|
taskQueue.push({
|
||||||
destroy() {
|
type: 'destroy',
|
||||||
Object.keys(notificationInstance).forEach(cacheKey => {
|
key,
|
||||||
Promise.resolve(notificationInstance[cacheKey]).then(instance => {
|
});
|
||||||
instance.destroy();
|
flushNotice();
|
||||||
});
|
}
|
||||||
delete notificationInstance[cacheKey]; // lgtm[js/missing-await]
|
|
||||||
});
|
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 => {
|
const staticMethods: typeof baseStaticMethods & Record<MethodType, (config: ArgsProps) => void> =
|
||||||
api[type] = (args: ArgsProps) =>
|
baseStaticMethods as any;
|
||||||
api.open({
|
|
||||||
...args,
|
methods.forEach(type => {
|
||||||
|
staticMethods[type] = config =>
|
||||||
|
open({
|
||||||
|
...config,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
api.warn = api.warning;
|
// ==============================================================================
|
||||||
api.useNotification = createUseNotification(getNotificationInstance, getRCNoticeProps);
|
// == Test ==
|
||||||
|
// ==============================================================================
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
export interface NotificationInstance {
|
/** @private Only Work in test env */
|
||||||
success(args: ArgsProps): void;
|
// eslint-disable-next-line import/no-mutable-exports
|
||||||
error(args: ArgsProps): void;
|
export let actWrapper: (wrapper: any) => void = noop;
|
||||||
info(args: ArgsProps): void;
|
|
||||||
warning(args: ArgsProps): void;
|
if (process.env.NODE_ENV === 'test') {
|
||||||
open(args: ArgsProps): void;
|
actWrapper = wrapper => {
|
||||||
|
act = wrapper;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationApi extends NotificationInstance {
|
export default staticMethods;
|
||||||
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;
|
|
||||||
|
@ -25,8 +25,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Jxm5nw61w/Notification.svg
|
|||||||
- `notification.warning(config)`
|
- `notification.warning(config)`
|
||||||
- `notification.warn(config)`
|
- `notification.warn(config)`
|
||||||
- `notification.open(config)`
|
- `notification.open(config)`
|
||||||
- `notification.close(key: String)`
|
- `notification.destroy(key?: String)`
|
||||||
- `notification.destroy()`
|
|
||||||
|
|
||||||
config 参数如下:
|
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-mentions": "~1.8.0",
|
||||||
"rc-menu": "~9.6.0",
|
"rc-menu": "~9.6.0",
|
||||||
"rc-motion": "^2.5.1",
|
"rc-motion": "^2.5.1",
|
||||||
"rc-notification": "~4.6.0",
|
"rc-notification": "~5.0.0-alpha.8",
|
||||||
"rc-pagination": "~3.1.9",
|
"rc-pagination": "~3.1.9",
|
||||||
"rc-picker": "~2.6.4",
|
"rc-picker": "~2.6.4",
|
||||||
"rc-progress": "~3.2.1",
|
"rc-progress": "~3.2.1",
|
||||||
@ -157,7 +157,7 @@
|
|||||||
"rc-tree-select": "~5.3.0",
|
"rc-tree-select": "~5.3.0",
|
||||||
"rc-trigger": "^5.2.10",
|
"rc-trigger": "^5.2.10",
|
||||||
"rc-upload": "~4.3.0",
|
"rc-upload": "~4.3.0",
|
||||||
"rc-util": "^5.20.0",
|
"rc-util": "^5.21.3",
|
||||||
"scroll-into-view-if-needed": "^2.2.25",
|
"scroll-into-view-if-needed": "^2.2.25",
|
||||||
"shallowequal": "^1.1.0"
|
"shallowequal": "^1.1.0"
|
||||||
},
|
},
|
||||||
@ -306,7 +306,7 @@
|
|||||||
"bundlesize": [
|
"bundlesize": [
|
||||||
{
|
{
|
||||||
"path": "./dist/antd.min.js",
|
"path": "./dist/antd.min.js",
|
||||||
"maxSize": "356 kB"
|
"maxSize": "358 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./dist/antd.min.css",
|
"path": "./dist/antd.min.css",
|
||||||
|
@ -327,7 +327,7 @@ class Footer extends React.Component<WrappedComponentProps & { location: any }>
|
|||||||
intl: { messages },
|
intl: { messages },
|
||||||
} = this.props;
|
} = this.props;
|
||||||
message.loading({
|
message.loading({
|
||||||
content: messages['app.footer.primary-color-changing'],
|
content: messages['app.footer.primary-color-changing'] as string,
|
||||||
key: 'change-primary-color',
|
key: 'change-primary-color',
|
||||||
});
|
});
|
||||||
const changeColor = () => {
|
const changeColor = () => {
|
||||||
@ -337,7 +337,7 @@ class Footer extends React.Component<WrappedComponentProps & { location: any }>
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
message.success({
|
message.success({
|
||||||
content: messages['app.footer.primary-color-changed'],
|
content: messages['app.footer.primary-color-changed'] as string,
|
||||||
key: 'change-primary-color',
|
key: 'change-primary-color',
|
||||||
});
|
});
|
||||||
this.setState({ color });
|
this.setState({ color });
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
# V5 breaking change 记录
|
# V5 breaking change 记录
|
||||||
|
|
||||||
|
- getPopupContainer: 所有的 getPopupContainer 都需要保证返回的是唯一的 div。React 18 concurrent 下会反复调用该方法。
|
||||||
- Dropdown
|
- Dropdown
|
||||||
- 魔改包裹元素样式移除,请使用 Space 组件
|
- 魔改包裹元素样式移除,请使用 Space 组件
|
||||||
- DropdownButton 的 prefixCls 改为 `dropdown`
|
- DropdownButton 的 prefixCls 改为 `dropdown`
|
||||||
- Upload List 结构变化
|
- Upload List 结构变化
|
||||||
|
- Notification
|
||||||
|
- 静态方法不在允许在 `open` 中动态设置 `prefixCls` `maxCount` `top` `bottom` `getContainer`,Notification 静态方法现在将只有一个实例。如果需要不同配置,请使用 `useNotification`。
|
||||||
|
- close 改名为 destroy 和 message 保持一致
|
||||||
|
Loading…
Reference in New Issue
Block a user