mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-27 20:49:53 +08:00
feat: Message hooks API (#25422)
* chore: comment on usePatchElement * refactor: conform message & notifaction code logic * feat: message useMessage, wip * feat: message.useMessage, it works now * fix: promise on regular api * feat: message hooks * chore: fix lint * chore: new line * chore: revert new line * refactor: prefixCls * fix: prefixCls * test: cov * chore * chore * chore * chore * docs * docs: message hooks faq * test: remove useless config provider * chore: remove some test codes * chore * docs: hooks version
This commit is contained in:
parent
6a65b47df3
commit
01cec29a8e
@ -23,6 +23,7 @@ lib/**/*
|
|||||||
node_modules
|
node_modules
|
||||||
_site
|
_site
|
||||||
dist
|
dist
|
||||||
|
coverage
|
||||||
**/*.d.ts
|
**/*.d.ts
|
||||||
# Scripts
|
# Scripts
|
||||||
scripts/previewEditor/**/*
|
scripts/previewEditor/**/*
|
@ -7,8 +7,11 @@ export default function usePatchElement(): [
|
|||||||
const [elements, setElements] = React.useState<React.ReactElement[]>([]);
|
const [elements, setElements] = React.useState<React.ReactElement[]>([]);
|
||||||
|
|
||||||
function patchElement(element: React.ReactElement) {
|
function patchElement(element: React.ReactElement) {
|
||||||
|
// append a new element to elements (and create a new ref)
|
||||||
setElements(originElements => [...originElements, element]);
|
setElements(originElements => [...originElements, element]);
|
||||||
|
|
||||||
|
// return a function that removes the new element out of elements (and create a new ref)
|
||||||
|
// it works a little like useEffect
|
||||||
return () => {
|
return () => {
|
||||||
setElements(originElements => originElements.filter(ele => ele !== element));
|
setElements(originElements => originElements.filter(ele => ele !== element));
|
||||||
};
|
};
|
||||||
|
@ -22,6 +22,17 @@ exports[`renders ./components/message/demo/duration.md correctly 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`renders ./components/message/demo/hooks.md correctly 1`] = `
|
||||||
|
<button
|
||||||
|
class="ant-btn ant-btn-primary"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Display normal message
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`renders ./components/message/demo/info.md correctly 1`] = `
|
exports[`renders ./components/message/demo/info.md correctly 1`] = `
|
||||||
<button
|
<button
|
||||||
class="ant-btn ant-btn-primary"
|
class="ant-btn ant-btn-primary"
|
||||||
|
195
components/message/__tests__/hooks.test.js
Normal file
195
components/message/__tests__/hooks.test.js
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import message from '..';
|
||||||
|
import ConfigProvider from '../../config-provider';
|
||||||
|
|
||||||
|
describe('message.hooks', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
message.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work', () => {
|
||||||
|
const Context = React.createContext('light');
|
||||||
|
|
||||||
|
const Demo = () => {
|
||||||
|
const [api, holder] = message.useMessage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider prefixCls="my-test">
|
||||||
|
<Context.Provider value="bamboo">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
api.open({
|
||||||
|
content: (
|
||||||
|
<Context.Consumer>
|
||||||
|
{name => <span className="hook-test-result">{name}</span>}
|
||||||
|
</Context.Consumer>
|
||||||
|
),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{holder}
|
||||||
|
</Context.Provider>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mount(<Demo />);
|
||||||
|
wrapper.find('button').simulate('click');
|
||||||
|
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
||||||
|
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with success', () => {
|
||||||
|
const Context = React.createContext('light');
|
||||||
|
|
||||||
|
const Demo = () => {
|
||||||
|
const [api, holder] = message.useMessage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider prefixCls="my-test">
|
||||||
|
<Context.Provider value="bamboo">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
api.success({
|
||||||
|
content: (
|
||||||
|
<Context.Consumer>
|
||||||
|
{name => <span className="hook-test-result">{name}</span>}
|
||||||
|
</Context.Consumer>
|
||||||
|
),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{holder}
|
||||||
|
</Context.Provider>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mount(<Demo />);
|
||||||
|
wrapper.find('button').simulate('click');
|
||||||
|
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
||||||
|
expect(document.querySelectorAll('.anticon-check-circle').length).toBe(1);
|
||||||
|
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with onClose', done => {
|
||||||
|
// if not use real timer, done won't be called
|
||||||
|
jest.useRealTimers();
|
||||||
|
const Demo = () => {
|
||||||
|
const [api, holder] = message.useMessage();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
api.open({
|
||||||
|
content: 'amazing',
|
||||||
|
duration: 1,
|
||||||
|
onClose() {
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{holder}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mount(<Demo />);
|
||||||
|
wrapper.find('button').simulate('click');
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with close promise', done => {
|
||||||
|
// if not use real timer, done won't be called
|
||||||
|
jest.useRealTimers();
|
||||||
|
const Demo = () => {
|
||||||
|
const [api, holder] = message.useMessage();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
api
|
||||||
|
.open({
|
||||||
|
content: 'good',
|
||||||
|
duration: 1,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{holder}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mount(<Demo />);
|
||||||
|
wrapper.find('button').simulate('click');
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with hide', () => {
|
||||||
|
let hide;
|
||||||
|
const Demo = () => {
|
||||||
|
const [api, holder] = message.useMessage();
|
||||||
|
return (
|
||||||
|
<ConfigProvider prefixCls="my-test">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
hide = api.open({
|
||||||
|
content: 'nice',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{holder}
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mount(<Demo />);
|
||||||
|
wrapper.find('button').simulate('click');
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(1);
|
||||||
|
hide();
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(document.querySelectorAll('.my-test-message-notice').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be same hook', () => {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
const Demo = () => {
|
||||||
|
const [, forceUpdate] = React.useState({});
|
||||||
|
const [api] = message.useMessage();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
count += 1;
|
||||||
|
expect(count).toEqual(1);
|
||||||
|
forceUpdate();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
mount(<Demo />);
|
||||||
|
});
|
||||||
|
});
|
42
components/message/demo/hooks.md
Normal file
42
components/message/demo/hooks.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
order: 10
|
||||||
|
title:
|
||||||
|
zh-CN: 通过 Hooks 获取上下文(4.5.0+)
|
||||||
|
en-US: Get context with hooks (4.5.0+)
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
|
||||||
|
Use `message.useMessage` to get `contextHolder` with context accessible issue.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { message, Button } from 'antd';
|
||||||
|
|
||||||
|
const Context = React.createContext({ name: 'Default' });
|
||||||
|
|
||||||
|
function Demo() {
|
||||||
|
const [messsageApi, contextHolder] = message.useMessage();
|
||||||
|
const info = () => {
|
||||||
|
messsageApi.open({
|
||||||
|
type: 'info',
|
||||||
|
content: <Context.Consumer>{({ name }) => `Hello, ${name}!`}</Context.Consumer>,
|
||||||
|
duration: 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{ name: 'Ant Design' }}>
|
||||||
|
{contextHolder}
|
||||||
|
<Button type="primary" onClick={info}>
|
||||||
|
Display normal message
|
||||||
|
</Button>
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.render(<Demo />, mountNode);
|
||||||
|
```
|
92
components/message/hooks/useMessage.tsx
Normal file
92
components/message/hooks/useMessage.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import useRCNotification from 'rc-notification/lib/useNotification';
|
||||||
|
import {
|
||||||
|
NotificationInstance as RCNotificationInstance,
|
||||||
|
NoticeContent as RCNoticeContent,
|
||||||
|
HolderReadyCallback as RCHolderReadyCallback,
|
||||||
|
} from 'rc-notification/lib/Notification';
|
||||||
|
import { ConfigConsumer, ConfigConsumerProps } from '../../config-provider';
|
||||||
|
import {
|
||||||
|
MessageInstance,
|
||||||
|
ArgsProps,
|
||||||
|
attachTypeApi,
|
||||||
|
ThenableArgument,
|
||||||
|
getKeyThenIncreaseKey,
|
||||||
|
} 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'];
|
||||||
|
|
||||||
|
// 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 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,
|
||||||
|
},
|
||||||
|
({ 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;
|
||||||
|
|
||||||
|
['success', 'info', 'warning', 'error', 'loading'].forEach(type =>
|
||||||
|
attachTypeApi(hookApiRef.current, type),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
hookApiRef.current,
|
||||||
|
<ConfigConsumer key="holder">
|
||||||
|
{(context: ConfigConsumerProps) => {
|
||||||
|
({ getPrefixCls } = context);
|
||||||
|
return holder;
|
||||||
|
}}
|
||||||
|
</ConfigConsumer>,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
return useMessage;
|
||||||
|
}
|
@ -90,3 +90,27 @@ message.config({
|
|||||||
| top | Distance from top | number | 24 | |
|
| top | Distance from top | number | 24 | |
|
||||||
| rtl | Whether to enable RTL mode | boolean | false | |
|
| rtl | Whether to enable RTL mode | boolean | false | |
|
||||||
| prefixCls | The prefix className of message node | string | `ant-message` | 4.5.0 |
|
| prefixCls | The prefix className of message node | string | `ant-message` | 4.5.0 |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Why I can not access context, redux in message?
|
||||||
|
|
||||||
|
antd will dynamic create React instance by `ReactDOM.render` when call message methods. Whose context is different with origin code located context.
|
||||||
|
|
||||||
|
When you need context info (like ConfigProvider context), you can use `message.useMessage` to get `api` instance and `contextHolder` node. And put it in your children:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const [api, contextHolder] = message.useMessage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context1.Provider value="Ant">
|
||||||
|
{/* contextHolder is inside Context1 which means api will get value of Context1 */}
|
||||||
|
{contextHolder}
|
||||||
|
<Context2.Provider value="Design">
|
||||||
|
{/* contextHolder is outside Context2 which means api will **not** get value of Context2 */}
|
||||||
|
</Context2.Provider>
|
||||||
|
</Context1.Provider>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection.
|
||||||
|
@ -1,28 +1,83 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Notification from 'rc-notification';
|
import RCNotification from 'rc-notification';
|
||||||
|
import {
|
||||||
|
NotificationInstance as RCNotificationInstance,
|
||||||
|
NoticeContent,
|
||||||
|
} from 'rc-notification/lib/Notification';
|
||||||
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
|
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
|
||||||
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
|
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
|
||||||
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
||||||
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
|
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
|
||||||
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
|
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
|
||||||
|
import createUseMessage from './hooks/useMessage';
|
||||||
|
|
||||||
|
type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
|
||||||
|
|
||||||
|
let messageInstance: RCNotificationInstance | null;
|
||||||
let defaultDuration = 3;
|
let defaultDuration = 3;
|
||||||
let defaultTop: number;
|
let defaultTop: number;
|
||||||
let messageInstance: any;
|
|
||||||
let key = 1;
|
let key = 1;
|
||||||
let prefixCls = 'ant-message';
|
let localPrefixCls = 'ant-message';
|
||||||
let transitionName = 'move-up';
|
let transitionName = 'move-up';
|
||||||
let getContainer: () => HTMLElement;
|
let getContainer: () => HTMLElement;
|
||||||
let maxCount: number;
|
let maxCount: number;
|
||||||
let rtl = false;
|
let rtl = false;
|
||||||
|
|
||||||
function getMessageInstance(callback: (i: any) => void) {
|
export function getKeyThenIncreaseKey() {
|
||||||
|
return key++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigOptions {
|
||||||
|
top?: number;
|
||||||
|
duration?: number;
|
||||||
|
prefixCls?: string;
|
||||||
|
getContainer?: () => HTMLElement;
|
||||||
|
transitionName?: string;
|
||||||
|
maxCount?: number;
|
||||||
|
rtl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMessageConfig(options: ConfigOptions) {
|
||||||
|
if (options.top !== undefined) {
|
||||||
|
defaultTop = options.top;
|
||||||
|
messageInstance = null; // delete messageInstance for new defaultTop
|
||||||
|
}
|
||||||
|
if (options.duration !== undefined) {
|
||||||
|
defaultDuration = options.duration;
|
||||||
|
}
|
||||||
|
if (options.prefixCls !== undefined) {
|
||||||
|
localPrefixCls = options.prefixCls;
|
||||||
|
}
|
||||||
|
if (options.getContainer !== undefined) {
|
||||||
|
getContainer = options.getContainer;
|
||||||
|
}
|
||||||
|
if (options.transitionName !== undefined) {
|
||||||
|
transitionName = options.transitionName;
|
||||||
|
messageInstance = null; // delete messageInstance for new transitionName
|
||||||
|
}
|
||||||
|
if (options.maxCount !== undefined) {
|
||||||
|
maxCount = options.maxCount;
|
||||||
|
messageInstance = null;
|
||||||
|
}
|
||||||
|
if (options.rtl !== undefined) {
|
||||||
|
rtl = options.rtl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRCNotificationInstance(
|
||||||
|
args: ArgsProps,
|
||||||
|
callback: (info: { prefixCls: string; instance: RCNotificationInstance }) => void,
|
||||||
|
) {
|
||||||
|
const prefixCls = args.prefixCls || localPrefixCls;
|
||||||
if (messageInstance) {
|
if (messageInstance) {
|
||||||
callback(messageInstance);
|
callback({
|
||||||
|
prefixCls,
|
||||||
|
instance: messageInstance,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Notification.newInstance(
|
RCNotification.newInstance(
|
||||||
{
|
{
|
||||||
prefixCls,
|
prefixCls,
|
||||||
transitionName,
|
transitionName,
|
||||||
@ -32,17 +87,21 @@ function getMessageInstance(callback: (i: any) => void) {
|
|||||||
},
|
},
|
||||||
(instance: any) => {
|
(instance: any) => {
|
||||||
if (messageInstance) {
|
if (messageInstance) {
|
||||||
callback(messageInstance);
|
callback({
|
||||||
|
prefixCls,
|
||||||
|
instance: messageInstance,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
messageInstance = instance;
|
messageInstance = instance;
|
||||||
callback(instance);
|
callback({
|
||||||
|
prefixCls,
|
||||||
|
instance,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
|
|
||||||
|
|
||||||
export interface ThenableArgument {
|
export interface ThenableArgument {
|
||||||
(val: any): void;
|
(val: any): void;
|
||||||
}
|
}
|
||||||
@ -53,10 +112,18 @@ export interface MessageType {
|
|||||||
promise: Promise<void>;
|
promise: Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const typeToIcon = {
|
||||||
|
info: InfoCircleFilled,
|
||||||
|
success: CheckCircleFilled,
|
||||||
|
error: CloseCircleFilled,
|
||||||
|
warning: ExclamationCircleFilled,
|
||||||
|
loading: LoadingOutlined,
|
||||||
|
};
|
||||||
export interface ArgsProps {
|
export interface ArgsProps {
|
||||||
content: React.ReactNode;
|
content: React.ReactNode;
|
||||||
duration: number | null;
|
duration: number | null;
|
||||||
type: NoticeType;
|
type: NoticeType;
|
||||||
|
prefixCls?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
key?: string | number;
|
key?: string | number;
|
||||||
@ -64,34 +131,15 @@ export interface ArgsProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap = {
|
function getRCNoticeProps(args: ArgsProps, prefixCls: string): NoticeContent {
|
||||||
info: InfoCircleFilled,
|
|
||||||
success: CheckCircleFilled,
|
|
||||||
error: CloseCircleFilled,
|
|
||||||
warning: ExclamationCircleFilled,
|
|
||||||
loading: LoadingOutlined,
|
|
||||||
};
|
|
||||||
|
|
||||||
function notice(args: ArgsProps): MessageType {
|
|
||||||
const duration = args.duration !== undefined ? args.duration : defaultDuration;
|
const duration = args.duration !== undefined ? args.duration : defaultDuration;
|
||||||
const IconComponent = iconMap[args.type];
|
const IconComponent = typeToIcon[args.type];
|
||||||
|
|
||||||
const messageClass = classNames(`${prefixCls}-custom-content`, {
|
const messageClass = classNames(`${prefixCls}-custom-content`, {
|
||||||
[`${prefixCls}-${args.type}`]: args.type,
|
[`${prefixCls}-${args.type}`]: args.type,
|
||||||
[`${prefixCls}-rtl`]: rtl === true,
|
[`${prefixCls}-rtl`]: rtl === true,
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
const target = args.key || key++;
|
key: args.key,
|
||||||
const closePromise = new Promise(resolve => {
|
|
||||||
const callback = () => {
|
|
||||||
if (typeof args.onClose === 'function') {
|
|
||||||
args.onClose();
|
|
||||||
}
|
|
||||||
return resolve(true);
|
|
||||||
};
|
|
||||||
getMessageInstance(instance => {
|
|
||||||
instance.notice({
|
|
||||||
key: target,
|
|
||||||
duration,
|
duration,
|
||||||
style: args.style || {},
|
style: args.style || {},
|
||||||
className: args.className,
|
className: args.className,
|
||||||
@ -101,8 +149,21 @@ function notice(args: ArgsProps): MessageType {
|
|||||||
<span>{args.content}</span>
|
<span>{args.content}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
onClose: callback,
|
onClose: args.onClose,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function notice(args: ArgsProps): MessageType {
|
||||||
|
const target = args.key || key++;
|
||||||
|
const closePromise = new Promise(resolve => {
|
||||||
|
const callback = () => {
|
||||||
|
if (typeof args.onClose === 'function') {
|
||||||
|
args.onClose();
|
||||||
|
}
|
||||||
|
return resolve(true);
|
||||||
|
};
|
||||||
|
getRCNotificationInstance(args, ({ prefixCls, instance }) => {
|
||||||
|
instance.notice(getRCNoticeProps({ ...args, key: target, onClose: callback }, prefixCls));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const result: any = () => {
|
const result: any = () => {
|
||||||
@ -128,44 +189,9 @@ function isArgsProps(content: JointContent): content is ArgsProps {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigOptions {
|
|
||||||
top?: number;
|
|
||||||
duration?: number;
|
|
||||||
prefixCls?: string;
|
|
||||||
getContainer?: () => HTMLElement;
|
|
||||||
transitionName?: string;
|
|
||||||
maxCount?: number;
|
|
||||||
rtl?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const api: any = {
|
const api: any = {
|
||||||
open: notice,
|
open: notice,
|
||||||
config(options: ConfigOptions) {
|
config: setMessageConfig,
|
||||||
if (options.top !== undefined) {
|
|
||||||
defaultTop = options.top;
|
|
||||||
messageInstance = null; // delete messageInstance for new defaultTop
|
|
||||||
}
|
|
||||||
if (options.duration !== undefined) {
|
|
||||||
defaultDuration = options.duration;
|
|
||||||
}
|
|
||||||
if (options.prefixCls !== undefined) {
|
|
||||||
prefixCls = options.prefixCls;
|
|
||||||
}
|
|
||||||
if (options.getContainer !== undefined) {
|
|
||||||
getContainer = options.getContainer;
|
|
||||||
}
|
|
||||||
if (options.transitionName !== undefined) {
|
|
||||||
transitionName = options.transitionName;
|
|
||||||
messageInstance = null; // delete messageInstance for new transitionName
|
|
||||||
}
|
|
||||||
if (options.maxCount !== undefined) {
|
|
||||||
maxCount = options.maxCount;
|
|
||||||
messageInstance = null;
|
|
||||||
}
|
|
||||||
if (options.rtl !== undefined) {
|
|
||||||
rtl = options.rtl;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (messageInstance) {
|
if (messageInstance) {
|
||||||
messageInstance.destroy();
|
messageInstance.destroy();
|
||||||
@ -174,10 +200,14 @@ const api: any = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
['success', 'info', 'warning', 'error', 'loading'].forEach(type => {
|
export function attachTypeApi(originalApi: any, type: string) {
|
||||||
api[type] = (content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose) => {
|
originalApi[type] = (
|
||||||
|
content: JointContent,
|
||||||
|
duration?: ConfigDuration,
|
||||||
|
onClose?: ConfigOnClose,
|
||||||
|
) => {
|
||||||
if (isArgsProps(content)) {
|
if (isArgsProps(content)) {
|
||||||
return api.open({ ...content, type });
|
return originalApi.open({ ...content, type });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof duration === 'function') {
|
if (typeof duration === 'function') {
|
||||||
@ -185,22 +215,29 @@ const api: any = {
|
|||||||
duration = undefined;
|
duration = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.open({ content, duration, type, onClose });
|
return originalApi.open({ content, duration, type, onClose });
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
['success', 'info', 'warning', 'error', 'loading'].forEach(type => attachTypeApi(api, type));
|
||||||
|
|
||||||
api.warn = api.warning;
|
api.warn = api.warning;
|
||||||
|
api.useMessage = createUseMessage(getRCNotificationInstance, getRCNoticeProps);
|
||||||
|
|
||||||
export interface MessageApi {
|
export interface MessageInstance {
|
||||||
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
|
||||||
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
open(args: ArgsProps): MessageType;
|
open(args: ArgsProps): MessageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageApi extends MessageInstance {
|
||||||
|
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
config(options: ConfigOptions): void;
|
config(options: ConfigOptions): void;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
|
useMessage(): [MessageInstance, React.ReactElement];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default api as MessageApi;
|
export default api as MessageApi;
|
||||||
|
@ -91,3 +91,27 @@ message.config({
|
|||||||
| top | 消息距离顶部的位置 | number | 24 | |
|
| top | 消息距离顶部的位置 | number | 24 | |
|
||||||
| rtl | 是否开启 RTL 模式 | boolean | false | |
|
| rtl | 是否开启 RTL 模式 | boolean | false | |
|
||||||
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 4.5.0 |
|
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 4.5.0 |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### 为什么 message 不能获取 context、redux 的内容?
|
||||||
|
|
||||||
|
直接调用 message 方法,antd 会通过 `ReactDOM.render` 动态创建新的 React 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
|
||||||
|
|
||||||
|
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `message.useMessage` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const [api, contextHolder] = message.useMessage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context1.Provider value="Ant">
|
||||||
|
{/* contextHolder 在 Context1 内,它可以获得 Context1 的 context */}
|
||||||
|
{contextHolder}
|
||||||
|
<Context2.Provider value="Design">
|
||||||
|
{/* contextHolder 在 Context2 外,因而不会获得 Context2 的 context */}
|
||||||
|
</Context2.Provider>
|
||||||
|
</Context1.Provider>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**异同:**通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
|
||||||
|
@ -23,6 +23,7 @@ let defaultPrefixCls = 'ant-notification';
|
|||||||
let defaultPlacement: NotificationPlacement = 'topRight';
|
let defaultPlacement: NotificationPlacement = 'topRight';
|
||||||
let defaultGetContainer: () => HTMLElement;
|
let defaultGetContainer: () => HTMLElement;
|
||||||
let defaultCloseIcon: React.ReactNode;
|
let defaultCloseIcon: React.ReactNode;
|
||||||
|
let rtl = false;
|
||||||
|
|
||||||
export interface ConfigProps {
|
export interface ConfigProps {
|
||||||
top?: number;
|
top?: number;
|
||||||
@ -35,7 +36,6 @@ export interface ConfigProps {
|
|||||||
rtl?: boolean;
|
rtl?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rtl = false;
|
|
||||||
function setNotificationConfig(options: ConfigProps) {
|
function setNotificationConfig(options: ConfigProps) {
|
||||||
const { duration, placement, bottom, top, getContainer, closeIcon, prefixCls } = options;
|
const { duration, placement, bottom, top, getContainer, closeIcon, prefixCls } = options;
|
||||||
if (prefixCls !== undefined) {
|
if (prefixCls !== undefined) {
|
||||||
@ -225,12 +225,14 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const api: any = {
|
function notice(args: ArgsProps) {
|
||||||
open: (args: ArgsProps) => {
|
|
||||||
getNotificationInstance(args, ({ prefixCls, instance }) => {
|
getNotificationInstance(args, ({ prefixCls, instance }) => {
|
||||||
instance.notice(getRCNoticeProps(args, prefixCls));
|
instance.notice(getRCNoticeProps(args, prefixCls));
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
|
const api: any = {
|
||||||
|
open: notice,
|
||||||
close(key: string) {
|
close(key: string) {
|
||||||
Object.keys(notificationInstance).forEach(cacheKey =>
|
Object.keys(notificationInstance).forEach(cacheKey =>
|
||||||
Promise.resolve(notificationInstance[cacheKey]).then(instance => {
|
Promise.resolve(notificationInstance[cacheKey]).then(instance => {
|
||||||
|
Loading…
Reference in New Issue
Block a user