feat: meta添加app、notification组件

This commit is contained in:
NICE CODE BY DEV 2024-02-21 17:20:54 +08:00
parent 43741393f7
commit e03338ab02
70 changed files with 5263 additions and 12 deletions

4
global.d.ts vendored
View File

@ -1,4 +0,0 @@
declare module '@zhst/func';
declare module '@zhst/hooks';
declare module '@zhst/meta';
declare module '@zhst/request';

View File

@ -38,6 +38,7 @@
"@types/zhst": "workspace:^"
},
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@zhst/func": "workspace:^",
"@zhst/hooks": "workspace:^",
"@zhst/meta": "workspace:^",

View File

@ -16,7 +16,11 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
import React, { useEffect, useState, useCallback, useRef, useImperativeHandle } from 'react';
import classNames from 'classnames';
import { useLatest } from '@zhst/hooks';
import { get, pick, isNull, generateImg, dataURLToBlob,
import { get, pick, isNull,
// @ts-ignore
generateImg,
// @ts-ignore
dataURLToBlob,
// @ts-ignore
getOdRect,
// @ts-ignore
@ -24,7 +28,9 @@ getExtendRect,
// @ts-ignore
getTransformRect,
// @ts-ignore
getRotateImg, getTransforms, addEventListenerWrapper, getFileByRect
getRotateImg, getTransforms, addEventListenerWrapper,
// @ts-ignore
getFileByRect
// @ts-ignore
} from '@zhst/func';
import Align from 'rc-align';

View File

@ -1,6 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Button, Tooltip } from 'antd';
// @ts-ignore
import { Button, Tooltip } from "../../..";
import Icon from "../../../iconfont";
import "./index.less";
var componentName = "zhst-image__btn-group";

View File

@ -14,7 +14,9 @@ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { noop, get, addEventListenerWrapper, dataURLToBlob, nextTick, toRealNumber, getTransforms, formatDurationTime
import { noop, get, addEventListenerWrapper,
// @ts-ignore
dataURLToBlob, nextTick, toRealNumber, getTransforms, formatDurationTime
// @ts-ignore
} from '@zhst/func';
import Align from 'rc-align';

View File

@ -41,4 +41,6 @@ export { default as Card } from "./card";
export { default as Skeleton } from "./skeleton";
export { default as Tooltip } from "./tooltip";
export { default as Tour } from "./tour";
export { default as Segmented } from "./segmented";
export { default as Segmented } from "./segmented";
export { default as App } from "./app";
export { default as notification } from "./notification";

View File

@ -5,7 +5,9 @@ import {
get,
pick,
isNull,
// @ts-ignore
generateImg,
// @ts-ignore
dataURLToBlob,
// @ts-ignore
getOdRect,
@ -17,6 +19,7 @@ import {
getRotateImg,
getTransforms,
addEventListenerWrapper,
// @ts-ignore
getFileByRect
// @ts-ignore
} from '@zhst/func';

View File

@ -1,6 +1,7 @@
import React, { MouseEvent } from 'react';
import classNames from 'classnames';
import { Button, Tooltip, TooltipProps } from 'antd';
// @ts-ignore
import { Button, Tooltip, TooltipProps } from '../../..';
import Icon from '../../../iconfont';
import './index.less';
@ -38,7 +39,7 @@ export const BtnGroup: React.FC<BtnGroupProps> = (props) => {
>
<Button
type="text"
onClick={(e) => {
onClick={(e: React.MouseEvent<HTMLElement, globalThis.MouseEvent>) => {
onClick(key, e);
}}
>

View File

@ -1,6 +1,6 @@
import React, { useRef } from 'react';
import { Button, Space } from 'antd'
import { Button, Space } from '@zhst/meta'
import { BigImagePreview } from '@zhst/meta'

View File

@ -3,6 +3,7 @@ import {
noop,
get,
addEventListenerWrapper,
// @ts-ignore
dataURLToBlob,
nextTick,
toRealNumber,

View File

@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders components/app/demo/basic.tsx extend context correctly 1`] = `
<div
class="ant-app"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open message
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open modal
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open notification
</span>
</button>
</div>
</div>
</div>
`;
exports[`renders components/app/demo/basic.tsx extend context correctly 2`] = `[]`;
exports[`renders components/app/demo/config.tsx extend context correctly 1`] = `
<div
class="ant-app"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Message for only one
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Notification for bottomLeft
</span>
</button>
</div>
</div>
</div>
`;
exports[`renders components/app/demo/config.tsx extend context correctly 2`] = `[]`;

View File

@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders components/app/demo/basic.tsx correctly 1`] = `
<div
class="ant-app"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open message
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open modal
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open notification
</span>
</button>
</div>
</div>
</div>
`;
exports[`renders components/app/demo/config.tsx correctly 1`] = `
<div
class="ant-app"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Message for only one
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Notification for bottomLeft
</span>
</button>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-app"
/>
`;
exports[`App single 1`] = `
<div
class="ant-app"
>
<div>
Hello World
</div>
</div>
`;

View File

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('app');

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('app');

View File

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('app', () => {
imageDemoTest('app');
});

View File

@ -0,0 +1,236 @@
import React, { useEffect } from 'react';
import { SmileOutlined } from '@ant-design/icons';
import type { NotificationConfig } from 'antd/es/notification/interface';
import App from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
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: React.FC = () => {
const { message } = App.useApp();
React.useEffect(() => {
message.success('Good!');
}, [message]);
return <div>Hello World</div>;
};
// Entry component
const MyApp: React.FC = () => (
<App>
<MyPage />
</App>
);
const { getByText, container } = render(<MyApp />);
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 });
});
it('should respect notification placement config from props in priority', async () => {
let consumedConfig: AppConfig | undefined;
const Consumer = () => {
const { notification } = App.useApp();
consumedConfig = React.useContext(AppConfigContext);
useEffect(() => {
notification.success({ message: 'Notification 1' });
notification.success({ message: 'Notification 2' });
notification.success({ message: 'Notification 3' });
}, [notification]);
return <div />;
};
const config: NotificationConfig = {
placement: 'bottomLeft',
top: 100,
bottom: 50,
};
const Wrapper = () => (
<App notification={config}>
<Consumer />
</App>
);
render(<Wrapper />);
await waitFakeTimer();
expect(consumedConfig?.notification).toStrictEqual(config);
expect(document.querySelector('.ant-notification-topRight')).not.toBeInTheDocument();
expect(document.querySelector('.ant-notification-bottomLeft')).toHaveStyle({
top: '',
left: '0px',
bottom: '50px',
});
});
it('support className', () => {
const { container } = render(
<App className="test-class">
<div>test</div>
</App>,
);
expect(container.querySelector<HTMLDivElement>('.ant-app')).toHaveClass('test-class');
});
it('support style', () => {
const { container } = render(
<App style={{ color: 'blue' }}>
<div>test</div>
</App>,
);
expect(container.querySelector<HTMLDivElement>('.ant-app')).toHaveStyle('color: blue;');
});
// https://github.com/ant-design/ant-design/issues/41197#issuecomment-1465803061
describe('restIcon style', () => {
beforeEach(() => {
Array.from(document.querySelectorAll('style')).forEach((style) => {
style.parentNode?.removeChild(style);
});
});
it('should work by default', () => {
const { container } = render(
<App>
<SmileOutlined />
</App>,
);
expect(container.querySelector('.anticon')).toBeTruthy();
const dynamicStyles = Array.from(document.querySelectorAll('style[data-css-hash]'));
expect(
dynamicStyles.some((style) => {
const { innerHTML } = style;
return innerHTML.startsWith('.anticon');
}),
).toBeTruthy();
});
});
describe('component', () => {
it('replace', () => {
const { container } = render(
<App component="section">
<p />
</App>,
);
expect(container.querySelector('section.ant-app')).toBeTruthy();
});
it('to false', () => {
const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const { container } = render(
<App component={false}>
<p />
</App>,
);
expect(warnSpy).not.toHaveBeenCalled();
expect(container.querySelector('.ant-app')).toBeFalsy();
warnSpy.mockRestore();
});
});
});

View File

@ -0,0 +1,26 @@
import React from 'react';
import type { ConfigOptions as MessageConfig, MessageInstance } from '../message/interface';
import type { HookAPI as ModalHookAPI } from '../modal/useModal';
import type { NotificationConfig, NotificationInstance } from '../notification/interface';
export interface AppConfig {
message?: MessageConfig;
notification?: NotificationConfig;
}
export const AppConfigContext = React.createContext<AppConfig>({});
export interface useAppProps {
message: MessageInstance;
notification: NotificationInstance;
modal: ModalHookAPI;
}
const AppContext = React.createContext<useAppProps>({
message: {},
notification: {},
modal: {},
} as useAppProps);
export default AppContext;

View File

@ -0,0 +1,7 @@
## zh-CN
获取 `message`、`notification`、`modal` 实例。
## en-US
Get instance for `message`, `notification`, `modal`.

View File

@ -0,0 +1,47 @@
import React from 'react';
import { App, Button, Space } from 'antd';
// Sub page
const MyPage = () => {
const { message, modal, notification } = App.useApp();
const showMessage = () => {
message.success('Success!');
};
const showModal = () => {
modal.warning({
title: 'This is a warning message',
content: 'some messages...some messages...',
});
};
const showNotification = () => {
notification.info({
message: `Notification topLeft`,
description: 'Hello, Ant Design!!',
placement: 'topLeft',
});
};
return (
<Space>
<Button type="primary" onClick={showMessage}>
Open message
</Button>
<Button type="primary" onClick={showModal}>
Open modal
</Button>
<Button type="primary" onClick={showNotification}>
Open notification
</Button>
</Space>
);
};
// Entry component
export default () => (
<App>
<MyPage />
</App>
);

View File

@ -0,0 +1,7 @@
## zh-CN
`message`、`notification` 进行配置。
## en-US
Config for `message`, `notification`.

View File

@ -0,0 +1,36 @@
import React from 'react';
import { App, Button, Space } from 'antd';
// Sub page
const MyPage = () => {
const { message, notification } = App.useApp();
const showMessage = () => {
message.success('Success!');
};
const showNotification = () => {
notification.info({
message: `Notification`,
description: 'Hello, Ant Design!!',
});
};
return (
<Space>
<Button type="primary" onClick={showMessage}>
Message for only one
</Button>
<Button type="primary" onClick={showNotification}>
Notification for bottomLeft
</Button>
</Space>
);
};
// Entry component
export default () => (
<App message={{ maxCount: 1 }} notification={{ placement: 'bottomLeft' }}>
<MyPage />
</App>
);

View File

@ -0,0 +1,94 @@
import type { ReactNode } from 'react';
import React, { useContext } from 'react';
import classNames from 'classnames';
import type { AnyObject, CustomComponent } from '../_util/type';
import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import useMessage from '../message/useMessage';
import useModal from '../modal/useModal';
import useNotification from '../notification/useNotification';
import type { AppConfig, useAppProps } from './context';
import AppContext, { AppConfigContext } from './context';
import useStyle from './style';
export interface AppProps<P = AnyObject> extends AppConfig {
style?: React.CSSProperties;
className?: string;
rootClassName?: string;
prefixCls?: string;
children?: ReactNode;
component?: CustomComponent<P> | false;
}
const useApp = () => React.useContext<useAppProps>(AppContext);
const App: React.FC<AppProps> & { useApp: () => useAppProps } = (props) => {
const {
prefixCls: customizePrefixCls,
children,
className,
rootClassName,
message,
notification,
style,
component = 'div',
} = props;
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
const prefixCls = getPrefixCls('app', customizePrefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const customClassName = classNames(hashId, prefixCls, className, rootClassName, cssVarCls);
const appConfig = useContext<AppConfig>(AppConfigContext);
const mergedAppConfig = React.useMemo<AppConfig>(
() => ({
message: { ...appConfig.message, ...message },
notification: { ...appConfig.notification, ...notification },
}),
[message, notification, appConfig.message, appConfig.notification],
);
const [messageApi, messageContextHolder] = useMessage(mergedAppConfig.message);
const [notificationApi, notificationContextHolder] = useNotification(
mergedAppConfig.notification,
);
const [ModalApi, ModalContextHolder] = useModal();
const memoizedContextValue = React.useMemo<useAppProps>(
() => ({
message: messageApi,
notification: notificationApi,
modal: ModalApi,
}),
[messageApi, notificationApi, ModalApi],
);
// ============================ Render ============================
const Component = component === false ? React.Fragment : component;
const rootProps: AppProps = {
className: customClassName,
style,
};
return wrapCSSVar(
<AppContext.Provider value={memoizedContextValue}>
<AppConfigContext.Provider value={mergedAppConfig}>
<Component {...(component === false ? undefined : rootProps)}>
{ModalContextHolder}
{messageContextHolder}
{notificationContextHolder}
{children}
</Component>
</AppConfigContext.Provider>
</AppContext.Provider>,
);
};
if (process.env.NODE_ENV !== 'production') {
App.displayName = 'App';
}
App.useApp = useApp;
export default App;

View File

@ -0,0 +1,133 @@
---
category: Components
subtitle: 包裹组件
group: 其他
title: App 包裹组件
demo:
cols: 2
---
新的包裹组件,提供重置样式和提供消费上下文的默认环境。
## 何时使用
- 提供可消费 React context 的 `message.xxx`、`Modal.xxx`、`notification.xxx` 的静态方法,可以简化 useMessage 等方法需要手动植入 `contextHolder` 的问题。
- 提供基于 `.ant-app` 的默认重置样式,解决原生元素没有 antd 规范样式的问题。
## 代码演示
<!-- prettier-ignore -->
<code src="./demo/basic.tsx">基本用法</code>
<code src="./demo/config.tsx">Hooks 配置</code>
## 如何使用
### 基础用法
App 组件通过 `Context` 提供上下文方法调用,因而 useApp 需要作为子组件才能使用,我们推荐在应用中顶层包裹 App。
```ts
import React from 'react';
import { App } from 'antd';
const MyPage: React.FC = () => {
const { message, notification, modal } = App.useApp();
message.success('Good!');
notification.info({ message: 'Good' });
modal.warning({ title: 'Good' });
// ....
// other message, notification, modal static function
return <div>Hello word</div>;
};
const MyApp: React.FC = () => (
<App>
<MyPage />
</App>
);
export default MyApp;
```
注意App.useApp 必须在 App 之下方可使用。
### 与 ConfigProvider 先后顺序
App 组件只能在 `ConfigProvider` 之下才能使用 Design Token 如果需要使用其样式重置能力,则 ConfigProvider 与 App 组件必须成对出现。
```ts
<ConfigProvider theme={{ ... }}>
<App>
...
</App>
</ConfigProvider>
```
### 内嵌使用场景(如无必要,尽量不做嵌套)
```ts
<App>
<Space>
...
<App>...</App>
</Space>
</App>
```
### 全局场景redux 场景)
```ts
// Entry component
import { App } from 'antd';
import type { MessageInstance } from 'antd/es/message/interface';
import type { ModalStaticFunctions } from 'antd/es/modal/confirm';
import type { NotificationInstance } from 'antd/es/notification/interface';
let message: MessageInstance;
let notification: NotificationInstance;
let modal: Omit<ModalStaticFunctions, 'warn'>;
export default () => {
const staticFunction = App.useApp();
message = staticFunction.message;
modal = staticFunction.modal;
notification = staticFunction.notification;
return null;
};
export { message, notification, modal };
```
```ts
// sub page
import React from 'react';
import { Button, Space } from 'antd';
import { message } from './store';
export default () => {
const showMessage = () => {
message.success('Success!');
};
return (
<Space>
<Button type="primary" onClick={showMessage}>
Open message
</Button>
</Space>
);
};
```
## API
通用属性参考:[通用属性](/docs/react/common-props)
### App
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| component | 设置渲染元素,为 `false` 则不创建 DOM 节点 | ComponentType | div | 5.11.0 |
| message | App 内 Message 的全局配置 | [MessageConfig](/components/message-cn/#messageconfig) | - | 5.3.0 |
| notification | App 内 Notification 的全局配置 | [NotificationConfig](/components/notification-cn/#notificationconfig) | - | 5.3.0 |

View File

@ -0,0 +1,25 @@
import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
import { genStyleHooks } from '../../theme/internal';
export type ComponentToken = {};
// @ts-ignore
interface AppToken extends FullToken<'App'> {}
// =============================== Base ===============================
const genBaseStyle: GenerateStyle<AppToken> = (token) => {
const { componentCls, colorText, fontSize, lineHeight, fontFamily } = token;
return {
[componentCls]: {
color: colorText,
fontSize,
lineHeight,
fontFamily,
},
};
};
// @ts-ignore
export const prepareComponentToken: GetDefaultToken<'App'> = () => ({});
// ============================== Export ==============================
// @ts-ignore
export default genStyleHooks('App', genBaseStyle, prepareComponentToken);

View File

@ -81,3 +81,7 @@ export { default as Tour } from './tour'
export type { TourLocale, TourProps, TourStepProps } from './tour/interface'
export { default as Segmented } from './segmented'
export type { SegmentedLabeledOption, SegmentedProps, SegmentedValue } from './segmented'
export { default as App } from './app';
export type { AppProps } from './app';
export { default as notification } from './notification';
export type { ArgsProps as NotificationArgsProps } from './notification';

View File

@ -0,0 +1,135 @@
import * as React from 'react';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import classNames from 'classnames';
import { Notice } from 'rc-notification';
import type { NoticeProps } from 'rc-notification/lib/Notice';
import { ConfigContext } from '../config-provider';
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
import type { IconType } from './interface';
import useStyle from './style';
import PurePanelStyle from './style/pure-panel';
export const TypeIcon = {
info: <InfoCircleFilled />,
success: <CheckCircleFilled />,
error: <CloseCircleFilled />,
warning: <ExclamationCircleFilled />,
loading: <LoadingOutlined />,
};
export function getCloseIcon(prefixCls: string, closeIcon?: React.ReactNode): React.ReactNode {
if (closeIcon === null || closeIcon === false) {
return null;
}
return (
closeIcon || (
<span className={`${prefixCls}-close-x`}>
<CloseOutlined className={`${prefixCls}-close-icon`} />
</span>
)
);
}
export interface PureContentProps {
prefixCls: string;
icon?: React.ReactNode;
message?: React.ReactNode;
description?: React.ReactNode;
btn?: React.ReactNode;
type?: IconType;
role?: 'alert' | 'status';
}
const typeToIcon = {
success: CheckCircleFilled,
info: InfoCircleFilled,
error: CloseCircleFilled,
warning: ExclamationCircleFilled,
};
export const PureContent: React.FC<PureContentProps> = (props) => {
const { prefixCls, icon, type, message, description, btn, role = 'alert' } = props;
let iconNode: React.ReactNode = null;
if (icon) {
iconNode = <span className={`${prefixCls}-icon`}>{icon}</span>;
} else if (type) {
iconNode = React.createElement(typeToIcon[type] || null, {
className: classNames(`${prefixCls}-icon`, `${prefixCls}-icon-${type}`),
});
}
return (
<div className={classNames({ [`${prefixCls}-with-icon`]: iconNode })} role={role}>
{iconNode}
<div className={`${prefixCls}-message`}>{message}</div>
<div className={`${prefixCls}-description`}>{description}</div>
{btn && <div className={`${prefixCls}-btn`}>{btn}</div>}
</div>
);
};
export interface PurePanelProps
extends Omit<NoticeProps, 'prefixCls' | 'eventKey'>,
Omit<PureContentProps, 'prefixCls' | 'children'> {
prefixCls?: string;
}
/** @private Internal Component. Do not use in your production. */
const PurePanel: React.FC<PurePanelProps> = (props) => {
const {
prefixCls: staticPrefixCls,
className,
icon,
type,
message,
description,
btn,
closable = true,
closeIcon,
className: notificationClassName,
...restProps
} = props;
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = staticPrefixCls || getPrefixCls('notification');
const noticePrefixCls = `${prefixCls}-notice`;
const rootCls = useCSSVarCls(prefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
return wrapCSSVar(
<div
className={classNames(`${noticePrefixCls}-pure-panel`, hashId, className, cssVarCls, rootCls)}
>
<PurePanelStyle prefixCls={prefixCls} />
<Notice
{...restProps}
prefixCls={prefixCls}
eventKey="pure"
duration={null}
closable={closable}
className={classNames({
notificationClassName,
})}
closeIcon={getCloseIcon(prefixCls, closeIcon)}
content={
<PureContent
prefixCls={noticePrefixCls}
icon={icon}
type={type}
message={message}
description={description}
btn={btn}
/>
}
/>
</div>,
);
};
export default PurePanel;

View File

@ -0,0 +1,783 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders components/notification/demo/basic.tsx extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/basic.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/custom-icon.tsx extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/custom-icon.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/custom-style.tsx extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/custom-style.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/duration.tsx extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/duration.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/hooks.tsx extend context correctly 1`] = `
Array [
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-upleft"
class="anticon anticon-radius-upleft"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-upleft"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M656 200h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm58 624h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 650h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm696-696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-174 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm174-696H358c-127 0-230 103-230 230v182c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V358c0-87.3 70.7-158 158-158h182c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
topLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-upright"
class="anticon anticon-radius-upright"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-upright"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M368 128h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-2 696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm522-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 128h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm174 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-48-696H484c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h182c87.3 0 158 70.7 158 158v182c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V358c0-127-103-230-230-230z"
/>
</svg>
</span>
</span>
<span>
topRight
</span>
</button>
</div>
</div>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-bottomleft"
class="anticon anticon-radius-bottomleft"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-bottomleft"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M712 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm2-696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM136 374h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-174h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm752 624h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-230 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm230 624H358c-87.3 0-158-70.7-158-158V484c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v182c0 127 103 230 230 230h182c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
bottomLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-bottomright"
class="anticon anticon-radius-bottomright"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-bottomright"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M368 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-58-624h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm578 102h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm292 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm174 0h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm230 276h-56c-4.4 0-8 3.6-8 8v182c0 87.3-70.7 158-158 158H484c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h182c127 0 230-103 230-230V484c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
bottomRight
</span>
</button>
</div>
</div>,
]
`;
exports[`renders components/notification/demo/hooks.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/placement.tsx extend context correctly 1`] = `
Array [
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="border-top"
class="anticon anticon-border-top"
role="img"
>
<svg
aria-hidden="true"
data-icon="border-top"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M872 144H152c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h720c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM208 310h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 498h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 166h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm166-166h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm166 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm332 332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm332-498h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm332 332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
top
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="border-bottom"
class="anticon anticon-border-bottom"
role="img"
>
<svg
aria-hidden="true"
data-icon="border-bottom"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M872 808H152c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h720c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-720-94h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-498h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0 332h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-166h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm166 166h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-332h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm332 0h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0 332h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm222-72h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-388 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm388-404h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-388 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm388 426h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-388 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm388-404h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-388 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8z"
/>
</svg>
</span>
</span>
<span>
bottom
</span>
</button>
</div>
</div>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-upleft"
class="anticon anticon-radius-upleft"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-upleft"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M656 200h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm58 624h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 650h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm696-696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-174 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm174-696H358c-127 0-230 103-230 230v182c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V358c0-87.3 70.7-158 158-158h182c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
topLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-upright"
class="anticon anticon-radius-upright"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-upright"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M368 128h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-2 696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm522-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 128h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm174 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-48-696H484c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h182c87.3 0 158 70.7 158 158v182c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V358c0-127-103-230-230-230z"
/>
</svg>
</span>
</span>
<span>
topRight
</span>
</button>
</div>
</div>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-bottomleft"
class="anticon anticon-radius-bottomleft"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-bottomleft"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M712 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm2-696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM136 374h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-174h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm752 624h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-230 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm230 624H358c-87.3 0-158-70.7-158-158V484c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v182c0 127 103 230 230 230h182c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
bottomLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-bottomright"
class="anticon anticon-radius-bottomright"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-bottomright"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M368 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-58-624h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm578 102h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm292 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm174 0h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm230 276h-56c-4.4 0-8 3.6-8 8v182c0 87.3-70.7 158-158 158H484c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h182c127 0 230-103 230-230V484c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
bottomRight
</span>
</button>
</div>
</div>,
]
`;
exports[`renders components/notification/demo/placement.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/render-panel.tsx extend context correctly 1`] = `
<div
class="ant-notification-notice-pure-panel"
>
<div
class="ant-notification-notice ant-notification-notice-closable"
>
<div
class="ant-notification-notice-content"
>
<div
class="ant-notification-notice-with-icon"
role="alert"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle ant-notification-notice-icon ant-notification-notice-icon-success"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
<div
class="ant-notification-notice-message"
>
Hello World!
</div>
<div
class="ant-notification-notice-description"
>
Hello World?
</div>
<div
class="ant-notification-notice-btn"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm"
type="button"
>
<span>
My Button
</span>
</button>
</div>
</div>
</div>
<a
class="ant-notification-notice-close"
tabindex="0"
>
<span
class="ant-notification-close-x"
>
<span
aria-label="close"
class="anticon anticon-close ant-notification-close-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</span>
</a>
</div>
</div>
`;
exports[`renders components/notification/demo/render-panel.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/stack.tsx extend context correctly 1`] = `
<div>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-large ant-space-gap-col-large"
>
<div
class="ant-space-item"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
style="width: 100%;"
>
<div
class="ant-space-item"
>
<span>
Enabled:
</span>
</div>
<div
class="ant-space-item"
>
<button
aria-checked="true"
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
<span
class="ant-switch-inner-checked"
/>
<span
class="ant-switch-inner-unchecked"
/>
</span>
</button>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
style="width: 100%;"
>
<div
class="ant-space-item"
>
<span>
Threshold:
</span>
</div>
<div
class="ant-space-item"
>
<div
class="ant-input-number"
>
<div
class="ant-input-number-handler-wrap"
>
<span
aria-disabled="false"
aria-label="Increase Value"
class="ant-input-number-handler ant-input-number-handler-up"
role="button"
unselectable="on"
>
<span
aria-label="up"
class="anticon anticon-up ant-input-number-handler-up-inner"
role="img"
>
<svg
aria-hidden="true"
data-icon="up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
/>
</svg>
</span>
</span>
<span
aria-disabled="false"
aria-label="Decrease Value"
class="ant-input-number-handler ant-input-number-handler-down"
role="button"
unselectable="on"
>
<span
aria-label="down"
class="anticon anticon-down ant-input-number-handler-down-inner"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-input-number-input-wrap"
>
<input
aria-valuemax="10"
aria-valuemin="1"
aria-valuenow="3"
autocomplete="off"
class="ant-input-number-input"
role="spinbutton"
step="1"
value="3"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
</div>
`;
exports[`renders components/notification/demo/stack.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/update.tsx extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/update.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/with-btn.tsx extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/with-btn.tsx extend context correctly 2`] = `[]`;
exports[`renders components/notification/demo/with-icon.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Success
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Info
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Warning
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Error
</span>
</button>
</div>
</div>
`;
exports[`renders components/notification/demo/with-icon.tsx extend context correctly 2`] = `[]`;

View File

@ -0,0 +1,761 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders components/notification/demo/basic.tsx correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/custom-icon.tsx correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/custom-style.tsx correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/duration.tsx correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/hooks.tsx correctly 1`] = `
Array [
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-upleft"
class="anticon anticon-radius-upleft"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-upleft"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M656 200h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm58 624h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 650h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm696-696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-174 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm174-696H358c-127 0-230 103-230 230v182c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V358c0-87.3 70.7-158 158-158h182c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
topLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-upright"
class="anticon anticon-radius-upright"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-upright"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M368 128h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-2 696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm522-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 128h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm174 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-48-696H484c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h182c87.3 0 158 70.7 158 158v182c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V358c0-127-103-230-230-230z"
/>
</svg>
</span>
</span>
<span>
topRight
</span>
</button>
</div>
</div>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-bottomleft"
class="anticon anticon-radius-bottomleft"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-bottomleft"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M712 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm2-696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM136 374h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-174h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm752 624h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-230 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm230 624H358c-87.3 0-158-70.7-158-158V484c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v182c0 127 103 230 230 230h182c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
bottomLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-bottomright"
class="anticon anticon-radius-bottomright"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-bottomright"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M368 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-58-624h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm578 102h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm292 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm174 0h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm230 276h-56c-4.4 0-8 3.6-8 8v182c0 87.3-70.7 158-158 158H484c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h182c127 0 230-103 230-230V484c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
bottomRight
</span>
</button>
</div>
</div>,
]
`;
exports[`renders components/notification/demo/placement.tsx correctly 1`] = `
Array [
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="border-top"
class="anticon anticon-border-top"
role="img"
>
<svg
aria-hidden="true"
data-icon="border-top"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M872 144H152c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h720c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM208 310h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 498h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 166h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm166-166h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm166 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm332 332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm332-498h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm332 332h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-332 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
top
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="border-bottom"
class="anticon anticon-border-bottom"
role="img"
>
<svg
aria-hidden="true"
data-icon="border-bottom"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M872 808H152c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h720c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-720-94h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-498h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0 332h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-166h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm166 166h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-332h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm332 0h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0 332h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm222-72h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-388 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm388-404h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-388 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm388 426h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-388 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm388-404h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-388 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8z"
/>
</svg>
</span>
</span>
<span>
bottom
</span>
</button>
</div>
</div>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-upleft"
class="anticon anticon-radius-upleft"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-upleft"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M656 200h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm58 624h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 650h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm696-696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-174 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm174-696H358c-127 0-230 103-230 230v182c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V358c0-87.3 70.7-158 158-158h182c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
topLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-upright"
class="anticon anticon-radius-upright"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-upright"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M368 128h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-2 696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm522-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 128h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm174 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-48-696H484c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h182c87.3 0 158 70.7 158 158v182c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V358c0-127-103-230-230-230z"
/>
</svg>
</span>
</span>
<span>
topRight
</span>
</button>
</div>
</div>,
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>,
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-bottomleft"
class="anticon anticon-radius-bottomleft"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-bottomleft"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M712 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm2-696h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM136 374h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0-174h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm752 624h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-348 0h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-230 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm230 624H358c-87.3 0-158-70.7-158-158V484c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v182c0 127 103 230 230 230h182c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
bottomLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="radius-bottomright"
class="anticon anticon-radius-bottomright"
role="img"
>
<svg
aria-hidden="true"
data-icon="radius-bottomright"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M368 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-58-624h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm578 102h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM192 824h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-174h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm292 72h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm174 0h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm230 276h-56c-4.4 0-8 3.6-8 8v182c0 87.3-70.7 158-158 158H484c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h182c127 0 230-103 230-230V484c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
<span>
bottomRight
</span>
</button>
</div>
</div>,
]
`;
exports[`renders components/notification/demo/render-panel.tsx correctly 1`] = `
<div
class="ant-notification-notice-pure-panel"
>
<div
class="ant-notification-notice ant-notification-notice-closable"
>
<div
class="ant-notification-notice-content"
>
<div
class="ant-notification-notice-with-icon"
role="alert"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle ant-notification-notice-icon ant-notification-notice-icon-success"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
<div
class="ant-notification-notice-message"
>
Hello World!
</div>
<div
class="ant-notification-notice-description"
>
Hello World?
</div>
<div
class="ant-notification-notice-btn"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm"
type="button"
>
<span>
My Button
</span>
</button>
</div>
</div>
</div>
<a
class="ant-notification-notice-close"
tabindex="0"
>
<span
class="ant-notification-close-x"
>
<span
aria-label="close"
class="anticon anticon-close ant-notification-close-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</span>
</a>
</div>
</div>
`;
exports[`renders components/notification/demo/stack.tsx correctly 1`] = `
<div>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-large ant-space-gap-col-large"
>
<div
class="ant-space-item"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
style="width:100%"
>
<div
class="ant-space-item"
>
<span>
Enabled:
</span>
</div>
<div
class="ant-space-item"
>
<button
aria-checked="true"
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
<span
class="ant-switch-inner-checked"
/>
<span
class="ant-switch-inner-unchecked"
/>
</span>
</button>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
style="width:100%"
>
<div
class="ant-space-item"
>
<span>
Threshold:
</span>
</div>
<div
class="ant-space-item"
>
<div
class="ant-input-number"
>
<div
class="ant-input-number-handler-wrap"
>
<span
aria-disabled="false"
aria-label="Increase Value"
class="ant-input-number-handler ant-input-number-handler-up"
role="button"
unselectable="on"
>
<span
aria-label="up"
class="anticon anticon-up ant-input-number-handler-up-inner"
role="img"
>
<svg
aria-hidden="true"
data-icon="up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
/>
</svg>
</span>
</span>
<span
aria-disabled="false"
aria-label="Decrease Value"
class="ant-input-number-handler ant-input-number-handler-down"
role="button"
unselectable="on"
>
<span
aria-label="down"
class="anticon anticon-down ant-input-number-handler-down-inner"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-input-number-input-wrap"
>
<input
aria-valuemax="10"
aria-valuemin="1"
aria-valuenow="3"
autocomplete="off"
class="ant-input-number-input"
role="spinbutton"
step="1"
value="3"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-divider ant-divider-horizontal"
role="separator"
/>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
</div>
`;
exports[`renders components/notification/demo/update.tsx correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/with-btn.tsx correctly 1`] = `
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open the notification box
</span>
</button>
`;
exports[`renders components/notification/demo/with-icon.tsx correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Success
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Info
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Warning
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Error
</span>
</button>
</div>
</div>
`;

View 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: undefined,
getContainer: undefined,
});
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);
});
});

View File

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('notification');

View File

@ -0,0 +1,6 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('notification', {
testRootProps: false,
nameCheckPathOnly: true,
});

View File

@ -0,0 +1,192 @@
import React from 'react';
import { StyleProvider, createCache, extractStyle } from '@ant-design/cssinjs';
import notification from '..';
import { fireEvent, pureRender, render } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
describe('notification.hooks', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should work', () => {
const Context = React.createContext('light');
const Demo: React.FC = () => {
const [api, holder] = notification.useNotification();
return (
<ConfigProvider prefixCls="my-test">
<Context.Provider value="bamboo">
<button
type="button"
onClick={() => {
api.open({
message: null,
description: (
<Context.Consumer>
{(name) => <span className="hook-test-result">{name}</span>}
</Context.Consumer>
),
duration: 0,
});
}}
>
test
</button>
{holder}
</Context.Provider>
</ConfigProvider>
);
};
const { container } = render(<Demo />);
fireEvent.click(container.querySelector('button')!);
expect(document.querySelectorAll('.my-test-notification-notice')).toHaveLength(1);
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
});
it('should work with success', () => {
const Context = React.createContext('light');
const Demo: React.FC = () => {
const [api, holder] = notification.useNotification();
return (
<ConfigProvider prefixCls="my-test">
<Context.Provider value="bamboo">
<button
type="button"
onClick={() => {
api.success({
message: null,
description: (
<Context.Consumer>
{(name) => <span className="hook-test-result">{name}</span>}
</Context.Consumer>
),
duration: 0,
});
}}
>
test
</button>
{holder}
</Context.Provider>
</ConfigProvider>
);
};
const { container } = render(<Demo />);
fireEvent.click(container.querySelector('button')!);
expect(document.querySelectorAll('.my-test-notification-notice')).toHaveLength(1);
expect(document.querySelectorAll('.anticon-check-circle')).toHaveLength(1);
expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
});
it('should be same hook', () => {
let count = 0;
const Demo: React.FC = () => {
const [, forceUpdate] = React.useState([]);
const [api] = notification.useNotification();
React.useEffect(() => {
count += 1;
expect(count).toEqual(1);
forceUpdate([]);
}, [api]);
return null;
};
pureRender(<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();
});
});
it('not export style in SSR', () => {
const cache = createCache();
const Demo = () => {
const [, holder] = notification.useNotification();
return <StyleProvider cache={cache}>{holder}</StyleProvider>;
};
render(<Demo />);
const styleText = extractStyle(cache, true);
expect(styleText).not.toContain('.ant-notification');
});
it('disable stack', () => {
const Demo = () => {
const [api, holder] = notification.useNotification({ stack: false });
React.useEffect(() => {
api.info({
message: null,
description: 'test',
});
}, []);
return holder;
};
render(<Demo />);
expect(document.querySelector('.ant-notification-stack')).toBeFalsy();
});
});

View File

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Notification image', () => {
imageDemoTest('notification');
});

View File

@ -0,0 +1,370 @@
import { UserOutlined } from '@ant-design/icons';
import React from 'react';
import notification, { actWrapper } from '..';
import { act, fireEvent } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import { awaitPromise, triggerMotionEnd } from './util';
describe('notification', () => {
beforeAll(() => {
actWrapper(act);
});
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(async () => {
// Clean up
notification.destroy();
await triggerMotionEnd();
notification.config({
prefixCls: undefined,
getContainer: undefined,
});
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: undefined,
});
});
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();
});
it('support props', () => {
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
props: { 'data-testid': 'test-notification' },
});
});
expect(document.querySelectorAll("[data-testid='test-notification']").length).toBe(1);
});
it('support role', async () => {
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
role: 'status',
});
});
expect(document.querySelectorAll('[role="status"]').length).toBe(1);
});
it('should hide close btn when closeIcon setting to null or false', async () => {
notification.config({
closeIcon: undefined,
});
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
className: 'normal',
});
notification.open({
message: 'Notification Title',
duration: 0,
className: 'custom',
closeIcon: <span className="custom-close-icon">Close</span>,
});
notification.open({
message: 'Notification Title',
duration: 0,
closeIcon: null,
className: 'with-null',
});
notification.open({
message: 'Notification Title',
duration: 0,
closeIcon: false,
className: 'with-false',
});
});
await awaitPromise();
expect(document.querySelectorAll('.normal .ant-notification-notice-close').length).toBe(1);
expect(document.querySelectorAll('.custom .custom-close-icon').length).toBe(1);
expect(document.querySelectorAll('.with-null .ant-notification-notice-close').length).toBe(0);
expect(document.querySelectorAll('.with-false .ant-notification-notice-close').length).toBe(0);
});
it('style.width could be overrided', async () => {
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
style: {
width: 600,
},
className: 'with-style',
});
});
await awaitPromise();
expect(document.querySelector('.with-style')).toHaveStyle({ width: '600px' });
expect(
document.querySelector('.ant-notification-notice-wrapper:has(.width-style)'),
).toHaveStyle({ width: '' });
});
});

View 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: undefined,
getContainer: undefined,
});
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-wrapper').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();
});
});
});

View File

@ -0,0 +1,57 @@
import React from 'react';
import notification, { actWrapper } from '..';
import { act, render, waitFakeTimer } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import { awaitPromise, triggerMotionEnd } from './util';
describe('notification static warning', () => {
beforeAll(() => {
actWrapper(act);
});
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(async () => {
// Clean up
notification.destroy();
await triggerMotionEnd();
jest.useRealTimers();
await awaitPromise();
});
// Follow test need keep order
it('no warning', async () => {
const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
notification.open({
message: <div className="bamboo" />,
duration: 0,
});
await waitFakeTimer();
expect(document.querySelector('.bamboo')).toBeTruthy();
expect(errSpy).not.toHaveBeenCalled();
});
it('warning if use theme', async () => {
const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<ConfigProvider theme={{}} />);
notification.open({
message: <div className="light" />,
duration: 0,
});
await waitFakeTimer();
expect(document.querySelector('.light')).toBeTruthy();
expect(errSpy).toHaveBeenCalledWith(
"Warning: [antd: notification] Static function can not consume context like dynamic theme. Please use 'App' component instead.",
);
});
});

View File

@ -0,0 +1,31 @@
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) => {
// close > notice > notice-wrapper
fireEvent.animationEnd(ele.parentNode?.parentNode?.parentNode!);
});
await awaitPromise();
}

View File

@ -0,0 +1,7 @@
## zh-CN
静态方法无法消费 Context推荐优先使用 Hooks 版本。
## en-US
Static methods cannot consume Context. Please use hooks first.

View File

@ -0,0 +1,20 @@
import React from 'react';
import { Button, notification } from 'antd';
const openNotification = () => {
notification.open({
message: 'Notification Title',
description:
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
onClick: () => {
console.log('Notification Clicked!');
},
});
};
const App: React.FC = () => (
<Button type="primary" onClick={openNotification}>
Open the notification box
</Button>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
图标可以被自定义。
## en-US
The icon can be customized to any react node.

View File

@ -0,0 +1,27 @@
import React from 'react';
import { SmileOutlined } from '@ant-design/icons';
import { Button, notification } from 'antd';
const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();
const openNotification = () => {
api.open({
message: 'Notification Title',
description:
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
icon: <SmileOutlined style={{ color: '#108ee9' }} />,
});
};
return (
<>
{contextHolder}
<Button type="primary" onClick={openNotification}>
Open the notification box
</Button>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
使用 style 和 className 来定义样式。
## en-US
The style and className are available to customize Notification.

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Button, notification } from 'antd';
const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();
const openNotification = () => {
api.open({
message: 'Notification Title',
description:
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
className: 'custom-class',
style: {
width: 600,
},
});
};
return (
<>
{contextHolder}
<Button type="primary" onClick={openNotification}>
Open the notification box
</Button>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
自定义通知框自动关闭的延时,默认 `4.5s`,取消自动关闭只要将该值设为 `0` 即可。
## en-US
`Duration` can be used to specify how long the notification stays open. After the duration time elapses, the notification closes automatically. If not specified, default value is 4.5 seconds. If you set the value to 0, the notification box will never close automatically.

View File

@ -0,0 +1,26 @@
import React from 'react';
import { Button, notification } from 'antd';
const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();
const openNotification = () => {
api.open({
message: 'Notification Title',
description:
'I will never close automatically. This is a purposely very very long description that has many many characters and words.',
duration: 0,
});
};
return (
<>
{contextHolder}
<Button type="primary" onClick={openNotification}>
Open the notification box
</Button>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
通过 `notification.useNotification` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
## en-US
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.

View File

@ -0,0 +1,66 @@
import {
RadiusBottomleftOutlined,
RadiusBottomrightOutlined,
RadiusUpleftOutlined,
RadiusUprightOutlined,
} from '@ant-design/icons';
import React, { useMemo } from 'react';
import { Button, Divider, Space, notification } from 'antd';
import type { NotificationPlacement } from 'antd/es/notification/interface';
const Context = React.createContext({ name: 'Default' });
const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();
const openNotification = (placement: NotificationPlacement) => {
api.info({
message: `Notification ${placement}`,
description: <Context.Consumer>{({ name }) => `Hello, ${name}!`}</Context.Consumer>,
placement,
});
};
const contextValue = useMemo(() => ({ name: 'Ant Design' }), []);
return (
<Context.Provider value={contextValue}>
{contextHolder}
<Space>
<Button
type="primary"
onClick={() => openNotification('topLeft')}
icon={<RadiusUpleftOutlined />}
>
topLeft
</Button>
<Button
type="primary"
onClick={() => openNotification('topRight')}
icon={<RadiusUprightOutlined />}
>
topRight
</Button>
</Space>
<Divider />
<Space>
<Button
type="primary"
onClick={() => openNotification('bottomLeft')}
icon={<RadiusBottomleftOutlined />}
>
bottomLeft
</Button>
<Button
type="primary"
onClick={() => openNotification('bottomRight')}
icon={<RadiusBottomrightOutlined />}
>
bottomRight
</Button>
</Space>
</Context.Provider>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
使用 `placement` 可以配置通知从右上角、右下角、左下角、左上角弹出。
## en-US
A notification box can appear from the `topRight`, `bottomRight`, `bottomLeft` or `topLeft` of the viewport via `placement`.

View File

@ -0,0 +1,78 @@
import React from 'react';
import {
BorderBottomOutlined,
BorderTopOutlined,
RadiusBottomleftOutlined,
RadiusBottomrightOutlined,
RadiusUpleftOutlined,
RadiusUprightOutlined,
} from '@ant-design/icons';
import { Button, Divider, notification, Space } from 'antd';
import type { NotificationPlacement } from 'antd/es/notification/interface';
const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();
const openNotification = (placement: NotificationPlacement) => {
api.info({
message: `Notification ${placement}`,
description:
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
placement,
});
};
return (
<>
{contextHolder}
<Space>
<Button type="primary" onClick={() => openNotification('top')} icon={<BorderTopOutlined />}>
top
</Button>
<Button
type="primary"
onClick={() => openNotification('bottom')}
icon={<BorderBottomOutlined />}
>
bottom
</Button>
</Space>
<Divider />
<Space>
<Button
type="primary"
onClick={() => openNotification('topLeft')}
icon={<RadiusUpleftOutlined />}
>
topLeft
</Button>
<Button
type="primary"
onClick={() => openNotification('topRight')}
icon={<RadiusUprightOutlined />}
>
topRight
</Button>
</Space>
<Divider />
<Space>
<Button
type="primary"
onClick={() => openNotification('bottomLeft')}
icon={<RadiusBottomleftOutlined />}
>
bottomLeft
</Button>
<Button
type="primary"
onClick={() => openNotification('bottomRight')}
icon={<RadiusBottomrightOutlined />}
>
bottomRight
</Button>
</Space>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
调试用组件,请勿直接使用。
## en-US
Debug usage. Do not use in your production.

View File

@ -0,0 +1,18 @@
import React from 'react';
import { Button, notification } from 'antd';
/** Test usage. Do not use in your production. */
const { _InternalPanelDoNotUseOrYouWillBeFired: InternalPanel } = notification;
export default () => (
<InternalPanel
message="Hello World!"
description="Hello World?"
type="success"
btn={
<Button type="primary" size="small">
My Button
</Button>
}
/>
);

View File

@ -0,0 +1,7 @@
## zh-CN
堆叠配置,默认开启。超过 3 个以上的消息会被自动收起,可以通过 `threshold` 来设置不会被收起的最大数量。
## en-US
Stack configuration, enabled by default. More than 3 notifications will be automatically stacked, and could be changed by `threshold`.

View File

@ -0,0 +1,59 @@
import React, { useMemo } from 'react';
import { Button, Divider, InputNumber, notification, Space, Switch } from 'antd';
const Context = React.createContext({ name: 'Default' });
const App: React.FC = () => {
const [enabled, setEnabled] = React.useState(true);
const [threshold, setThreshold] = React.useState(3);
const [api, contextHolder] = notification.useNotification({
stack: enabled
? {
threshold,
}
: false,
});
const openNotification = () => {
api.open({
message: 'Notification Title',
description: `${Array(Math.round(Math.random() * 5) + 1)
.fill('This is the content of the notification.')
.join('\n')}`,
duration: null,
});
};
const contextValue = useMemo(() => ({ name: 'Ant Design' }), []);
return (
<Context.Provider value={contextValue}>
{contextHolder}
<div>
<Space size="large">
<Space style={{ width: '100%' }}>
<span>Enabled: </span>
<Switch checked={enabled} onChange={(v) => setEnabled(v)} />
</Space>
<Space style={{ width: '100%' }}>
<span>Threshold: </span>
<InputNumber
disabled={!enabled}
value={threshold}
step={1}
min={1}
max={10}
onChange={(v) => setThreshold(v || 0)}
/>
</Space>
</Space>
<Divider />
<Button type="primary" onClick={openNotification}>
Open the notification box
</Button>
</div>
</Context.Provider>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
可以通过唯一的 key 来更新内容。
## en-US
Update content with unique key.

View File

@ -0,0 +1,34 @@
import React from 'react';
import { Button, notification } from 'antd';
const key = 'updatable';
const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();
const openNotification = () => {
api.open({
key,
message: 'Notification Title',
description: 'description.',
});
setTimeout(() => {
api.open({
key,
message: 'New Title',
description: 'New description.',
});
}, 1000);
};
return (
<>
{contextHolder}
<Button type="primary" onClick={openNotification}>
Open the notification box
</Button>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
自定义关闭按钮的样式和文字。
## en-US
To customize the style or font of the close button.

View File

@ -0,0 +1,45 @@
import React from 'react';
import { Button, notification, Space } from 'antd';
const close = () => {
console.log(
'Notification was closed. Either the close button was clicked or duration time elapsed.',
);
};
const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();
const openNotification = () => {
const key = `open${Date.now()}`;
const btn = (
<Space>
<Button type="link" size="small" onClick={() => api.destroy()}>
Destroy All
</Button>
<Button type="primary" size="small" onClick={() => api.destroy(key)}>
Confirm
</Button>
</Space>
);
api.open({
message: 'Notification Title',
description:
'A function will be be called after the notification is closed (automatically after the "duration" time of manually).',
btn,
key,
onClose: close,
});
};
return (
<>
{contextHolder}
<Button type="primary" onClick={openNotification}>
Open the notification box
</Button>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
通知提醒框左侧有图标。
## en-US
A notification box with a icon at the left side.

View File

@ -0,0 +1,30 @@
import React from 'react';
import { Button, notification, Space } from 'antd';
type NotificationType = 'success' | 'info' | 'warning' | 'error';
const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();
const openNotificationWithIcon = (type: NotificationType) => {
api[type]({
message: 'Notification Title',
description:
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
});
};
return (
<>
{contextHolder}
<Space>
<Button onClick={() => openNotificationWithIcon('success')}>Success</Button>
<Button onClick={() => openNotificationWithIcon('info')}>Info</Button>
<Button onClick={() => openNotificationWithIcon('warning')}>Warning</Button>
<Button onClick={() => openNotificationWithIcon('error')}>Error</Button>
</Space>
</>
);
};
export default App;

View File

@ -0,0 +1,253 @@
import * as React from 'react';
import { render } from 'rc-util/lib/React/render';
import ConfigProvider, { globalConfig, warnContext } from '../config-provider';
import type { ArgsProps, GlobalConfigProps, NotificationInstance } from './interface';
import PurePanel from './PurePanel';
import useNotification, { useInternalNotification } from './useNotification';
export type { ArgsProps };
let notification: GlobalNotification | null = null;
let act: (callback: VoidFunction) => Promise<void> | void = (callback: VoidFunction) => callback();
interface GlobalNotification {
fragment: DocumentFragment;
instance?: NotificationInstance | null;
sync?: VoidFunction;
}
type Task =
| {
type: 'open';
config: ArgsProps;
}
| {
type: 'destroy';
key: React.Key;
};
let taskQueue: Task[] = [];
let defaultGlobalConfig: GlobalConfigProps = {};
function getGlobalContext() {
const {
prefixCls: globalPrefixCls,
getContainer: globalGetContainer,
rtl,
maxCount,
top,
bottom,
} = defaultGlobalConfig;
const mergedPrefixCls = globalPrefixCls ?? globalConfig().getPrefixCls('notification');
const mergedContainer = globalGetContainer?.() || document.body;
return {
prefixCls: mergedPrefixCls,
getContainer: () => mergedContainer!,
rtl,
maxCount,
top,
bottom,
};
}
interface GlobalHolderRef {
instance: NotificationInstance;
sync: () => void;
}
const GlobalHolder = React.forwardRef<GlobalHolderRef, {}>((_, ref) => {
const [notificationConfig, setNotificationConfig] =
React.useState<GlobalConfigProps>(getGlobalContext);
const [api, holder] = useInternalNotification(notificationConfig);
const global = globalConfig();
const rootPrefixCls = global.getRootPrefixCls();
const rootIconPrefixCls = global.getIconPrefixCls();
const theme = global.getTheme();
const sync = () => {
setNotificationConfig(getGlobalContext);
};
React.useEffect(sync, []);
React.useImperativeHandle(ref, () => {
const instance: NotificationInstance = { ...api };
// @ts-ignore
Object.keys(instance).forEach((method: keyof NotificationInstance) => {
instance[method] = (...args: any[]) => {
sync();
return (api as any)[method](...args);
};
});
return {
instance,
sync,
};
});
return (
<ConfigProvider prefixCls={rootPrefixCls} iconPrefixCls={rootIconPrefixCls} theme={theme}>
{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;
}
// Notification not ready
if (!notification.instance) {
return;
}
// >>> Execute task
taskQueue.forEach((task) => {
// eslint-disable-next-line default-case
switch (task.type) {
case 'open': {
act(() => {
notification!.instance!.open({
...defaultGlobalConfig,
...task.config,
});
});
break;
}
case 'destroy':
act(() => {
notification?.instance!.destroy(task.key);
});
break;
}
});
// Clean up
taskQueue = [];
}
// ==============================================================================
// == Export ==
// ==============================================================================
function setNotificationGlobalConfig(config: GlobalConfigProps) {
defaultGlobalConfig = {
...defaultGlobalConfig,
...config,
};
// Trigger sync for it
act(() => {
notification?.sync?.();
});
}
function open(config: ArgsProps) {
// Warning if exist theme
if (process.env.NODE_ENV !== 'production') {
warnContext('notification');
}
taskQueue.push({
type: 'open',
config,
});
flushNotice();
}
function destroy(key: React.Key) {
taskQueue.push({
type: 'destroy',
key,
});
flushNotice();
}
interface BaseMethods {
open: (config: ArgsProps) => void;
destroy: (key?: React.Key) => void;
config: (config: GlobalConfigProps) => void;
useNotification: typeof useNotification;
/** @private Internal Component. Do not use in your production. */
_InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel;
}
type StaticFn = (config: ArgsProps) => void;
interface NoticeMethods {
success: StaticFn;
info: StaticFn;
warning: StaticFn;
error: StaticFn;
}
const methods: (keyof NoticeMethods)[] = ['success', 'info', 'warning', 'error'];
const baseStaticMethods: BaseMethods = {
open,
// @ts-ignore
destroy,
config: setNotificationGlobalConfig,
useNotification,
_InternalPanelDoNotUseOrYouWillBeFired: PurePanel,
};
const staticMethods = baseStaticMethods as NoticeMethods & BaseMethods;
methods.forEach((type: keyof NoticeMethods) => {
staticMethods[type] = (config) => open({ ...config, type });
});
// ==============================================================================
// == Test ==
// ==============================================================================
const noop = () => {};
/** @internal Only Work in test env */
// eslint-disable-next-line import/no-mutable-exports
export let actWrapper: (wrapper: any) => void = noop;
if (process.env.NODE_ENV === 'test') {
actWrapper = (wrapper) => {
act = wrapper;
};
}
export default staticMethods;

View File

@ -0,0 +1,142 @@
---
category: Components
group: 反馈
noinstant: true
title: Notification
subtitle: 通知提醒框
demo:
cols: 2
---
全局展示通知提醒信息。
## 何时使用
在系统四个角显示通知提醒信息。经常用于以下情况:
- 较为复杂的通知内容。
- 带有交互的通知,给出用户下一步的行动点。
- 系统主动推送。
## 代码演示
<!-- prettier-ignore -->
<code src="./demo/hooks.tsx">Hooks 调用(推荐)</code>
<code src="./demo/duration.tsx">自动关闭的延时</code>
<code src="./demo/with-icon.tsx">带有图标的通知提醒框</code>
<code src="./demo/with-btn.tsx">自定义按钮</code>
<code src="./demo/custom-icon.tsx">自定义图标</code>
<code src="./demo/placement.tsx">位置</code>
<code src="./demo/custom-style.tsx">自定义样式</code>
<code src="./demo/update.tsx">更新消息内容</code>
<code src="./demo/stack.tsx" version="5.10.0">堆叠</code>
<code src="./demo/basic.tsx">静态方法(不推荐)</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
## API
通用属性参考:[通用属性](/docs/react/common-props)
- `notification.success(config)`
- `notification.error(config)`
- `notification.info(config)`
- `notification.warning(config)`
- `notification.open(config)`
- `notification.destroy(key?: String)`
config 参数如下:
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| btn | 自定义关闭按钮 | ReactNode | - | - |
| className | 自定义 CSS class | string | - | - |
| closeIcon | 自定义关闭图标 | boolean \| ReactNode | true | 5.7.0:设置为 null 或 false 时隐藏关闭按钮 |
| description | 通知提醒内容,必选 | ReactNode | - | - |
| duration | 默认 4.5 秒后自动关闭,配置为 null 则不自动关闭 | number | 4.5 | - |
| icon | 自定义图标 | ReactNode | - | - |
| key | 当前通知唯一标志 | string | - | - |
| message | 通知提醒标题,必选 | ReactNode | - | - |
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | - |
| style | 自定义内联样式 | [CSSProperties](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e434515761b36830c3e58a970abf5186f005adac/types/react/index.d.ts#L794) | - | - |
| role | 供屏幕阅读器识别的通知内容语义,默认为 `alert`。此情况下屏幕阅读器会立即打断当前正在阅读的其他内容,转而阅读通知内容 | `alert \| status` | `alert` | 5.6.0 |
| onClick | 点击通知时触发的回调函数 | function | - | - |
| onClose | 当通知关闭时触发 | function | - | - |
| props | 透传至通知 `div` 上的 props 对象,支持传入 `data-*` `aria-*``role` 作为对象的属性。需要注意的是,虽然在 TypeScript 类型中声明的类型支持传入 `data-*` 作为对象的属性,但目前只允许传入 `data-testid` 作为对象的属性。 详见 https://github.com/microsoft/TypeScript/issues/28960 | Object | - | - |
- `notification.useNotification(config)`
config 参数如下:
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| bottom | 消息从底部弹出时,距离底部的位置,单位像素 | number | 24 | |
| closeIcon | 自定义关闭图标 | boolean \| ReactNode | true | 5.7.0:设置为 null 或 false 时隐藏关闭按钮 |
| getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | |
| placement | 弹出位置,可选 `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | string | `topRight` | |
| rtl | 是否开启 RTL 模式 | boolean | false | |
| stack | 堆叠模式,超过阈值时会将所有消息收起 | boolean \| `{ threshold: number }` | `{ threshold: 3 }` | 5.10.0 |
| top | 消息从顶部弹出时,距离顶部的位置,单位像素 | number | 24 | |
| maxCount | 最大显示数,超过限制时,最早的消息会被自动关闭 | number | - | 4.17.0 |
### 全局配置
还提供了一个全局配置方法,在调用前提前配置,全局一次生效。
`notification.config(options)`
> 当你使用 `ConfigProvider` 进行全局化配置时,系统会默认自动开启 RTL 模式。(4.3.0+)
>
> 当你想单独使用,可通过如下设置开启 RTL 模式。
```js
notification.config({
placement: 'bottomRight',
bottom: 50,
duration: 3,
rtl: true,
});
```
#### notification.config
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| bottom | 消息从底部弹出时,距离底部的位置,单位像素 | number | 24 | |
| closeIcon | 自定义关闭图标 | boolean \| ReactNode | true | 5.7.0:设置为 null 或 false 时隐藏关闭按钮 |
| duration | 默认自动关闭延时,单位秒 | number | 4.5 | |
| getContainer | 配置渲染节点的输出位置,但依旧为全屏展示 | () => HTMLNode | () => document.body | |
| placement | 弹出位置,可选 `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | string | `topRight` | |
| rtl | 是否开启 RTL 模式 | boolean | false | |
| top | 消息从顶部弹出时,距离顶部的位置,单位像素 | number | 24 | |
| maxCount | 最大显示数,超过限制时,最早的消息会被自动关闭 | number | - | 4.17.0 |
## FAQ
### 为什么 notification 不能获取 context、redux 的内容和 ConfigProvider 的 `locale/prefixCls/theme` 等配置?
直接调用 notification 方法antd 会通过 `ReactDOM.render` 动态创建新的 React 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `notification.useNotification` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
```ts
const [api, contextHolder] = notification.useNotification();
return (
<Context1.Provider value="Ant">
{/* contextHolder 在 Context1 内,它可以获得 Context1 的 context */}
{contextHolder}
<Context2.Provider value="Design">
{/* contextHolder 在 Context2 外,因而不会获得 Context2 的 context */}
</Context2.Provider>
</Context1.Provider>
);
```
**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
> 可通过 [App 包裹组件](/components/app-cn) 简化 `useNotification` 等方法需要手动植入 contextHolder 的问题。
### 静态方法如何设置 prefixCls
你可以通过 [`ConfigProvider.config`](/components/config-provider-cn#configproviderconfig-4130) 进行设置。

View File

@ -0,0 +1,70 @@
import type * as React from 'react';
interface DivProps extends React.HTMLProps<HTMLDivElement> {
'data-testid'?: string;
}
export const NotificationPlacements = [
'top',
'topLeft',
'topRight',
'bottom',
'bottomLeft',
'bottomRight',
] as const;
export type NotificationPlacement = typeof NotificationPlacements[number];
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?: boolean | React.ReactNode;
props?: DivProps;
role?: 'alert' | 'status';
}
type StaticFn = (args: ArgsProps) => void;
export interface NotificationInstance {
success: StaticFn;
error: StaticFn;
info: StaticFn;
warning: StaticFn;
open: StaticFn;
destroy(key?: React.Key): void;
}
export interface GlobalConfigProps {
top?: number;
bottom?: number;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement | ShadowRoot;
placement?: NotificationPlacement;
closeIcon?: React.ReactNode;
rtl?: boolean;
maxCount?: number;
props?: DivProps;
}
export interface NotificationConfig {
top?: number;
bottom?: number;
prefixCls?: string;
getContainer?: () => HTMLElement | ShadowRoot;
placement?: NotificationPlacement;
maxCount?: number;
rtl?: boolean;
stack?: boolean | { threshold?: number };
}

View File

@ -0,0 +1,307 @@
import type { CSSObject } from '@ant-design/cssinjs';
import { Keyframes, unit } from '@ant-design/cssinjs';
import { CONTAINER_MAX_OFFSET } from '../../_util/hooks/useZIndex';
import { resetComponent } from '../../style';
import type { AliasToken, FullToken, GenerateStyle } from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal';
import genNotificationPlacementStyle from './placement';
import genStackStyle from './stack';
import type { GenStyleFn } from '../../theme/util/genComponentStyleHook';
/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
/**
* @desc z-index
* @descEN z-index of Notification
*/
zIndexPopup: number;
/**
* @desc
* @descEN Width of Notification
*/
width: number;
/** @internal */
closeBtnHoverBg: string;
}
// @ts-ignore
export interface NotificationToken extends FullToken<'Notification'> {
animationMaxHeight: number;
notificationBg: string;
notificationPadding: string;
notificationPaddingVertical: number;
notificationPaddingHorizontal: number;
notificationIconSize: number | string;
notificationCloseButtonSize: number | string;
notificationMarginBottom: number;
notificationMarginEdge: number;
notificationStackLayer: number;
}
export const genNoticeStyle = (token: NotificationToken): CSSObject => {
const {
iconCls,
componentCls, // .ant-notification
boxShadow,
fontSizeLG,
notificationMarginBottom,
borderRadiusLG,
colorSuccess,
colorInfo,
colorWarning,
colorError,
colorTextHeading,
notificationBg,
notificationPadding,
notificationMarginEdge,
fontSize,
lineHeight,
// @ts-ignore
width,
notificationIconSize,
colorText,
} = token;
const noticeCls = `${componentCls}-notice`;
return {
position: 'relative',
marginBottom: notificationMarginBottom,
marginInlineStart: 'auto',
background: notificationBg,
borderRadius: borderRadiusLG,
boxShadow,
[noticeCls]: {
padding: notificationPadding,
width,
maxWidth: `calc(100vw - ${unit(token.calc(notificationMarginEdge).mul(2).equal())})`,
overflow: 'hidden',
lineHeight,
wordWrap: 'break-word',
},
[`${componentCls}-close-icon`]: {
fontSize,
cursor: 'pointer',
},
[`${noticeCls}-message`]: {
marginBottom: token.marginXS,
color: colorTextHeading,
fontSize: fontSizeLG,
lineHeight: token.lineHeightLG,
},
[`${noticeCls}-description`]: {
fontSize,
color: colorText,
},
[`${noticeCls}-closable ${noticeCls}-message`]: {
paddingInlineEnd: token.paddingLG,
},
[`${noticeCls}-with-icon ${noticeCls}-message`]: {
marginBottom: token.marginXS,
marginInlineStart: token.calc(token.marginSM).add(notificationIconSize).equal(),
fontSize: fontSizeLG,
},
[`${noticeCls}-with-icon ${noticeCls}-description`]: {
marginInlineStart: token.calc(token.marginSM).add(notificationIconSize).equal(),
fontSize,
},
// Icon & color style in different selector level
// https://github.com/ant-design/ant-design/issues/16503
// https://github.com/ant-design/ant-design/issues/15512
[`${noticeCls}-icon`]: {
position: 'absolute',
fontSize: notificationIconSize,
lineHeight: 1,
// icon-font
[`&-success${iconCls}`]: {
color: colorSuccess,
},
[`&-info${iconCls}`]: {
color: colorInfo,
},
[`&-warning${iconCls}`]: {
color: colorWarning,
},
[`&-error${iconCls}`]: {
color: colorError,
},
},
[`${noticeCls}-close`]: {
position: 'absolute',
top: token.notificationPaddingVertical,
insetInlineEnd: token.notificationPaddingHorizontal,
color: token.colorIcon,
outline: 'none',
width: token.notificationCloseButtonSize,
height: token.notificationCloseButtonSize,
borderRadius: token.borderRadiusSM,
transition: `background-color ${token.motionDurationMid}, color ${token.motionDurationMid}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&:hover': {
color: token.colorIconHover,
// @ts-ignore
backgroundColor: token.closeBtnHoverBg,
},
},
[`${noticeCls}-btn`]: {
float: 'right',
marginTop: token.marginSM,
},
};
};
const genNotificationStyle: GenerateStyle<NotificationToken> = (token) => {
const {
componentCls, // .ant-notification
notificationMarginBottom,
notificationMarginEdge,
motionDurationMid,
motionEaseInOut,
} = token;
const noticeCls = `${componentCls}-notice`;
const fadeOut = new Keyframes('antNotificationFadeOut', {
'0%': {
maxHeight: token.animationMaxHeight,
marginBottom: notificationMarginBottom,
},
'100%': {
maxHeight: 0,
marginBottom: 0,
paddingTop: 0,
paddingBottom: 0,
opacity: 0,
},
});
return [
// ============================ Holder ============================
{
[componentCls]: {
...resetComponent(token),
position: 'fixed',
// @ts-ignore
zIndex: token.zIndexPopup,
marginRight: {
value: notificationMarginEdge,
_skip_check_: true,
},
[`${componentCls}-hook-holder`]: {
position: 'relative',
},
// animation
[`${componentCls}-fade-appear-prepare`]: {
opacity: '0 !important',
},
[`${componentCls}-fade-enter, ${componentCls}-fade-appear`]: {
animationDuration: token.motionDurationMid,
animationTimingFunction: motionEaseInOut,
animationFillMode: 'both',
opacity: 0,
animationPlayState: 'paused',
},
[`${componentCls}-fade-leave`]: {
animationTimingFunction: motionEaseInOut,
animationFillMode: 'both',
animationDuration: motionDurationMid,
animationPlayState: 'paused',
},
[`${componentCls}-fade-enter${componentCls}-fade-enter-active, ${componentCls}-fade-appear${componentCls}-fade-appear-active`]:
{
animationPlayState: 'running',
},
[`${componentCls}-fade-leave${componentCls}-fade-leave-active`]: {
animationName: fadeOut,
animationPlayState: 'running',
},
// RTL
'&-rtl': {
direction: 'rtl',
[`${noticeCls}-btn`]: {
float: 'left',
},
},
},
},
// ============================ Notice ============================
{
[componentCls]: {
[`${noticeCls}-wrapper`]: {
...genNoticeStyle(token),
},
},
},
];
};
// ============================== Export ==============================
export const prepareComponentToken = (token: AliasToken) => ({
zIndexPopup: token.zIndexPopupBase + CONTAINER_MAX_OFFSET + 50,
width: 384,
closeBtnHoverBg: token.wireframe ? 'transparent' : token.colorFillContent,
});
export const prepareNotificationToken: (
// @ts-ignore
token: Parameters<GenStyleFn<'Notification'>>[0],
) => NotificationToken = (token) => {
const notificationPaddingVertical = token.paddingMD;
const notificationPaddingHorizontal = token.paddingLG;
const notificationToken = mergeToken<NotificationToken>(token, {
notificationBg: token.colorBgElevated,
notificationPaddingVertical,
notificationPaddingHorizontal,
notificationIconSize: token.calc(token.fontSizeLG).mul(token.lineHeightLG).equal(),
notificationCloseButtonSize: token.calc(token.controlHeightLG).mul(0.55).equal(),
notificationMarginBottom: token.margin,
notificationPadding: `${unit(token.paddingMD)} ${unit(token.paddingContentHorizontalLG)}`,
notificationMarginEdge: token.marginLG,
animationMaxHeight: 150,
notificationStackLayer: 3,
});
return notificationToken;
};
export default genStyleHooks(
// @ts-ignore
'Notification',
(token) => {
const notificationToken = prepareNotificationToken(token);
return [
genNotificationStyle(notificationToken),
genNotificationPlacementStyle(notificationToken),
genStackStyle(notificationToken),
];
},
prepareComponentToken,
);

View File

@ -0,0 +1,113 @@
import type { CSSObject } from '@ant-design/cssinjs';
import { Keyframes } from '@ant-design/cssinjs';
import type { NotificationToken } from '.';
import type { GenerateStyle } from '../../theme/internal';
const genNotificationPlacementStyle: GenerateStyle<NotificationToken, CSSObject> = (token) => {
const { componentCls, notificationMarginEdge, animationMaxHeight } = token;
const noticeCls = `${componentCls}-notice`;
const rightFadeIn = new Keyframes('antNotificationFadeIn', {
'0%': {
transform: `translate3d(100%, 0, 0)`,
opacity: 0,
},
'100%': {
transform: `translate3d(0, 0, 0)`,
opacity: 1,
},
});
const topFadeIn = new Keyframes('antNotificationTopFadeIn', {
'0%': {
top: -animationMaxHeight,
opacity: 0,
},
'100%': {
top: 0,
opacity: 1,
},
});
const bottomFadeIn = new Keyframes('antNotificationBottomFadeIn', {
'0%': {
bottom: token.calc(animationMaxHeight).mul(-1).equal(),
opacity: 0,
},
'100%': {
bottom: 0,
opacity: 1,
},
});
const leftFadeIn = new Keyframes('antNotificationLeftFadeIn', {
'0%': {
transform: `translate3d(-100%, 0, 0)`,
opacity: 0,
},
'100%': {
transform: `translate3d(0, 0, 0)`,
opacity: 1,
},
});
return {
[componentCls]: {
[`&${componentCls}-top, &${componentCls}-bottom`]: {
marginInline: 0,
[noticeCls]: {
marginInline: 'auto auto',
},
},
[`&${componentCls}-top`]: {
[`${componentCls}-fade-enter${componentCls}-fade-enter-active, ${componentCls}-fade-appear${componentCls}-fade-appear-active`]:
{
animationName: topFadeIn,
},
},
[`&${componentCls}-bottom`]: {
[`${componentCls}-fade-enter${componentCls}-fade-enter-active, ${componentCls}-fade-appear${componentCls}-fade-appear-active`]:
{
animationName: bottomFadeIn,
},
},
[`&${componentCls}-topRight, &${componentCls}-bottomRight`]: {
[`${componentCls}-fade-enter${componentCls}-fade-enter-active, ${componentCls}-fade-appear${componentCls}-fade-appear-active`]:
{
animationName: rightFadeIn,
},
},
[`&${componentCls}-topLeft, &${componentCls}-bottomLeft`]: {
marginRight: {
value: 0,
_skip_check_: true,
},
marginLeft: {
value: notificationMarginEdge,
_skip_check_: true,
},
[noticeCls]: {
marginInlineEnd: 'auto',
marginInlineStart: 0,
},
[`${componentCls}-fade-enter${componentCls}-fade-enter-active, ${componentCls}-fade-appear${componentCls}-fade-appear-active`]:
{
animationName: leftFadeIn,
},
},
},
};
};
export default genNotificationPlacementStyle;

View File

@ -0,0 +1,25 @@
import { genSubStyleComponent } from '../../theme/internal';
import { prepareComponentToken, genNoticeStyle, prepareNotificationToken } from '.';
import { unit } from '@ant-design/cssinjs';
export default genSubStyleComponent(
// @ts-ignore
['Notification', 'PurePanel'],
(token) => {
const noticeCls = `${token.componentCls}-notice`;
const notificationToken = prepareNotificationToken(token);
return {
[`${noticeCls}-pure-panel`]: {
...genNoticeStyle(notificationToken),
// @ts-ignore
width: notificationToken.width,
maxWidth: `calc(100vw - ${unit(
token.calc(notificationToken.notificationMarginEdge).mul(2).equal(),
)})`,
margin: 0,
},
};
},
prepareComponentToken,
);

View File

@ -0,0 +1,119 @@
import type { GenerateStyle } from '../../theme/internal';
import type { NotificationToken } from '.';
import type { CSSObject } from '@ant-design/cssinjs';
import type { NotificationPlacement } from '../interface';
import { NotificationPlacements } from '../interface';
const placementAlignProperty: Record<NotificationPlacement, 'left' | 'right'> = {
topLeft: 'left',
topRight: 'right',
bottomLeft: 'left',
bottomRight: 'right',
top: 'left',
bottom: 'left',
};
const genPlacementStackStyle = (
token: NotificationToken,
placement: NotificationPlacement,
): CSSObject => {
const { componentCls } = token;
return {
[`${componentCls}-${placement}`]: {
[`&${componentCls}-stack > ${componentCls}-notice-wrapper`]: {
[placement.startsWith('top') ? 'top' : 'bottom']: 0,
[placementAlignProperty[placement]]: { value: 0, _skip_check_: true },
},
},
};
};
const genStackChildrenStyle = (token: NotificationToken): CSSObject => {
const childrenStyle: CSSObject = {};
for (let i = 1; i < token.notificationStackLayer; i++) {
childrenStyle[`&:nth-last-child(${i + 1})`] = {
overflow: 'hidden',
[`& > ${token.componentCls}-notice`]: {
opacity: 0,
transition: `opacity ${token.motionDurationMid}`,
},
};
}
return {
[`&:not(:nth-last-child(-n+${token.notificationStackLayer}))`]: {
opacity: 0,
overflow: 'hidden',
color: 'transparent',
pointerEvents: 'none',
},
...childrenStyle,
};
};
const genStackedNoticeStyle = (token: NotificationToken): CSSObject => {
const childrenStyle: CSSObject = {};
for (let i = 1; i < token.notificationStackLayer; i++) {
childrenStyle[`&:nth-last-child(${i + 1})`] = {
background: token.colorBgBlur,
backdropFilter: 'blur(10px)',
'-webkit-backdrop-filter': 'blur(10px)',
};
}
return {
...childrenStyle,
};
};
const genStackStyle: GenerateStyle<NotificationToken> = (token) => {
const { componentCls } = token;
return {
[`${componentCls}-stack`]: {
[`& > ${componentCls}-notice-wrapper`]: {
transition: `all ${token.motionDurationSlow}, backdrop-filter 0s`,
position: 'absolute',
...genStackChildrenStyle(token),
},
},
[`${componentCls}-stack:not(${componentCls}-stack-expanded)`]: {
[`& > ${componentCls}-notice-wrapper`]: {
...genStackedNoticeStyle(token),
},
},
[`${componentCls}-stack${componentCls}-stack-expanded`]: {
[`& > ${componentCls}-notice-wrapper`]: {
'&:not(:nth-last-child(-n + 1))': {
opacity: 1,
overflow: 'unset',
color: 'inherit',
pointerEvents: 'auto',
[`& > ${token.componentCls}-notice`]: {
opacity: 1,
},
},
'&:after': {
content: '""',
position: 'absolute',
height: token.margin,
width: '100%',
insetInline: 0,
bottom: token.calc(token.margin).mul(-1).equal(),
background: 'transparent',
pointerEvents: 'auto',
},
},
},
...NotificationPlacements.map((placement) => genPlacementStackStyle(token, placement)).reduce(
(acc, cur) => ({ ...acc, ...cur }),
{},
),
};
};
export default genStackStyle;

View File

@ -0,0 +1,220 @@
import * as React from 'react';
import type { FC, PropsWithChildren } from 'react';
import classNames from 'classnames';
import { NotificationProvider, useNotification as useRcNotification } from 'rc-notification';
import type { NotificationAPI, NotificationConfig as RcNotificationConfig } from 'rc-notification';
import { devUseWarning } from '../_util/warning';
import { ConfigContext } from '../config-provider';
import type { ComponentStyleConfig } from '../config-provider/context';
import type {
ArgsProps,
NotificationConfig,
NotificationInstance,
NotificationPlacement,
} from './interface';
import { getCloseIcon, PureContent } from './PurePanel';
import useStyle from './style';
import { getMotion, getPlacementStyle } from './util';
import { useToken } from '../theme/internal';
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
const DEFAULT_OFFSET = 24;
const DEFAULT_DURATION = 4.5;
const DEFAULT_PLACEMENT: NotificationPlacement = 'topRight';
// ==============================================================================
// == Holder ==
// ==============================================================================
type HolderProps = NotificationConfig & {
onAllRemoved?: VoidFunction;
};
interface HolderRef extends NotificationAPI {
prefixCls: string;
notification?: ComponentStyleConfig;
}
const Wrapper: FC<PropsWithChildren<{ prefixCls: string }>> = ({ children, prefixCls }) => {
const rootCls = useCSSVarCls(prefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
return wrapCSSVar(
<NotificationProvider classNames={{ list: classNames(hashId, cssVarCls, rootCls) }}>
{children}
</NotificationProvider>,
);
};
const renderNotifications: RcNotificationConfig['renderNotifications'] = (
node,
{ prefixCls, key },
) => (
<Wrapper prefixCls={prefixCls} key={key}>
{node}
</Wrapper>
);
const Holder = React.forwardRef<HolderRef, HolderProps>((props, ref) => {
const {
top,
bottom,
prefixCls: staticPrefixCls,
getContainer: staticGetContainer,
maxCount,
rtl,
onAllRemoved,
stack,
} = props;
const { getPrefixCls, getPopupContainer, notification } = React.useContext(ConfigContext);
const [, token] = useToken();
const prefixCls = staticPrefixCls || getPrefixCls('notification');
// =============================== Style ===============================
const getStyle = (placement: NotificationPlacement): React.CSSProperties =>
getPlacementStyle(placement, top ?? DEFAULT_OFFSET, bottom ?? DEFAULT_OFFSET);
const getClassName = () => classNames({ [`${prefixCls}-rtl`]: rtl });
// ============================== Motion ===============================
const getNotificationMotion = () => getMotion(prefixCls);
// ============================== Origin ===============================
const [api, holder] = useRcNotification({
prefixCls,
style: getStyle,
className: getClassName,
motion: getNotificationMotion,
closable: true,
closeIcon: getCloseIcon(prefixCls),
duration: DEFAULT_DURATION,
getContainer: () => staticGetContainer?.() || getPopupContainer?.() || document.body,
maxCount,
onAllRemoved,
renderNotifications,
stack:
stack === false
? false
: {
threshold: typeof stack === 'object' ? stack?.threshold : undefined,
offset: 8,
gap: token.margin,
},
});
// ================================ Ref ================================
React.useImperativeHandle(ref, () => ({
...api,
prefixCls,
notification,
}));
return holder;
});
// ==============================================================================
// == Hook ==
// ==============================================================================
export function useInternalNotification(
notificationConfig?: HolderProps,
): readonly [NotificationInstance, React.ReactElement] {
const holderRef = React.useRef<HolderRef>(null);
const warning = devUseWarning('Notification');
// ================================ API ================================
const wrapAPI = React.useMemo<NotificationInstance>(() => {
// Wrap with notification content
// >>> Open
const open = (config: ArgsProps) => {
if (!holderRef.current) {
warning(
false,
'usage',
'You are calling notice in render which will break in React 18 concurrent mode. Please trigger in effect instead.',
);
return;
}
const { open: originOpen, prefixCls, notification } = holderRef.current;
const noticePrefixCls = `${prefixCls}-notice`;
const {
message,
description,
icon,
type,
btn,
className,
style,
role = 'alert',
closeIcon,
...restConfig
} = config;
const realCloseIcon = getCloseIcon(noticePrefixCls, closeIcon);
return originOpen({
// use placement from props instead of hard-coding "topRight"
placement: notificationConfig?.placement ?? DEFAULT_PLACEMENT,
...restConfig,
content: (
<PureContent
prefixCls={noticePrefixCls}
icon={icon}
type={type}
message={message}
description={description}
btn={btn}
role={role}
/>
),
className: classNames(
type && `${noticePrefixCls}-${type}`,
className,
notification?.className,
),
style: { ...notification?.style, ...style },
closeIcon: realCloseIcon,
closable: !!realCloseIcon,
});
};
// >>> 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="notification-holder" {...notificationConfig} ref={holderRef} />,
] as const;
}
export default function useNotification(notificationConfig?: NotificationConfig) {
return useInternalNotification(notificationConfig);
}

View 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`,
};
}

View File

@ -68,6 +68,9 @@ importers:
packages/biz:
dependencies:
'@ant-design/icons':
specifier: ^5.2.6
version: 5.2.6(react-dom@18.2.0)(react@18.2.0)
'@zhst/func':
specifier: workspace:^
version: link:../func