feat(App): add message & notification options (#40456) (#40458)

* feat(App): add message & notification options

* chore: test

* docs: App docs

* feat: config inherit in nest app

* feat: provide & consume config context internally

* docs(App): property literal

* fix(App): repect config from props in priority

* Update components/app/index.en-US.md

* Update components/app/index.zh-CN.md

---------

Co-authored-by: MadCcc <1075746765@qq.com>
This commit is contained in:
luo3house 2023-02-15 15:05:21 +08:00 committed by GitHub
parent 5c1162a140
commit 9f98fc243e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 171 additions and 26 deletions

View File

@ -1,13 +1,24 @@
import React from 'react';
import React, { useEffect } from 'react';
import App from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { render } from '../../../tests/utils';
import { render, waitFakeTimer } from '../../../tests/utils';
import type { AppConfig } from '../context';
import { AppConfigContext } from '../context';
describe('App', () => {
mountTest(App);
rtlTest(App);
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});
it('single', () => {
// Sub page
const MyPage = () => {
@ -30,4 +41,85 @@ describe('App', () => {
expect(getByText('Hello World')).toBeTruthy();
expect(container.firstChild).toMatchSnapshot();
});
it('should work as message and notification config configured in app', async () => {
let consumedConfig: AppConfig | undefined;
const Consumer = () => {
const { message, notification } = App.useApp();
consumedConfig = React.useContext(AppConfigContext);
useEffect(() => {
message.success('Message 1');
message.success('Message 2');
notification.success({ message: 'Notification 1' });
notification.success({ message: 'Notification 2' });
notification.success({ message: 'Notification 3' });
}, [message, notification]);
return <div />;
};
const Wrapper = () => (
<App message={{ maxCount: 1 }} notification={{ maxCount: 2 }}>
<Consumer />
</App>
);
render(<Wrapper />);
await waitFakeTimer();
expect(consumedConfig?.message).toStrictEqual({ maxCount: 1 });
expect(consumedConfig?.notification).toStrictEqual({ maxCount: 2 });
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(2);
});
it('should be a merged config configured in nested app', async () => {
let offsetConsumedConfig: AppConfig | undefined;
let maxCountConsumedConfig: AppConfig | undefined;
const OffsetConsumer = () => {
offsetConsumedConfig = React.useContext(AppConfigContext);
return <div />;
};
const MaxCountConsumer = () => {
maxCountConsumedConfig = React.useContext(AppConfigContext);
return <div />;
};
const Wrapper = () => (
<App message={{ maxCount: 1 }} notification={{ maxCount: 2 }}>
<App message={{ top: 32 }} notification={{ top: 96 }}>
<OffsetConsumer />
</App>
<MaxCountConsumer />
</App>
);
render(<Wrapper />);
expect(offsetConsumedConfig?.message).toStrictEqual({ maxCount: 1, top: 32 });
expect(offsetConsumedConfig?.notification).toStrictEqual({ maxCount: 2, top: 96 });
expect(maxCountConsumedConfig?.message).toStrictEqual({ maxCount: 1 });
expect(maxCountConsumedConfig?.notification).toStrictEqual({ maxCount: 2 });
});
it('should respect config from props in priority', async () => {
let config: AppConfig | undefined;
const Consumer = () => {
config = React.useContext(AppConfigContext);
return <div />;
};
const Wrapper = () => (
<App message={{ maxCount: 10, top: 20 }} notification={{ maxCount: 30, bottom: 40 }}>
<App message={{ maxCount: 11 }} notification={{ bottom: 41 }}>
<Consumer />
</App>
</App>
);
render(<Wrapper />);
expect(config?.message).toStrictEqual({ maxCount: 11, top: 20 });
expect(config?.notification).toStrictEqual({ maxCount: 30, bottom: 41 });
});
});

View File

@ -1,8 +1,15 @@
import React from 'react';
import type { MessageInstance } from '../message/interface';
import type { NotificationInstance } from '../notification/interface';
import type { MessageInstance, ConfigOptions as MessageConfig } from '../message/interface';
import type { NotificationInstance, NotificationConfig } from '../notification/interface';
import type { ModalStaticFunctions } from '../modal/confirm';
export type AppConfig = {
message?: MessageConfig;
notification?: NotificationConfig;
};
export const AppConfigContext = React.createContext<AppConfig>({});
type ModalType = Omit<ModalStaticFunctions, 'warn'>;
export interface useAppProps {
message: MessageInstance;

View File

@ -117,3 +117,12 @@ export default () => {
);
};
```
## API
### App
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| message | Global config for Message | [MessageConfig](/components/message/#messageconfig) | - | 5.3.0 |
| notification | Global config for Notification | [NotificationConfig](/components/notification/#notificationconfig) | - | 5.3.0 |

View File

@ -6,8 +6,8 @@ import { ConfigContext } from '../config-provider';
import useMessage from '../message/useMessage';
import useModal from '../modal/useModal';
import useNotification from '../notification/useNotification';
import type { useAppProps } from './context';
import AppContext from './context';
import AppContext, { AppConfigContext } from './context';
import type { AppConfig, useAppProps } from './context';
import useStyle from './style';
export type AppProps = {
@ -15,19 +15,37 @@ export type AppProps = {
rootClassName?: string;
prefixCls?: string;
children?: ReactNode;
};
} & AppConfig;
const useApp = () => React.useContext<useAppProps>(AppContext);
const useApp = () => React.useContext(AppContext);
const App: React.FC<AppProps> & { useApp: () => useAppProps } = (props) => {
const { prefixCls: customizePrefixCls, children, className, rootClassName } = props;
const App: React.FC<AppProps> & { useApp: typeof useApp } = (props) => {
const {
prefixCls: customizePrefixCls,
children,
className,
rootClassName,
message,
notification,
} = props;
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
const prefixCls = getPrefixCls('app', customizePrefixCls);
const [wrapSSR, hashId] = useStyle(prefixCls);
const customClassName = classNames(hashId, prefixCls, className, rootClassName);
const [messageApi, messageContextHolder] = useMessage();
const [notificationApi, notificationContextHolder] = useNotification();
const appConfig = useContext(AppConfigContext);
const mergedAppConfig = React.useMemo<AppConfig>(
() => ({
message: { ...appConfig.message, ...message },
notification: { ...appConfig.notification, ...notification },
}),
[message, notification, appConfig.message, appConfig.message],
);
const [messageApi, messageContextHolder] = useMessage(mergedAppConfig.message);
const [notificationApi, notificationContextHolder] = useNotification(
mergedAppConfig.notification,
);
const [ModalApi, ModalContextHolder] = useModal();
const memoizedContextValue = React.useMemo<useAppProps>(
@ -41,12 +59,14 @@ const App: React.FC<AppProps> & { useApp: () => useAppProps } = (props) => {
return wrapSSR(
<AppContext.Provider value={memoizedContextValue}>
<div className={customClassName}>
{ModalContextHolder}
{messageContextHolder}
{notificationContextHolder}
{children}
</div>
<AppConfigContext.Provider value={mergedAppConfig}>
<div className={customClassName}>
{ModalContextHolder}
{messageContextHolder}
{notificationContextHolder}
{children}
</div>
</AppConfigContext.Provider>
</AppContext.Provider>,
);
};

View File

@ -119,3 +119,12 @@ export default () => {
);
};
```
## API
### App
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| message | App 内 Message 的全局配置 | [MessageConfig](/components/message-cn/#messageconfig) | - | 5.3.0 |
| notification | App 内 Notification 的全局配置 | [NotificationConfig](/components/notification-cn/#notificationconfig) | - | 5.3.0 |

View File

@ -65,11 +65,15 @@ The properties of config are as follows:
`notification` also provides a global `config()` method that can be used for specifying the default options. Once this method is used, all the notification boxes will take into account these globally defined options when displaying.
- `notification.config(options)`
### Global configuration
> When you use `ConfigProvider` for global configuration, the system will automatically start RTL mode by default.(4.3.0+)
>
> When you want to use it alone, you can start the RTL mode through the following settings.
`notification.config(options)`
> When you use `ConfigProvider` for global configuration, the system will automatically start RTL mode by default.(4.3.0+)
>
> When you want to use it alone, you can start the RTL mode through the following settings.
#### notification.config
```js
notification.config({

View File

@ -63,13 +63,15 @@ config 参数如下:
| onClick | 点击通知时触发的回调函数 | function | - |
| onClose | 当通知关闭时触发 | function | - |
### 全局配置
还提供了一个全局配置方法,在调用前提前配置,全局一次生效。
- `notification.config(options)`
`notification.config(options)`
> 当你使用 `ConfigProvider` 进行全局化配置时,系统会默认自动开启 RTL 模式。(4.3.0+)
>
> 当你想单独使用,可通过如下设置开启 RTL 模式。
> 当你使用 `ConfigProvider` 进行全局化配置时,系统会默认自动开启 RTL 模式。(4.3.0+)
>
> 当你想单独使用,可通过如下设置开启 RTL 模式。
```js
notification.config({
@ -80,6 +82,8 @@ notification.config({
});
```
#### notification.config
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| bottom | 消息从底部弹出时,距离底部的位置,单位像素 | number | 24 | |