diff --git a/packages/meta/src/checkbox/demo/debug-line.tsx b/packages/meta/src/checkbox/demo/debug-line.tsx
index c3e8a11..6806c1f 100644
--- a/packages/meta/src/checkbox/demo/debug-line.tsx
+++ b/packages/meta/src/checkbox/demo/debug-line.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Checkbox, ConfigProvider, Radio, Space } from 'antd';
+import { Checkbox, ConfigProvider, Radio, Space } from '@zhst/meta';
const sharedStyle: React.CSSProperties = {
border: '1px solid red',
diff --git a/packages/meta/src/checkbox/demo/disabled.tsx b/packages/meta/src/checkbox/demo/disabled.tsx
index aeb519f..f8d6625 100644
--- a/packages/meta/src/checkbox/demo/disabled.tsx
+++ b/packages/meta/src/checkbox/demo/disabled.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Checkbox } from 'antd';
+import { Checkbox } from '@zhst/meta';
const App: React.FC = () => (
<>
diff --git a/packages/meta/src/checkbox/demo/group.tsx b/packages/meta/src/checkbox/demo/group.tsx
index a7c8c9c..fdeb7bb 100644
--- a/packages/meta/src/checkbox/demo/group.tsx
+++ b/packages/meta/src/checkbox/demo/group.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { Checkbox } from 'antd';
-import type { CheckboxValueType } from 'antd/es/checkbox/Group';
+import { Checkbox } from '@zhst/meta';
+import type { CheckboxValueType } from '@zhst/meta';
const onChange = (checkedValues: CheckboxValueType[]) => {
console.log('checked = ', checkedValues);
diff --git a/packages/meta/src/checkbox/demo/layout.tsx b/packages/meta/src/checkbox/demo/layout.tsx
index ec3df13..65c8352 100644
--- a/packages/meta/src/checkbox/demo/layout.tsx
+++ b/packages/meta/src/checkbox/demo/layout.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { Checkbox, Col, Row } from 'antd';
-import type { CheckboxValueType } from 'antd/es/checkbox/Group';
+import { Checkbox, Col, Row } from '@zhst/meta';
+import type { CheckboxValueType } from '@zhst/meta';
const onChange = (checkedValues: CheckboxValueType[]) => {
console.log('checked = ', checkedValues);
diff --git a/packages/meta/src/config-provider/demo/direction.tsx b/packages/meta/src/config-provider/demo/direction.tsx
index bb6cb8a..6bfdc4d 100644
--- a/packages/meta/src/config-provider/demo/direction.tsx
+++ b/packages/meta/src/config-provider/demo/direction.tsx
@@ -8,7 +8,7 @@ import {
SmileOutlined,
} from '@ant-design/icons';
import React, { useState } from 'react';
-import type { RadioChangeEvent } from 'antd';
+import type { RadioChangeEvent } from '@zhst/meta';
import {
Badge,
Button,
@@ -29,7 +29,7 @@ import {
Switch,
Tree,
TreeSelect,
-} from 'antd';
+} from '@zhst/meta';
import type { DirectionType } from 'antd/es/config-provider';
const InputGroup = Input.Group;
diff --git a/packages/meta/src/config-provider/demo/locale.tsx b/packages/meta/src/config-provider/demo/locale.tsx
index f49a206..09fdc50 100644
--- a/packages/meta/src/config-provider/demo/locale.tsx
+++ b/packages/meta/src/config-provider/demo/locale.tsx
@@ -5,7 +5,7 @@
import { EllipsisOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import React, { useState } from 'react';
-import type { RadioChangeEvent, TourProps, UploadFile } from 'antd';
+import type { RadioChangeEvent, TourProps, UploadFile } from '@zhst/meta';
import {
Upload,
Tour,
@@ -29,7 +29,7 @@ import {
Image,
InputNumber,
Divider,
-} from 'antd';
+} from '@zhst/meta';
import type { Locale } from 'antd/es/locale';
import enUS from 'antd/locale/en_US';
import zhCN from 'antd/locale/zh_CN';
diff --git a/packages/meta/src/date-picker/demo/basic.tsx b/packages/meta/src/date-picker/demo/basic.tsx
index 9a943c1..3d49ea3 100644
--- a/packages/meta/src/date-picker/demo/basic.tsx
+++ b/packages/meta/src/date-picker/demo/basic.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import type { DatePickerProps } from 'antd';
-import { DatePicker, Space } from 'antd';
+import type { DatePickerProps } from '@zhst/meta';
+import { DatePicker, Space } from '@zhst/meta';
const onChange: DatePickerProps['onChange'] = (date, dateString) => {
console.log(date, dateString);
diff --git a/packages/meta/src/date-picker/demo/bordered.tsx b/packages/meta/src/date-picker/demo/bordered.tsx
index 058476f..ee5c774 100644
--- a/packages/meta/src/date-picker/demo/bordered.tsx
+++ b/packages/meta/src/date-picker/demo/bordered.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { DatePicker, Space } from 'antd';
+import { DatePicker, Space } from '@zhst/meta';
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/cell-render.tsx b/packages/meta/src/date-picker/demo/cell-render.tsx
index f54a60b..fd1743f 100644
--- a/packages/meta/src/date-picker/demo/cell-render.tsx
+++ b/packages/meta/src/date-picker/demo/cell-render.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { DatePicker, Space, theme } from 'antd';
+import { DatePicker, Space, theme } from '@zhst/meta';
import type { Dayjs } from 'dayjs';
import type { CellRenderInfo } from 'rc-picker/es/interface';
diff --git a/packages/meta/src/date-picker/demo/component-token.tsx b/packages/meta/src/date-picker/demo/component-token.tsx
index 97b5710..0a7a833 100644
--- a/packages/meta/src/date-picker/demo/component-token.tsx
+++ b/packages/meta/src/date-picker/demo/component-token.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import type { DatePickerProps } from 'antd';
-import { ConfigProvider, DatePicker, Space, TimePicker } from 'antd';
+import type { DatePickerProps } from '@zhst/meta';
+import { ConfigProvider, DatePicker, Space, TimePicker } from '@zhst/meta';
import dayjs from 'dayjs';
/** Test usage. Do not use in your production. */
diff --git a/packages/meta/src/date-picker/demo/disabled-date.tsx b/packages/meta/src/date-picker/demo/disabled-date.tsx
index 2c08389..617fa01 100644
--- a/packages/meta/src/date-picker/demo/disabled-date.tsx
+++ b/packages/meta/src/date-picker/demo/disabled-date.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
-import { DatePicker, Space } from 'antd';
-import type { RangePickerProps } from 'antd/es/date-picker';
+import { DatePicker, Space } from '@zhst/meta';
+import type { RangePickerProps } from '@zhst/meta';
dayjs.extend(customParseFormat);
diff --git a/packages/meta/src/date-picker/demo/disabled.tsx b/packages/meta/src/date-picker/demo/disabled.tsx
index 5e38222..74f70b3 100644
--- a/packages/meta/src/date-picker/demo/disabled.tsx
+++ b/packages/meta/src/date-picker/demo/disabled.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
-import { DatePicker, Space } from 'antd';
+import { DatePicker, Space } from '@zhst/meta';
dayjs.extend(customParseFormat);
diff --git a/packages/meta/src/date-picker/demo/extra-footer.tsx b/packages/meta/src/date-picker/demo/extra-footer.tsx
index 6a1405b..6754319 100644
--- a/packages/meta/src/date-picker/demo/extra-footer.tsx
+++ b/packages/meta/src/date-picker/demo/extra-footer.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { DatePicker, Space } from 'antd';
+import { DatePicker, Space } from '@zhst/meta';
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/format.tsx b/packages/meta/src/date-picker/demo/format.tsx
index ff37abe..a8663da 100644
--- a/packages/meta/src/date-picker/demo/format.tsx
+++ b/packages/meta/src/date-picker/demo/format.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
-import type { DatePickerProps } from 'antd';
-import { DatePicker, Space } from 'antd';
+import type { DatePickerProps } from '@zhst/meta';
+import { DatePicker, Space } from '@zhst/meta';
dayjs.extend(customParseFormat);
diff --git a/packages/meta/src/date-picker/demo/mode.tsx b/packages/meta/src/date-picker/demo/mode.tsx
index 4b64a5a..5d95122 100644
--- a/packages/meta/src/date-picker/demo/mode.tsx
+++ b/packages/meta/src/date-picker/demo/mode.tsx
@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import type { Dayjs } from 'dayjs';
-import type { DatePickerProps } from 'antd';
-import { DatePicker, Space } from 'antd';
-import type { RangePickerProps } from 'antd/es/date-picker';
+import type { DatePickerProps } from '@zhst/meta';
+import { DatePicker, Space } from '@zhst/meta';
+import type { RangePickerProps } from '@zhst/meta';
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/placement.tsx b/packages/meta/src/date-picker/demo/placement.tsx
index a980b75..c08a1eb 100644
--- a/packages/meta/src/date-picker/demo/placement.tsx
+++ b/packages/meta/src/date-picker/demo/placement.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
-import type { DatePickerProps, RadioChangeEvent } from 'antd';
-import { DatePicker, Radio } from 'antd';
+import type { DatePickerProps, RadioChangeEvent } from '@zhst/meta';
+import { DatePicker, Radio } from '@zhst/meta';
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/preset-ranges.tsx b/packages/meta/src/date-picker/demo/preset-ranges.tsx
index cecbab0..5c94b1e 100644
--- a/packages/meta/src/date-picker/demo/preset-ranges.tsx
+++ b/packages/meta/src/date-picker/demo/preset-ranges.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs';
-import type { TimeRangePickerProps } from 'antd';
-import { DatePicker, Space } from 'antd';
+import type { TimeRangePickerProps } from '@zhst/meta';
+import { DatePicker, Space } from '@zhst/meta';
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/range-picker.tsx b/packages/meta/src/date-picker/demo/range-picker.tsx
index 215d730..205c30f 100644
--- a/packages/meta/src/date-picker/demo/range-picker.tsx
+++ b/packages/meta/src/date-picker/demo/range-picker.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { DatePicker, Space } from 'antd';
+import { DatePicker, Space } from '@zhst/meta';
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/render-panel.tsx b/packages/meta/src/date-picker/demo/render-panel.tsx
index bdab140..9002e22 100644
--- a/packages/meta/src/date-picker/demo/render-panel.tsx
+++ b/packages/meta/src/date-picker/demo/render-panel.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { DatePicker } from 'antd';
+import { DatePicker } from '@zhst/meta';
const { _InternalPanelDoNotUseOrYouWillBeFired: InternalDatePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/select-in-range.tsx b/packages/meta/src/date-picker/demo/select-in-range.tsx
index 0222e1f..41cf5ea 100644
--- a/packages/meta/src/date-picker/demo/select-in-range.tsx
+++ b/packages/meta/src/date-picker/demo/select-in-range.tsx
@@ -1,6 +1,6 @@
import type { Dayjs } from 'dayjs';
import React, { useState } from 'react';
-import { DatePicker } from 'antd';
+import { DatePicker } from '@zhst/meta';
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/size.tsx b/packages/meta/src/date-picker/demo/size.tsx
index 17250bd..6d1b309 100644
--- a/packages/meta/src/date-picker/demo/size.tsx
+++ b/packages/meta/src/date-picker/demo/size.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
-import type { RadioChangeEvent } from 'antd';
-import { DatePicker, Radio, Space } from 'antd';
+import type { RadioChangeEvent } from '@zhst/meta';
+import { DatePicker, Radio, Space } from '@zhst/meta';
import type { SizeType } from 'antd/es/config-provider/SizeContext';
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/start-end.tsx b/packages/meta/src/date-picker/demo/start-end.tsx
index 78247e0..e8eb384 100644
--- a/packages/meta/src/date-picker/demo/start-end.tsx
+++ b/packages/meta/src/date-picker/demo/start-end.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import type { Dayjs } from 'dayjs';
-import { DatePicker, Space } from 'antd';
+import { DatePicker, Space } from '@zhst/meta';
const App: React.FC = () => {
const [startValue, setStartValue] = useState
(null);
diff --git a/packages/meta/src/date-picker/demo/status.tsx b/packages/meta/src/date-picker/demo/status.tsx
index 6aa6074..7f53e9a 100644
--- a/packages/meta/src/date-picker/demo/status.tsx
+++ b/packages/meta/src/date-picker/demo/status.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { DatePicker, Space } from 'antd';
+import { DatePicker, Space } from '@zhst/meta';
const App: React.FC = () => (
diff --git a/packages/meta/src/date-picker/demo/suffix.tsx b/packages/meta/src/date-picker/demo/suffix.tsx
index 01c5d69..9a6cbfd 100644
--- a/packages/meta/src/date-picker/demo/suffix.tsx
+++ b/packages/meta/src/date-picker/demo/suffix.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { SmileOutlined } from '@ant-design/icons';
import type { Dayjs } from 'dayjs';
-import { DatePicker, Space } from 'antd';
+import { DatePicker, Space } from '@zhst/meta';
const smileIcon = ;
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/demo/switchable.tsx b/packages/meta/src/date-picker/demo/switchable.tsx
index a579d94..305be54 100644
--- a/packages/meta/src/date-picker/demo/switchable.tsx
+++ b/packages/meta/src/date-picker/demo/switchable.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
-import type { DatePickerProps, TimePickerProps } from 'antd';
-import { DatePicker, Select, Space, TimePicker } from 'antd';
+import type { DatePickerProps, TimePickerProps } from '@zhst/meta';
+import { DatePicker, Select, Space, TimePicker } from '@zhst/meta';
const { Option } = Select;
diff --git a/packages/meta/src/date-picker/demo/time.tsx b/packages/meta/src/date-picker/demo/time.tsx
index eb226d7..b6fefd3 100644
--- a/packages/meta/src/date-picker/demo/time.tsx
+++ b/packages/meta/src/date-picker/demo/time.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { DatePicker, Space } from 'antd';
-import type { DatePickerProps, RangePickerProps } from 'antd/es/date-picker';
+import { DatePicker, Space } from '@zhst/meta';
+import type { DatePickerProps, RangePickerProps } from '@zhst/meta';
const { RangePicker } = DatePicker;
diff --git a/packages/meta/src/date-picker/index.md b/packages/meta/src/date-picker/index.md
new file mode 100644
index 0000000..9e6a36d
--- /dev/null
+++ b/packages/meta/src/date-picker/index.md
@@ -0,0 +1,244 @@
+---
+category: Components
+group: 数据录入
+title: DatePicker 日期选择框
+subtitle: 日期选择框
+description: 输入或选择日期的控件。
+demo:
+ cols: 2
+---
+
+## 何时使用
+
+当用户需要输入一个日期,可以点击标准输入框,弹出日期面板进行选择。
+
+## 代码演示
+
+
+基本
+范围选择器
+
+日期格式
+日期时间选择
+禁用
+不可选择日期和时间
+选择不超过七天的范围
+预设范围
+额外的页脚
+三种大小
+
+自定义状态
+无边框
+弹出位置
+受控面板
+自定义日期范围选择
+后缀图标
+\_InternalPanelDoNotUseOrYouWillBeFired
+
+
+## API
+
+通用属性参考:[通用属性](/docs/react/common-props)
+
+日期类组件包括以下五种形式。
+
+- DatePicker
+- DatePicker\[picker="month"]
+- DatePicker\[picker="week"]
+- DatePicker\[picker="year"]
+- DatePicker\[picker="quarter"] (4.1.0 新增)
+- RangePicker
+
+### 国际化配置
+
+默认配置为 en-US,如果你需要设置其他语言,推荐在入口处使用我们提供的国际化组件,详见:[ConfigProvider 国际化](https://ant.design/components/config-provider-cn/)。
+
+如有特殊需求(仅修改单一组件的语言),请使用 locale 参数,参考:[默认配置](https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json)。
+
+
+:::warning
+在搭配 Nextjs 的 App Router 使用时,注意在引入 dayjs 的 locale 文件时加上 `'use client'`。这是由于 Ant Design 的组件都是客户端组件,在 RSC 中引入 dayjs 的 locale 文件将不会在客户端生效。
+:::
+
+```js
+import locale from 'antd/es/date-picker/locale/zh_CN';
+
+import 'dayjs/locale/zh-cn';
+
+ ;
+```
+
+### 共同的 API
+
+以下 API 为 DatePicker、 RangePicker 共享的 API。
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| allowClear | 自定义清除按钮 | boolean \| { clearIcon?: ReactNode } | true | 5.8.0: 支持对象类型 |
+| autoFocus | 自动获取焦点 | boolean | false | |
+| bordered | 是否有边框 | boolean | true | |
+| className | 选择器 className | string | - | |
+| dateRender | 自定义日期单元格的内容,5.4.0 起用 `cellRender` 代替 | function(currentDate: dayjs, today: dayjs) => React.ReactNode | - | < 5.4.0 |
+| changeOnBlur | 失去焦点时触发 `change` 事件,例如 datetime 下不再需要点击确认按钮 | boolean | false | 5.5.0 |
+| cellRender | 自定义单元格的内容 | (current: dayjs, info: { originNode: React.ReactElement,today: DateType, range?: 'start' \| 'end', type: PanelMode, locale?: Locale, subType?: 'hour' \| 'minute' \| 'second' \| 'meridiem' }) => React.ReactNode | - | 5.4.0 |
+| disabled | 禁用 | boolean | false | |
+| disabledDate | 不可选择的日期 | (currentDate: dayjs) => boolean | - | |
+| format | 设置日期格式,为数组时支持多格式匹配,展示以第一个为准。配置参考 [dayjs#format](https://day.js.org/docs/zh-CN/display/format#%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%8D%A0%E4%BD%8D%E7%AC%A6%E5%88%97%E8%A1%A8)。示例:[自定义格式](#components-date-picker-demo-format) | [formatType](#formattype) | [rc-picker](https://github.com/react-component/picker/blob/f512f18ed59d6791280d1c3d7d37abbb9867eb0b/src/utils/uiUtil.ts#L155-L177) | |
+| popupClassName | 额外的弹出日历 className | string | - | 4.23.0 |
+| getPopupContainer | 定义浮层的容器,默认为 body 上新建 div | function(trigger) | - | |
+| inputReadOnly | 设置输入框为只读(避免在移动设备上打开虚拟键盘) | boolean | false | |
+| locale | 国际化配置 | object | [默认配置](https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json) | |
+| mode | 日期面板的状态([设置后无法选择年份/月份?](/docs/react/faq#当我指定了-datepickerrangepicker-的-mode-属性后点击后无法选择年份月份)) | `time` \| `date` \| `month` \| `year` \| `decade` | - | |
+| nextIcon | 自定义下一个图标 | ReactNode | - | 4.17.0 |
+| open | 控制弹层是否展开 | boolean | - | |
+| panelRender | 自定义渲染面板 | (panelNode) => ReactNode | - | 4.5.0 |
+| picker | 设置选择器类型 | `date` \| `week` \| `month` \| `quarter` \| `year` | `date` | `quarter`: 4.1.0 |
+| placeholder | 输入框提示文字 | string \| \[string, string] | - | |
+| placement | 选择框弹出的位置 | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | |
+| popupStyle | 额外的弹出日历样式 | CSSProperties | {} | |
+| prevIcon | 自定义上一个图标 | ReactNode | - | 4.17.0 |
+| presets | 预设时间范围快捷选择, 自 `5.8.0` 起 value 支持函数返回值 | { label: React.ReactNode, value: Dayjs \| (() => Dayjs) }\[] | - | |
+| size | 输入框大小,`large` 高度为 40px,`small` 为 24px,默认是 32px | `large` \| `middle` \| `small` | - | |
+| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
+| style | 自定义输入框样式 | CSSProperties | {} | |
+| suffixIcon | 自定义的选择框后缀图标 | ReactNode | - | |
+| superNextIcon | 自定义 `>>` 切换图标 | ReactNode | - | 4.17.0 |
+| superPrevIcon | 自定义 `<<` 切换图标 | ReactNode | - | 4.17.0 |
+| onOpenChange | 弹出日历和关闭日历的回调 | function(open) | - | |
+| onPanelChange | 日历面板切换的回调 | function(value, mode) | - | |
+
+### 共同的方法
+
+| 名称 | 描述 | 版本 |
+| ------- | -------- | ---- |
+| blur() | 移除焦点 | |
+| focus() | 获取焦点 | |
+
+### DatePicker
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| defaultValue | 默认日期,如果开始时间或结束时间为 `null` 或者 `undefined`,日期范围将是一个开区间 | [dayjs](https://day.js.org/) | - | |
+| disabledTime | 不可选择的时间 | function(date) | - | |
+| format | 展示的日期格式,配置参考 [dayjs#format](https://day.js.org/docs/zh-CN/display/format#%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%8D%A0%E4%BD%8D%E7%AC%A6%E5%88%97%E8%A1%A8)。 | [formatType](#formattype) | `YYYY-MM-DD` | |
+| renderExtraFooter | 在面板中添加额外的页脚 | (mode) => React.ReactNode | - | |
+| showNow | 当设定了 `showTime` 的时候,面板是否显示“此刻”按钮 | boolean | - | 4.4.0 |
+| showTime | 增加时间选择功能 | Object \| boolean | [TimePicker Options](/components/time-picker-cn#api) | |
+| showTime.defaultValue | 设置用户选择日期时默认的时分秒,[例子](#components-date-picker-demo-disabled-date) | [dayjs](https://day.js.org/) | dayjs() | |
+| showToday | 是否展示“今天”按钮 | boolean | true | |
+| value | 日期 | [dayjs](https://day.js.org/) | - | |
+| onChange | 时间发生变化的回调 | function(date: dayjs, dateString: string) | - | |
+| onOk | 点击确定按钮的回调 | function() | - | |
+| onPanelChange | 日期面板变化时的回调 | function(value, mode) | - | |
+
+### DatePicker\[picker=year]
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| defaultValue | 默认日期 | [dayjs](https://day.js.org/) | - | |
+| format | 展示的日期格式,配置参考 [dayjs#format](https://day.js.org/docs/zh-CN/display/format#%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%8D%A0%E4%BD%8D%E7%AC%A6%E5%88%97%E8%A1%A8)。 | [formatType](#formattype) | `YYYY` | |
+| renderExtraFooter | 在面板中添加额外的页脚 | () => React.ReactNode | - | |
+| value | 日期 | [dayjs](https://day.js.org/) | - | |
+| onChange | 时间发生变化的回调,发生在用户选择时间时 | function(date: dayjs, dateString: string) | - | |
+
+### DatePicker\[picker=quarter]
+
+`4.1.0` 新增。
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| defaultValue | 默认日期 | [dayjs](https://day.js.org/) | - | |
+| format | 展示的日期格式,配置参考 [dayjs#format](https://day.js.org/docs/zh-CN/display/format#%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%8D%A0%E4%BD%8D%E7%AC%A6%E5%88%97%E8%A1%A8)。 | [formatType](#formattype) | `YYYY-\QQ` | |
+| renderExtraFooter | 在面板中添加额外的页脚 | () => React.ReactNode | - | |
+| value | 日期 | [dayjs](https://day.js.org/) | - | |
+| onChange | 时间发生变化的回调,发生在用户选择时间时 | function(date: dayjs, dateString: string) | - | |
+
+### DatePicker\[picker=month]
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| defaultValue | 默认日期 | [dayjs](https://day.js.org/) | - | |
+| format | 展示的日期格式,配置参考 [dayjs#format](https://day.js.org/docs/zh-CN/display/format#%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%8D%A0%E4%BD%8D%E7%AC%A6%E5%88%97%E8%A1%A8)。 | [formatType](#formattype) | `YYYY-MM` | |
+| renderExtraFooter | 在面板中添加额外的页脚 | () => React.ReactNode | - | |
+| value | 日期 | [dayjs](https://day.js.org/) | - | |
+| onChange | 时间发生变化的回调,发生在用户选择时间时 | function(date: dayjs, dateString: string) | - | |
+
+### DatePicker\[picker=week]
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| defaultValue | 默认日期 | [dayjs](https://day.js.org/) | - | |
+| format | 展示的日期格式,配置参考 [dayjs#format](https://day.js.org/docs/zh-CN/display/format#%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%8D%A0%E4%BD%8D%E7%AC%A6%E5%88%97%E8%A1%A8)。 | [formatType](#formattype) | `YYYY-wo` | |
+| renderExtraFooter | 在面板中添加额外的页脚 | (mode) => React.ReactNode | - | |
+| value | 日期 | [dayjs](https://day.js.org/) | - | |
+| onChange | 时间发生变化的回调,发生在用户选择时间时 | function(date: dayjs, dateString: string) | - | |
+
+### RangePicker
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| allowEmpty | 允许起始项部分为空 | \[boolean, boolean] | \[false, false] | |
+| dateRender | 自定义日期单元格的内容,5.4.0 起用 `cellRender` 代替 | function(currentDate: dayjs, today: dayjs) => React.ReactNode | - | < 5.4.0 |
+| cellRender | 自定义单元格的内容。 | (current: dayjs, info: { originNode: React.ReactElement,today: DateType, range?: 'start' \| 'end', type: PanelMode, locale?: Locale, subType?: 'hour' \| 'minute' \| 'second' \| 'meridiem' }) => React.ReactNode | - | 5.4.0 |
+| defaultValue | 默认日期 | [dayjs](https://day.js.org/)\[] | - | |
+| disabled | 禁用起始项 | \[boolean, boolean] | - | |
+| disabledTime | 不可选择的时间 | function(date: dayjs, partial: `start` \| `end`) | - | |
+| format | 展示的日期格式,配置参考 [dayjs#format](https://day.js.org/docs/zh-CN/display/format#%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%8D%A0%E4%BD%8D%E7%AC%A6%E5%88%97%E8%A1%A8)。 | [formatType](#formattype) | `YYYY-MM-DD HH:mm:ss` | |
+| presets | 预设时间范围快捷选择,自 `5.8.0` 起 value 支持函数返回值 | { label: React.ReactNode, value: (Dayjs \| (() => Dayjs))\[] }\[] | - | |
+| renderExtraFooter | 在面板中添加额外的页脚 | () => React.ReactNode | - | |
+| separator | 设置分隔符 | React.ReactNode | ` ` | |
+| showTime | 增加时间选择功能 | Object\|boolean | [TimePicker Options](/components/time-picker-cn#api) | |
+| showTime.defaultValue | 设置用户选择日期时默认的时分秒,[例子](#components-date-picker-demo-disabled-date) | [dayjs](https://day.js.org/)\[] | \[dayjs(), dayjs()] | |
+| value | 日期 | [dayjs](https://day.js.org/)\[] | - | |
+| onCalendarChange | 待选日期发生变化的回调。`info` 参数自 4.4.0 添加 | function(dates: \[dayjs, dayjs], dateStrings: \[string, string], info: { range:`start`\|`end` }) | - | |
+| onChange | 日期范围发生变化的回调 | function(dates: \[dayjs, dayjs], dateStrings: \[string, string]) | - | |
+
+#### formatType
+
+```ts
+import type { Dayjs } from 'dayjs';
+
+type Generic = string;
+type GenericFn = (value: Dayjs) => string;
+
+export type FormatType = Generic | GenericFn | Array;
+```
+
+## FAQ
+
+### 当我指定了 DatePicker/RangePicker 的 mode 属性后,点击后无法选择年份/月份?
+
+请参考[常见问答](/docs/react/faq#当我指定了-datepickerrangepicker-的-mode-属性后点击后无法选择年份月份)
+
+### 为何日期选择年份后返回的是日期面板而不是月份面板?
+
+当用户选择完年份后,系统会直接切换至日期面板,而非显式提供月份选择。这样做的设计在于用户只需进行一次点击即可完成年份修改,无需再次点击进入月份选择界面,从而减少了用户的操作负担,同时也避免需要额外感知月份的记忆负担。
+
+### 如何在 DatePicker 中使用自定义日期库(如 Moment.js )?
+
+请参考[《使用自定义日期库》](/docs/react/use-custom-date-library#datepicker)
+
+### 为什么时间类组件的国际化 locale 设置不生效?
+
+参考 FAQ [为什么时间类组件的国际化 locale 设置不生效?](/docs/react/faq#为什么时间类组件的国际化-locale-设置不生效)。
+
+### 如何修改周的起始日?
+
+请使用正确的[语言包](/docs/react/i18n-cn)([#5605](https://github.com/ant-design/ant-design/issues/5605)),或者修改 dayjs 的 `locale` 配置:
+
+```js
+import dayjs from 'dayjs';
+
+import 'dayjs/locale/zh-cn';
+
+import updateLocale from 'dayjs/plugin/updateLocale';
+
+dayjs.extend(updateLocale);
+dayjs.updateLocale('zh-cn', {
+ weekStart: 0,
+});
+```
+
+### 为何使用 `panelRender` 时,原来面板无法切换?
+
+当你通过 `panelRender` 动态改变层级结构时,会使得原本的 Panel 被当做新的节点删除并创建。这使得其原本的状态会被重置,保持结构稳定即可。详情请参考 [#27263](https://github.com/ant-design/ant-design/issues/27263)。
diff --git a/packages/meta/src/index.tsx b/packages/meta/src/index.tsx
index ce7f37d..544fb69 100644
--- a/packages/meta/src/index.tsx
+++ b/packages/meta/src/index.tsx
@@ -8,8 +8,12 @@ export type { VideoViewProps, VideoViewRef } from './VideoPlayer'
export { default as Tabs } from './tabs'
export type { TabPaneProps, TabsProps } from './tabs';
export { default as Button } from './button'
+export { default as message } from './message'
+export type { ArgsProps } from './message'
export type { ButtonProps, ButtonGroupProps } from './button';
export { default as Space } from './space'
+export { default as Slider } from './slider'
+export type { SliderBaseProps, SliderMarks, SliderRangeProps, SliderSingleProps, SliderTooltipProps } from './slider';
export type { SpaceProps } from './space';
export { default as Switch } from './switch'
export type { SwitchProps } from './switch';
@@ -21,8 +25,8 @@ export type { ColProps } from './col';
export { default as TimePicker } from './time-picker'
export type { TimePickerProps } from './time-picker';
export { default as DatePicker } from './date-picker'
-export type { DatePickerProps } from './date-picker';
-export { default as Calender } from './calendar'
+export type { DatePickerProps, DatePickerType, WeekPickerProps, MonthPickerProps, RangePickerProps } from './date-picker';
+export { default as Calendar } from './calendar'
export type { CalendarProps } from './calendar';
export { default as Empty } from './empty'
export type { EmptyProps } from './empty';
@@ -33,7 +37,8 @@ export type { SelectProps } from './select';
export { default as Radio } from './radio'
export type { RadioProps, RadioChangeEvent, RadioGroupButtonStyle, RadioChangeEventTarget, RadioGroupContextProps, RadioGroupProps, RadioGroupOptionType } from './radio';
export { default as Checkbox } from './checkbox'
-export type { CheckboxProps, CheckboxGroupProps } from './checkbox';
+export type { CheckboxProps, CheckboxGroupProps, CheckboxChangeEvent } from './checkbox';
+export type { CheckboxOptionType, CheckboxValueType } from './checkbox/Group';
export { default as Input } from './input'
export type { InputProps, PasswordProps, SearchProps, GroupProps } from './input';
export { default as ConfigProvider } from './config-provider'
@@ -57,3 +62,22 @@ export { default as Flex } from './flex'
export { default as Score } from './score'
export { default as Progress } from './progress'
export type { ProgressProps, ProgressAriaProps } from './progress'
+export { default as theme } from './theme'
+export { default as Badge } from './badge'
+export type { BadgeProps } from './badge'
+export { default as Alert } from './alert'
+export type { AlertProps } from './alert'
+export { default as Popover } from './popover'
+export type { PopoverProps } from './popover'
+export { default as Avatar } from './avatar'
+export type { AvatarProps } from './avatar'
+export { default as Card } from './card'
+export type { CardGridProps, CardInterface, CardMetaProps, CardProps, CardTabListType } from './card'
+export { default as Skeleton } from './skeleton'
+export type { SkeletonProps } from './skeleton'
+export { default as Tooltip } from './tooltip'
+export type { TooltipAlignConfig, TooltipProps, TooltipPlacement, TooltipPropsWithOverlay, AbstractTooltipProps, TooltipPropsWithTitle, TooltipRef } from './tooltip'
+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'
diff --git a/packages/meta/src/message/PurePanel.tsx b/packages/meta/src/message/PurePanel.tsx
new file mode 100644
index 0000000..9213b4c
--- /dev/null
+++ b/packages/meta/src/message/PurePanel.tsx
@@ -0,0 +1,75 @@
+import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
+import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
+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 * as React from 'react';
+import { ConfigContext } from '../config-provider';
+import type { NoticeType } from './interface';
+import useStyle from './style';
+import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
+
+export const TypeIcon = {
+ info: ,
+ success: ,
+ error: ,
+ warning: ,
+ loading: ,
+};
+
+export interface PureContentProps {
+ prefixCls: string;
+ type?: NoticeType;
+ icon?: React.ReactNode;
+ children: React.ReactNode;
+}
+
+export const PureContent: React.FC = ({ prefixCls, type, icon, children }) => (
+
+ {icon || TypeIcon[type!]}
+ {children}
+
+);
+
+export interface PurePanelProps
+ extends Omit,
+ Omit {
+ prefixCls?: string;
+}
+
+/** @private Internal Component. Do not use in your production. */
+const PurePanel: React.FC = (props) => {
+ const { prefixCls: staticPrefixCls, className, type, icon, content, ...restProps } = props;
+ const { getPrefixCls } = React.useContext(ConfigContext);
+
+ const prefixCls = staticPrefixCls || getPrefixCls('message');
+
+ const rootCls = useCSSVarCls(prefixCls);
+ const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
+
+ return wrapCSSVar(
+
+ {content}
+
+ }
+ />,
+ );
+};
+
+export default PurePanel;
diff --git a/packages/meta/src/message/__tests__/__snapshots__/demo-extend.test.ts.snap b/packages/meta/src/message/__tests__/__snapshots__/demo-extend.test.ts.snap
new file mode 100644
index 0000000..e6af62b
--- /dev/null
+++ b/packages/meta/src/message/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -0,0 +1,254 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders components/message/demo/component-token.tsx extend context correctly 1`] = `
+Array [
+
+
+
+
+
+
+
+
+
+ Hello World!
+
+
+
+
,
+
+
+
+
+
+
+
+
+
+ Hello World!
+
+
+
+
,
+]
+`;
+
+exports[`renders components/message/demo/component-token.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/message/demo/custom-style.tsx extend context correctly 1`] = `
+
+
+ Customized style
+
+
+`;
+
+exports[`renders components/message/demo/custom-style.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/message/demo/duration.tsx extend context correctly 1`] = `
+
+
+ Customized display duration
+
+
+`;
+
+exports[`renders components/message/demo/duration.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/message/demo/hooks.tsx extend context correctly 1`] = `
+
+
+ Display normal message
+
+
+`;
+
+exports[`renders components/message/demo/hooks.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/message/demo/info.tsx extend context correctly 1`] = `
+
+
+ Static Method
+
+
+`;
+
+exports[`renders components/message/demo/info.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/message/demo/loading.tsx extend context correctly 1`] = `
+
+
+ Display a loading indicator
+
+
+`;
+
+exports[`renders components/message/demo/loading.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/message/demo/other.tsx extend context correctly 1`] = `
+
+
+
+
+ Success
+
+
+
+
+
+
+ Error
+
+
+
+
+
+
+ Warning
+
+
+
+
+`;
+
+exports[`renders components/message/demo/other.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/message/demo/render-panel.tsx extend context correctly 1`] = `
+
+
+
+
+
+
+
+
+
+ Hello World!
+
+
+
+
+`;
+
+exports[`renders components/message/demo/render-panel.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/message/demo/thenable.tsx extend context correctly 1`] = `
+
+
+ Display sequential messages
+
+
+`;
+
+exports[`renders components/message/demo/thenable.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/message/demo/update.tsx extend context correctly 1`] = `
+
+
+ Open the message box
+
+
+`;
+
+exports[`renders components/message/demo/update.tsx extend context correctly 2`] = `[]`;
diff --git a/packages/meta/src/message/__tests__/__snapshots__/demo.test.ts.snap b/packages/meta/src/message/__tests__/__snapshots__/demo.test.ts.snap
new file mode 100644
index 0000000..27458a2
--- /dev/null
+++ b/packages/meta/src/message/__tests__/__snapshots__/demo.test.ts.snap
@@ -0,0 +1,234 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders components/message/demo/component-token.tsx correctly 1`] = `
+Array [
+
+
+
+
+
+
+
+
+
+ Hello World!
+
+
+
+
,
+
+
+
+
+
+
+
+
+
+ Hello World!
+
+
+
+
,
+]
+`;
+
+exports[`renders components/message/demo/custom-style.tsx correctly 1`] = `
+
+
+ Customized style
+
+
+`;
+
+exports[`renders components/message/demo/duration.tsx correctly 1`] = `
+
+
+ Customized display duration
+
+
+`;
+
+exports[`renders components/message/demo/hooks.tsx correctly 1`] = `
+
+
+ Display normal message
+
+
+`;
+
+exports[`renders components/message/demo/info.tsx correctly 1`] = `
+
+
+ Static Method
+
+
+`;
+
+exports[`renders components/message/demo/loading.tsx correctly 1`] = `
+
+
+ Display a loading indicator
+
+
+`;
+
+exports[`renders components/message/demo/other.tsx correctly 1`] = `
+
+
+
+
+ Success
+
+
+
+
+
+
+ Error
+
+
+
+
+
+
+ Warning
+
+
+
+
+`;
+
+exports[`renders components/message/demo/render-panel.tsx correctly 1`] = `
+
+
+
+
+
+
+
+
+
+ Hello World!
+
+
+
+
+`;
+
+exports[`renders components/message/demo/thenable.tsx correctly 1`] = `
+
+
+ Display sequential messages
+
+
+`;
+
+exports[`renders components/message/demo/update.tsx correctly 1`] = `
+
+
+ Open the message box
+
+
+`;
diff --git a/packages/meta/src/message/__tests__/config.test.tsx b/packages/meta/src/message/__tests__/config.test.tsx
new file mode 100644
index 0000000..62bfc20
--- /dev/null
+++ b/packages/meta/src/message/__tests__/config.test.tsx
@@ -0,0 +1,221 @@
+import message, { actWrapper } from '..';
+import { act } from '../../../tests/utils';
+import ConfigProvider from '../../config-provider';
+import { awaitPromise, triggerMotionEnd } from './util';
+
+describe('message.config', () => {
+ beforeAll(() => {
+ actWrapper(act);
+ });
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(async () => {
+ // Clean up
+ message.destroy();
+ await triggerMotionEnd();
+
+ jest.useRealTimers();
+
+ await awaitPromise();
+ });
+
+ it('should be able to config top', async () => {
+ message.config({
+ top: 100,
+ });
+
+ message.info('whatever');
+ await awaitPromise();
+
+ expect(document.querySelector('.ant-message')).toHaveStyle({
+ top: '100px',
+ });
+ });
+
+ it('should be able to config rtl', async () => {
+ message.config({
+ rtl: true,
+ });
+
+ message.info('whatever');
+ await awaitPromise();
+
+ expect(document.querySelector('.ant-message-rtl')).toBeTruthy();
+ });
+
+ it('should be able to config getContainer', async () => {
+ const div = document.createElement('div');
+ div.className = 'custom-container';
+ document.body.appendChild(div);
+
+ message.config({
+ getContainer: () => div,
+ });
+
+ message.info('whatever');
+ await awaitPromise();
+
+ expect(div.querySelector('.ant-message')).toBeTruthy();
+
+ message.config({
+ getContainer: undefined,
+ });
+
+ document.body.removeChild(div);
+ });
+
+ it('should be able to config maxCount', async () => {
+ message.config({
+ maxCount: 5,
+ });
+ for (let i = 0; i < 10; i += 1) {
+ message.info('test');
+ }
+
+ message.info('last');
+ await awaitPromise();
+
+ const noticeWithoutLeaving = Array.from(
+ document.querySelectorAll('.ant-message-notice-wrapper'),
+ ).filter((ele) => !ele.classList.contains('ant-message-move-up-leave'));
+
+ expect(noticeWithoutLeaving).toHaveLength(5);
+ expect(noticeWithoutLeaving[4].textContent).toEqual('last');
+
+ await triggerMotionEnd();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+
+ message.config({
+ maxCount: undefined,
+ });
+ });
+
+ it('should be able to config duration', async () => {
+ message.config({
+ duration: 5,
+ });
+
+ message.info('last');
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+
+ act(() => {
+ jest.advanceTimersByTime(4000);
+ });
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+
+ act(() => {
+ jest.advanceTimersByTime(2000);
+ });
+
+ await triggerMotionEnd('.ant-message-notice-wrapper');
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+
+ message.config({
+ duration: undefined,
+ });
+ });
+
+ it('customize prefix should auto get transition prefixCls', async () => {
+ message.config({
+ prefixCls: 'light-message',
+ });
+
+ message.info('bamboo');
+ await awaitPromise();
+
+ expect(document.querySelector('.light-message-move-up')).toBeTruthy();
+
+ message.config({
+ prefixCls: undefined,
+ });
+ });
+
+ it('should be able to global config rootPrefixCls', async () => {
+ ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
+
+ message.info('last');
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ expect(document.querySelectorAll('.prefix-test-message-notice')).toHaveLength(1);
+ expect(document.querySelectorAll('.bamboo-info-circle')).toHaveLength(1);
+ ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null! });
+ });
+
+ it('should be able to config prefixCls', async () => {
+ message.config({
+ prefixCls: 'prefix-test',
+ });
+
+ message.info('last');
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
+ message.config({
+ prefixCls: '', // can be set to empty, ant default value is set in ConfigProvider
+ });
+ });
+
+ it('should be able to config transitionName', async () => {
+ message.config({
+ transitionName: '',
+ });
+
+ message.info('last');
+ await awaitPromise();
+
+ expect(document.querySelector('.ant-message-notice')).toBeTruthy();
+ expect(document.querySelectorAll('.ant-move-up-enter')).toHaveLength(0);
+ message.config({
+ transitionName: undefined,
+ });
+ });
+
+ it('should be able to config getContainer, although messageInstance already exists', async () => {
+ function createContainer(): [HTMLElement, VoidFunction] {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ return [
+ container,
+ () => {
+ document.body.removeChild(container);
+ },
+ ];
+ }
+ const [container1, removeContainer1] = createContainer();
+ const [container2, removeContainer2] = createContainer();
+ expect(container1.querySelector('.ant-message-notice')).toBeFalsy();
+ expect(container2.querySelector('.ant-message-notice')).toBeFalsy();
+
+ message.config({
+ getContainer: () => container1,
+ });
+ const messageText1 = 'mounted in container1';
+
+ message.info(messageText1);
+ await awaitPromise();
+ expect(container1.querySelector('.ant-message-notice')!.textContent).toEqual(messageText1);
+
+ // Config will directly change container
+ message.config({
+ getContainer: () => container2,
+ });
+ const messageText2 = 'mounted in container2';
+
+ message.info(messageText2);
+ expect(container2.querySelectorAll('.ant-message-notice')[1]!.textContent).toEqual(
+ messageText2,
+ );
+
+ removeContainer1();
+ removeContainer2();
+ });
+});
diff --git a/packages/meta/src/message/__tests__/demo-extend.test.ts b/packages/meta/src/message/__tests__/demo-extend.test.ts
new file mode 100644
index 0000000..54ac135
--- /dev/null
+++ b/packages/meta/src/message/__tests__/demo-extend.test.ts
@@ -0,0 +1,3 @@
+import { extendTest } from '../../../tests/shared/demoTest';
+
+extendTest('message');
diff --git a/packages/meta/src/message/__tests__/demo.test.ts b/packages/meta/src/message/__tests__/demo.test.ts
new file mode 100644
index 0000000..707bf8f
--- /dev/null
+++ b/packages/meta/src/message/__tests__/demo.test.ts
@@ -0,0 +1,6 @@
+import demoTest from '../../../tests/shared/demoTest';
+
+demoTest('message', {
+ testRootProps: false,
+ nameCheckPathOnly: true,
+});
diff --git a/packages/meta/src/message/__tests__/hooks.test.tsx b/packages/meta/src/message/__tests__/hooks.test.tsx
new file mode 100644
index 0000000..d5ba98c
--- /dev/null
+++ b/packages/meta/src/message/__tests__/hooks.test.tsx
@@ -0,0 +1,302 @@
+/* eslint-disable jsx-a11y/control-has-associated-label */
+import React, { useEffect } from 'react';
+import { act } from 'react-dom/test-utils';
+import { StyleProvider, createCache, extractStyle } from '@ant-design/cssinjs';
+import message from '..';
+import { fireEvent, render } from '../../../tests/utils';
+import ConfigProvider from '../../config-provider';
+import { triggerMotionEnd } from './util';
+
+describe('message.hooks', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('should work', () => {
+ const Context = React.createContext('light');
+
+ const Demo: React.FC = () => {
+ const [api, holder] = message.useMessage();
+
+ return (
+
+
+ {
+ api.open({
+ duration: 0,
+ content: (
+
+ {(name) => {name} }
+
+ ),
+ });
+ }}
+ >
+ test
+
+ {holder}
+
+
+ );
+ };
+
+ const { container } = render( );
+ fireEvent.click(container.querySelector('button')!);
+ expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
+ expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
+ });
+
+ it('should work with success', () => {
+ const Context = React.createContext('light');
+
+ const Demo: React.FC = () => {
+ const [api, holder] = message.useMessage();
+
+ return (
+
+
+ {
+ api.success({
+ duration: 0,
+ content: (
+
+ {(name) => {name} }
+
+ ),
+ });
+ }}
+ >
+ test
+
+ {holder}
+
+
+ );
+ };
+
+ const { container } = render( );
+ fireEvent.click(container.querySelector('button')!);
+ expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
+ expect(document.querySelectorAll('.anticon-check-circle')).toHaveLength(1);
+ expect(document.querySelector('.hook-test-result')!.textContent).toEqual('bamboo');
+ });
+
+ it('should work with onClose', (done) => {
+ const Demo = () => {
+ const [api, holder] = message.useMessage();
+ return (
+ <>
+ {
+ api.open({ content: 'amazing', duration: 1, onClose: done });
+ }}
+ >
+ test
+
+ {holder}
+ >
+ );
+ };
+
+ const { container } = render( );
+ fireEvent.click(container.querySelector('button')!);
+
+ triggerMotionEnd();
+ });
+
+ it('should work with close promise', (done) => {
+ const Demo = () => {
+ const [api, holder] = message.useMessage();
+ return (
+ <>
+ {
+ api.open({ content: 'good', duration: 1 }).then(() => {
+ done();
+ });
+ }}
+ >
+ test
+
+ {holder}
+ >
+ );
+ };
+
+ const { container } = render( );
+ fireEvent.click(container.querySelector('button')!);
+
+ triggerMotionEnd();
+ });
+
+ it('should work with hide', async () => {
+ let hide: VoidFunction;
+ const Demo = () => {
+ const [api, holder] = message.useMessage();
+ return (
+
+ {
+ hide = api.open({ content: 'nice', duration: 0 });
+ }}
+ >
+ test
+
+ {holder}
+
+ );
+ };
+
+ const { container } = render( );
+ fireEvent.click(container.querySelector('button')!);
+
+ expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
+
+ act(() => {
+ hide!();
+ });
+ await triggerMotionEnd('.my-test-message-move-up-leave');
+
+ expect(document.querySelectorAll('.my-test-message-notice')).toHaveLength(0);
+ });
+
+ it('should be same hook', () => {
+ let cacheAPI: any;
+
+ const Demo: React.FC = () => {
+ const [, forceUpdate] = React.useState([]);
+ const [api] = message.useMessage();
+ React.useEffect(() => {
+ if (!cacheAPI) {
+ cacheAPI = api;
+ } else {
+ expect(cacheAPI).toBe(api);
+ }
+
+ forceUpdate([]);
+ }, [api]);
+
+ return null;
+ };
+
+ render( );
+ });
+
+ it("should use ConfigProvider's getPopupContainer as message container", () => {
+ const containerId = 'container';
+ const div = document.createElement('div');
+ div.id = containerId;
+ document.body.appendChild(div);
+
+ const getPopupContainer = () => div;
+
+ const Demo = () => {
+ const [api, holder] = message.useMessage();
+ return (
+
+ {holder}
+ {
+ api.success({
+ duration: 0,
+ content: happy ,
+ });
+ }}
+ >
+ test
+
+
+ );
+ };
+
+ const { container } = render( );
+ fireEvent.click(container.querySelector('button')!);
+
+ expect(div.querySelectorAll('.my-test-message-notice')).toHaveLength(1);
+ expect(div.querySelectorAll('.anticon-check-circle')).toHaveLength(1);
+ expect(div.querySelector('.hook-content')!.textContent).toEqual('happy');
+ expect(document.querySelectorAll(`#${containerId}`)).toHaveLength(1);
+ });
+
+ it('warning if user call update in render', () => {
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ const Demo = () => {
+ const [api, holder] = message.useMessage();
+ const calledRef = React.useRef(false);
+
+ if (!calledRef.current) {
+ api.info({
+ content:
,
+ });
+ calledRef.current = true;
+ }
+
+ return holder;
+ };
+
+ render( );
+
+ expect(document.querySelector('.bamboo')).toBeFalsy();
+ expect(errorSpy).toHaveBeenCalledWith(
+ 'Warning: [antd: Message] You are calling notice in render which will break in React 18 concurrent mode. Please trigger in effect instead.',
+ );
+
+ errorSpy.mockRestore();
+ });
+
+ it('not export style in SSR', () => {
+ const cache = createCache();
+
+ const Demo = () => {
+ const [, holder] = message.useMessage();
+
+ return {holder} ;
+ };
+
+ render( );
+
+ const styleText = extractStyle(cache, true);
+ expect(styleText).not.toContain('.ant-message');
+ });
+
+ it('component fontSize should work', () => {
+ const Demo = () => {
+ const [api, holder] = message.useMessage();
+
+ useEffect(() => {
+ api.info({
+ content:
,
+ className: 'fontSize',
+ });
+ }, []);
+
+ return (
+
+ {holder}
+
+ );
+ };
+
+ render( );
+
+ const msg = document.querySelector('.fontSize');
+
+ expect(msg).toBeTruthy();
+ expect(msg).toHaveStyle({
+ fontSize: '20px',
+ });
+ });
+});
diff --git a/packages/meta/src/message/__tests__/image.test.ts b/packages/meta/src/message/__tests__/image.test.ts
new file mode 100644
index 0000000..eb57916
--- /dev/null
+++ b/packages/meta/src/message/__tests__/image.test.ts
@@ -0,0 +1,5 @@
+import { imageDemoTest } from '../../../tests/shared/imageTest';
+
+describe('Message image', () => {
+ imageDemoTest('message');
+});
diff --git a/packages/meta/src/message/__tests__/immediately.test.tsx b/packages/meta/src/message/__tests__/immediately.test.tsx
new file mode 100644
index 0000000..6a46031
--- /dev/null
+++ b/packages/meta/src/message/__tests__/immediately.test.tsx
@@ -0,0 +1,59 @@
+import message, { actDestroy, actWrapper } from '..';
+import { act } from '../../../tests/utils';
+import { awaitPromise, triggerMotionEnd } from './util';
+
+describe('call close immediately', () => {
+ beforeAll(() => {
+ actWrapper(act);
+ });
+
+ beforeEach(() => {
+ actDestroy();
+ jest.useFakeTimers();
+ });
+
+ afterEach(async () => {
+ // Clean up
+ message.destroy();
+ await triggerMotionEnd();
+
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ jest.useRealTimers();
+
+ await awaitPromise();
+ });
+
+ it('open', async () => {
+ const closeFn = message.open({
+ content: '',
+ });
+ closeFn();
+
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+
+ // Created close
+ const closeFn2 = message.open({
+ content: 'showed',
+ });
+ await awaitPromise();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+
+ closeFn2();
+ await triggerMotionEnd();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ });
+
+ it('info', async () => {
+ const closeFn = message.info('Message1', 0);
+ closeFn();
+
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ });
+});
diff --git a/packages/meta/src/message/__tests__/index.test.tsx b/packages/meta/src/message/__tests__/index.test.tsx
new file mode 100644
index 0000000..58b91a3
--- /dev/null
+++ b/packages/meta/src/message/__tests__/index.test.tsx
@@ -0,0 +1,237 @@
+import { SmileOutlined } from '@ant-design/icons';
+import React from 'react';
+import message, { actWrapper } from '..';
+import { act, fireEvent, waitFakeTimer } from '../../../tests/utils';
+import { awaitPromise, triggerMotionEnd } from './util';
+
+describe('message', () => {
+ beforeAll(() => {
+ actWrapper(act);
+ });
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(async () => {
+ // Clean up
+ message.destroy();
+ await triggerMotionEnd();
+
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ jest.useRealTimers();
+
+ await awaitPromise();
+ });
+
+ it('should be able to hide manually', async () => {
+ const hide1 = message.info('whatever', 0);
+ const hide2 = message.info('whatever', 0);
+
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
+
+ hide1();
+ await triggerMotionEnd();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+
+ hide2();
+ await triggerMotionEnd();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ });
+
+ it('should be able to remove manually with a unique key', async () => {
+ const key1 = 'key1';
+ const key2 = 'key2';
+
+ message.info({ content: 'Message1', key: 'key1', duration: 0 });
+ message.info({ content: 'Message2', key: 'key2', duration: 0 });
+
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
+
+ message.destroy(key1);
+ await triggerMotionEnd();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+
+ message.destroy(key2);
+ await triggerMotionEnd();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ });
+
+ it('should be able to destroy globally', async () => {
+ message.info('whatever', 0);
+ message.info('whatever', 0);
+
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message')).toHaveLength(1);
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
+
+ message.destroy();
+ await triggerMotionEnd();
+
+ expect(document.querySelectorAll('.ant-message')).toHaveLength(0);
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ });
+
+ it('should not need to use duration argument when using the onClose arguments', async () => {
+ const onClose = jest.fn();
+ const close = message.info('whatever', onClose);
+
+ await awaitPromise();
+
+ close();
+ await triggerMotionEnd();
+
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('should have the default duration when using the onClose arguments', async () => {
+ const onClose = jest.fn();
+
+ message.info('whatever', onClose);
+ await awaitPromise();
+
+ act(() => {
+ jest.advanceTimersByTime(2500);
+ });
+
+ expect(document.querySelector('.ant-message-move-up-leave')).toBeFalsy();
+
+ act(() => {
+ jest.advanceTimersByTime(1000);
+ });
+ expect(document.querySelector('.ant-message-move-up-leave')).toBeTruthy();
+ });
+
+ it('trigger onClick method', async () => {
+ const onClick = jest.fn();
+ message.info({
+ onClick,
+ duration: 0,
+ content: 'message info',
+ });
+
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+ fireEvent.click(document.querySelector('.ant-message-notice')!);
+
+ expect(onClick).toHaveBeenCalled();
+ });
+
+ it('should be called like promise', async () => {
+ const onClose = jest.fn();
+ message.info('whatever').then(onClose);
+ await awaitPromise();
+
+ act(() => {
+ jest.advanceTimersByTime(2500);
+ });
+ expect(onClose).not.toHaveBeenCalled();
+
+ act(() => {
+ jest.advanceTimersByTime(1000);
+ });
+ await waitFakeTimer(); // Wait to let event loop run
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ // https://github.com/ant-design/ant-design/issues/8201
+ it('should hide message correctly', async () => {
+ const hide = message.loading('Action in progress..', 0);
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+
+ hide!();
+ await triggerMotionEnd();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ });
+
+ it('should allow custom icon', async () => {
+ message.open({ content: 'Message', icon: });
+
+ await awaitPromise();
+ expect(document.querySelector('.anticon-smile')).toBeTruthy();
+ });
+
+ it('should have no icon', async () => {
+ message.open({ content: 'Message', icon: });
+
+ await awaitPromise();
+ expect(document.querySelector('.ant-message-notice .anticon')).toBeFalsy();
+ });
+
+ it('should have no icon when not pass icon props', async () => {
+ message.open({ content: 'Message' });
+
+ await awaitPromise();
+ expect(document.querySelector('.ant-message-notice .anticon')).toBeFalsy();
+ });
+
+ // https://github.com/ant-design/ant-design/issues/8201
+ it('should destroy messages correctly', async () => {
+ message.loading('Action in progress1..', 0);
+ message.loading('Action in progress2..', 0);
+ setTimeout(() => message.destroy(), 1000);
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
+
+ await triggerMotionEnd();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ });
+
+ it('should support update message content with a unique key', async () => {
+ const key = 'updatable';
+
+ message.loading({ content: 'Loading...', key });
+ // Testing that content of the message should be updated.
+ setTimeout(() => message.success({ content: 'Loaded', key }), 1000);
+ setTimeout(() => message.destroy(), 3000);
+ await awaitPromise();
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+ act(() => {
+ jest.advanceTimersByTime(1500);
+ });
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+ expect(document.querySelector('.ant-message-move-up-leave')).toBeFalsy();
+
+ await triggerMotionEnd();
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
+ });
+
+ it('update message content with a unique key and cancel manually', async () => {
+ const key = 'updatable';
+
+ const hideLoading = message.loading({ content: 'Loading...', key, duration: 0 });
+ await awaitPromise();
+
+ setTimeout(() => {
+ act(() => {
+ hideLoading();
+ });
+ }, 1000);
+
+ expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
+
+ act(() => {
+ jest.advanceTimersByTime(1500);
+ });
+ expect(document.querySelectorAll('.ant-message-move-up-leave')).toHaveLength(1);
+ });
+
+ it('should not throw error when pass null', async () => {
+ message.error(null);
+ await awaitPromise();
+ });
+});
diff --git a/packages/meta/src/message/__tests__/static-warning.test.tsx b/packages/meta/src/message/__tests__/static-warning.test.tsx
new file mode 100644
index 0000000..007b1e4
--- /dev/null
+++ b/packages/meta/src/message/__tests__/static-warning.test.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import message, { actWrapper } from '..';
+import { act, render, waitFakeTimer } from '../../../tests/utils';
+import ConfigProvider from '../../config-provider';
+import { awaitPromise, triggerMotionEnd } from './util';
+
+describe('message static warning', () => {
+ beforeAll(() => {
+ actWrapper(act);
+ });
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(async () => {
+ // Clean up
+ message.destroy();
+ await triggerMotionEnd();
+
+ jest.useRealTimers();
+
+ await awaitPromise();
+ });
+
+ // Follow test need keep order
+ it('no warning', async () => {
+ const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ message.success({
+ content:
,
+ 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( );
+
+ message.success({
+ content:
,
+ duration: 0,
+ });
+ await waitFakeTimer();
+
+ expect(document.querySelector('.light')).toBeTruthy();
+
+ expect(errSpy).toHaveBeenCalledWith(
+ "Warning: [antd: message] Static function can not consume context like dynamic theme. Please use 'App' component instead.",
+ );
+ });
+});
diff --git a/packages/meta/src/message/__tests__/type.test.tsx b/packages/meta/src/message/__tests__/type.test.tsx
new file mode 100644
index 0000000..fab819d
--- /dev/null
+++ b/packages/meta/src/message/__tests__/type.test.tsx
@@ -0,0 +1,56 @@
+import message, { actWrapper } from '..';
+import { act } from '../../../tests/utils';
+import { awaitPromise, triggerMotionEnd } from './util';
+
+describe('message.typescript', () => {
+ beforeAll(() => {
+ actWrapper(act);
+ });
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(async () => {
+ // Clean up
+ message.destroy();
+ await triggerMotionEnd();
+
+ jest.useRealTimers();
+
+ await awaitPromise();
+ });
+
+ it('promise without arguments', async () => {
+ message.success('yes!!!', 0);
+ await Promise.resolve();
+ });
+
+ it('promise with one arguments', async () => {
+ const filled = jest.fn();
+
+ message.success('yes!!!').then(filled);
+
+ await triggerMotionEnd();
+
+ expect(filled).toHaveBeenCalledWith(true);
+ });
+
+ it('promise two arguments', async () => {
+ const filled = jest.fn();
+ const rejected = jest.fn();
+
+ message.success('yes!!!').then(filled, rejected);
+
+ await triggerMotionEnd();
+
+ expect(filled).toHaveBeenCalledWith(true);
+ expect(rejected).not.toHaveBeenCalled();
+ });
+
+ it('hide', async () => {
+ const hide = message.loading('doing...');
+ await Promise.resolve();
+ hide();
+ });
+});
diff --git a/packages/meta/src/message/__tests__/util.ts b/packages/meta/src/message/__tests__/util.ts
new file mode 100644
index 0000000..aae4bcb
--- /dev/null
+++ b/packages/meta/src/message/__tests__/util.ts
@@ -0,0 +1,25 @@
+import { act, fireEvent } from '../../../tests/utils';
+
+export async function awaitPromise() {
+ for (let i = 0; i < 10; i += 1) {
+ // eslint-disable-next-line no-await-in-loop
+ await Promise.resolve();
+ }
+}
+
+export async function triggerMotionEnd(selector: string = '.ant-message-move-up-leave') {
+ await awaitPromise();
+
+ // Flush css motion state update
+ for (let i = 0; i < 5; i += 1) {
+ act(() => {
+ jest.runAllTimers();
+ });
+ }
+
+ document.querySelectorAll(selector).forEach((ele) => {
+ fireEvent.animationEnd(ele);
+ });
+
+ await awaitPromise();
+}
diff --git a/packages/meta/src/message/demo/component-token.md b/packages/meta/src/message/demo/component-token.md
new file mode 100644
index 0000000..584563d
--- /dev/null
+++ b/packages/meta/src/message/demo/component-token.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+Component Token Debug.
+
+## en-US
+
+Component Token Debug
diff --git a/packages/meta/src/message/demo/component-token.tsx b/packages/meta/src/message/demo/component-token.tsx
new file mode 100644
index 0000000..45ef6a6
--- /dev/null
+++ b/packages/meta/src/message/demo/component-token.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { ConfigProvider, message } from '@zhst/meta';
+
+/** Test usage. Do not use in your production. */
+const { _InternalPanelDoNotUseOrYouWillBeFired: InternalPanel } = message;
+
+export default () => (
+ <>
+
+
+
+
+
+
+ >
+);
diff --git a/packages/meta/src/message/demo/custom-style.md b/packages/meta/src/message/demo/custom-style.md
new file mode 100644
index 0000000..04c8e9c
--- /dev/null
+++ b/packages/meta/src/message/demo/custom-style.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+使用 `style` 和 `className` 来定义样式。
+
+## en-US
+
+The `style` and `className` are available to customize Message.
diff --git a/packages/meta/src/message/demo/custom-style.tsx b/packages/meta/src/message/demo/custom-style.tsx
new file mode 100644
index 0000000..c431a44
--- /dev/null
+++ b/packages/meta/src/message/demo/custom-style.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { Button, message } from '@zhst/meta';
+
+const App: React.FC = () => {
+ const [messageApi, contextHolder] = message.useMessage();
+
+ const success = () => {
+ messageApi.open({
+ type: 'success',
+ content: 'This is a prompt message with custom className and style',
+ className: 'custom-class',
+ style: {
+ marginTop: '20vh',
+ },
+ });
+ };
+
+ return (
+ <>
+ {contextHolder}
+ Customized style
+ >
+ );
+};
+
+export default App;
diff --git a/packages/meta/src/message/demo/duration.md b/packages/meta/src/message/demo/duration.md
new file mode 100644
index 0000000..3522d7e
--- /dev/null
+++ b/packages/meta/src/message/demo/duration.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+自定义时长 `10s`,默认时长为 `3s`。
+
+## en-US
+
+Customize message display duration from default `3s` to `10s`.
diff --git a/packages/meta/src/message/demo/duration.tsx b/packages/meta/src/message/demo/duration.tsx
new file mode 100644
index 0000000..408be7f
--- /dev/null
+++ b/packages/meta/src/message/demo/duration.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { Button, message } from '@zhst/meta';
+
+const App: React.FC = () => {
+ const [messageApi, contextHolder] = message.useMessage();
+
+ const success = () => {
+ messageApi.open({
+ type: 'success',
+ content: 'This is a prompt message for success, and it will disappear in 10 seconds',
+ duration: 10,
+ });
+ };
+
+ return (
+ <>
+ {contextHolder}
+ Customized display duration
+ >
+ );
+};
+
+export default App;
diff --git a/packages/meta/src/message/demo/hooks.md b/packages/meta/src/message/demo/hooks.md
new file mode 100644
index 0000000..757383d
--- /dev/null
+++ b/packages/meta/src/message/demo/hooks.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
+
+## en-US
+
+Use `message.useMessage` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `message` static method, because static method cannot consume context, and ConfigProvider data will not work.
diff --git a/packages/meta/src/message/demo/hooks.tsx b/packages/meta/src/message/demo/hooks.tsx
new file mode 100644
index 0000000..d9c2471
--- /dev/null
+++ b/packages/meta/src/message/demo/hooks.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Button, message } from '@zhst/meta';
+
+const App: React.FC = () => {
+ const [messageApi, contextHolder] = message.useMessage();
+
+ const info = () => {
+ messageApi.info('Hello, Ant Design!');
+ };
+
+ return (
+ <>
+ {contextHolder}
+
+ Display normal message
+
+ >
+ );
+};
+
+export default App;
diff --git a/packages/meta/src/message/demo/info.md b/packages/meta/src/message/demo/info.md
new file mode 100644
index 0000000..b913751
--- /dev/null
+++ b/packages/meta/src/message/demo/info.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+静态方法无法消费 Context,推荐优先使用 Hooks 版本。
+
+## en-US
+
+Static methods cannot consume Context. Please use hooks first.
diff --git a/packages/meta/src/message/demo/info.tsx b/packages/meta/src/message/demo/info.tsx
new file mode 100644
index 0000000..fcb7e28
--- /dev/null
+++ b/packages/meta/src/message/demo/info.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Button, message } from '@zhst/meta';
+
+const info = () => {
+ message.info('This is a normal message');
+};
+
+const App: React.FC = () => (
+
+ Static Method
+
+);
+
+export default App;
diff --git a/packages/meta/src/message/demo/loading.md b/packages/meta/src/message/demo/loading.md
new file mode 100644
index 0000000..2ed153d
--- /dev/null
+++ b/packages/meta/src/message/demo/loading.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+进行全局 loading,异步自行移除。
+
+## en-US
+
+Display a global loading indicator, which is dismissed by itself asynchronously.
diff --git a/packages/meta/src/message/demo/loading.tsx b/packages/meta/src/message/demo/loading.tsx
new file mode 100644
index 0000000..db66e34
--- /dev/null
+++ b/packages/meta/src/message/demo/loading.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Button, message } from '@zhst/meta';
+
+const App: React.FC = () => {
+ const [messageApi, contextHolder] = message.useMessage();
+
+ const success = () => {
+ messageApi.open({
+ type: 'loading',
+ content: 'Action in progress..',
+ duration: 0,
+ });
+ // Dismiss manually and asynchronously
+ setTimeout(messageApi.destroy, 2500);
+ };
+ return (
+ <>
+ {contextHolder}
+ Display a loading indicator
+ >
+ );
+};
+
+export default App;
diff --git a/packages/meta/src/message/demo/other.md b/packages/meta/src/message/demo/other.md
new file mode 100644
index 0000000..1090944
--- /dev/null
+++ b/packages/meta/src/message/demo/other.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+包括成功、失败、警告。
+
+## en-US
+
+Messages of success, error and warning types.
diff --git a/packages/meta/src/message/demo/other.tsx b/packages/meta/src/message/demo/other.tsx
new file mode 100644
index 0000000..6c2b83c
--- /dev/null
+++ b/packages/meta/src/message/demo/other.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { Button, message, Space } from '@zhst/meta';
+
+const App: React.FC = () => {
+ const [messageApi, contextHolder] = message.useMessage();
+
+ const success = () => {
+ messageApi.open({
+ type: 'success',
+ content: 'This is a success message',
+ });
+ };
+
+ const error = () => {
+ messageApi.open({
+ type: 'error',
+ content: 'This is an error message',
+ });
+ };
+
+ const warning = () => {
+ messageApi.open({
+ type: 'warning',
+ content: 'This is a warning message',
+ });
+ };
+
+ return (
+ <>
+ {contextHolder}
+
+ Success
+ Error
+ Warning
+
+ >
+ );
+};
+
+export default App;
diff --git a/packages/meta/src/message/demo/render-panel.md b/packages/meta/src/message/demo/render-panel.md
new file mode 100644
index 0000000..70bcbec
--- /dev/null
+++ b/packages/meta/src/message/demo/render-panel.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+调试用组件,请勿直接使用。
+
+## en-US
+
+Debug usage. Do not use in your production.
diff --git a/packages/meta/src/message/demo/render-panel.tsx b/packages/meta/src/message/demo/render-panel.tsx
new file mode 100644
index 0000000..49740c6
--- /dev/null
+++ b/packages/meta/src/message/demo/render-panel.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { message } from '@zhst/meta';
+
+/** Test usage. Do not use in your production. */
+const { _InternalPanelDoNotUseOrYouWillBeFired: InternalPanel } = message;
+
+export default () => ;
diff --git a/packages/meta/src/message/demo/thenable.md b/packages/meta/src/message/demo/thenable.md
new file mode 100644
index 0000000..465c8a0
--- /dev/null
+++ b/packages/meta/src/message/demo/thenable.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+可以通过 then 接口在关闭后运行 callback 。以上用例将在每个 message 将要结束时通过 then 显示新的 message 。
+
+## en-US
+
+`message` provides a promise interface for `onClose`. The above example will display a new message when the old message is about to close.
diff --git a/packages/meta/src/message/demo/thenable.tsx b/packages/meta/src/message/demo/thenable.tsx
new file mode 100644
index 0000000..afdd6d8
--- /dev/null
+++ b/packages/meta/src/message/demo/thenable.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { Button, message } from '@zhst/meta';
+
+const App: React.FC = () => {
+ const [messageApi, contextHolder] = message.useMessage();
+
+ const success = () => {
+ messageApi
+ .open({
+ type: 'loading',
+ content: 'Action in progress..',
+ duration: 2.5,
+ })
+ .then(() => message.success('Loading finished', 2.5))
+ .then(() => message.info('Loading finished', 2.5));
+ };
+
+ return (
+ <>
+ {contextHolder}
+ Display sequential messages
+ >
+ );
+};
+
+export default App;
diff --git a/packages/meta/src/message/demo/update.md b/packages/meta/src/message/demo/update.md
new file mode 100644
index 0000000..d919d72
--- /dev/null
+++ b/packages/meta/src/message/demo/update.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+可以通过唯一的 `key` 来更新内容。
+
+## en-US
+
+Update message content with unique `key`.
diff --git a/packages/meta/src/message/demo/update.tsx b/packages/meta/src/message/demo/update.tsx
new file mode 100644
index 0000000..e347090
--- /dev/null
+++ b/packages/meta/src/message/demo/update.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { Button, message } from '@zhst/meta';
+
+const App: React.FC = () => {
+ const [messageApi, contextHolder] = message.useMessage();
+ const key = 'updatable';
+
+ const openMessage = () => {
+ messageApi.open({
+ key,
+ type: 'loading',
+ content: 'Loading...',
+ });
+ setTimeout(() => {
+ messageApi.open({
+ key,
+ type: 'success',
+ content: 'Loaded!',
+ duration: 2,
+ });
+ }, 1000);
+ };
+
+ return (
+ <>
+ {contextHolder}
+
+ Open the message box
+
+ >
+ );
+};
+
+export default App;
diff --git a/packages/meta/src/message/index.tsx b/packages/meta/src/message/index.tsx
new file mode 100644
index 0000000..eb25f6b
--- /dev/null
+++ b/packages/meta/src/message/index.tsx
@@ -0,0 +1,356 @@
+import * as React from 'react';
+import { render } from 'rc-util/lib/React/render';
+
+import ConfigProvider, { globalConfig, warnContext } from '../config-provider';
+import type {
+ ArgsProps,
+ ConfigOptions,
+ MessageInstance,
+ MessageType,
+ NoticeType,
+ TypeOpen,
+} from './interface';
+import PurePanel from './PurePanel';
+import useMessage, { useInternalMessage } from './useMessage';
+import { wrapPromiseFn } from './util';
+
+export type { ArgsProps };
+
+let message: GlobalMessage | null = null;
+
+let act: (callback: VoidFunction) => Promise | void = (callback) => callback();
+
+interface GlobalMessage {
+ fragment: DocumentFragment;
+ instance?: MessageInstance | null;
+ sync?: VoidFunction;
+}
+
+interface OpenTask {
+ type: 'open';
+ config: ArgsProps;
+ resolve: VoidFunction;
+ setCloseFn: (closeFn: VoidFunction) => void;
+ skipped?: boolean;
+}
+
+interface TypeTask {
+ type: NoticeType;
+ args: Parameters;
+ resolve: VoidFunction;
+ setCloseFn: (closeFn: VoidFunction) => void;
+ skipped?: boolean;
+}
+
+type Task =
+ | OpenTask
+ | TypeTask
+ | {
+ type: 'destroy';
+ key: React.Key;
+ skipped?: boolean;
+ };
+
+let taskQueue: Task[] = [];
+
+let defaultGlobalConfig: ConfigOptions = {};
+
+function getGlobalContext() {
+ const {
+ prefixCls: globalPrefixCls,
+ getContainer: globalGetContainer,
+ duration,
+ rtl,
+ maxCount,
+ top,
+ } = defaultGlobalConfig;
+ const mergedPrefixCls = globalPrefixCls ?? globalConfig().getPrefixCls('message');
+ const mergedContainer = globalGetContainer?.() || document.body;
+
+ return {
+ prefixCls: mergedPrefixCls,
+ getContainer: () => mergedContainer!,
+ duration,
+ rtl,
+ maxCount,
+ top,
+ };
+}
+
+interface GlobalHolderRef {
+ instance: MessageInstance;
+ sync: () => void;
+}
+
+const GlobalHolder = React.forwardRef((_, ref) => {
+ const [messageConfig, setMessageConfig] = React.useState(getGlobalContext);
+
+ const [api, holder] = useInternalMessage(messageConfig);
+
+ const global = globalConfig();
+ const rootPrefixCls = global.getRootPrefixCls();
+ const rootIconPrefixCls = global.getIconPrefixCls();
+ const theme = global.getTheme();
+
+ const sync = () => {
+ setMessageConfig(getGlobalContext);
+ };
+
+ React.useEffect(sync, []);
+
+ React.useImperativeHandle(ref, () => {
+ const instance: MessageInstance = { ...api };
+ // @ts-ignore
+ Object.keys(instance).forEach((method: keyof MessageInstance) => {
+ instance[method] = (...args: any[]) => {
+ sync();
+ return (api as any)[method](...args);
+ };
+ });
+
+ return {
+ instance,
+ sync,
+ };
+ });
+
+ return (
+
+ {holder}
+
+ );
+});
+
+function flushNotice() {
+ if (!message) {
+ const holderFragment = document.createDocumentFragment();
+
+ const newMessage: GlobalMessage = {
+ fragment: holderFragment,
+ };
+
+ message = newMessage;
+
+ // Delay render to avoid sync issue
+ act(() => {
+ render(
+ {
+ const { instance, sync } = node || {};
+
+ // React 18 test env will throw if call immediately in ref
+ Promise.resolve().then(() => {
+ if (!newMessage.instance && instance) {
+ newMessage.instance = instance;
+ newMessage.sync = sync;
+ flushNotice();
+ }
+ });
+ }}
+ />,
+ holderFragment,
+ );
+ });
+
+ return;
+ }
+
+ // Notification not ready
+ if (!message.instance) {
+ return;
+ }
+
+ // >>> Execute task
+ taskQueue.forEach((task) => {
+ const { type, skipped } = task;
+
+ // Only `skipped` when user call notice but cancel it immediately
+ // and instance not ready
+ if (!skipped) {
+ switch (type) {
+ case 'open': {
+ act(() => {
+ const closeFn = message!.instance!.open({
+ ...defaultGlobalConfig,
+ ...task.config,
+ });
+
+ closeFn?.then(task.resolve);
+ task.setCloseFn(closeFn);
+ });
+ break;
+ }
+
+ case 'destroy':
+ act(() => {
+ message?.instance!.destroy(task.key);
+ });
+ break;
+
+ // Other type open
+ default: {
+ act(() => {
+ const closeFn = message!.instance;
+
+ closeFn?.then(task.resolve);
+ task.setCloseFn(closeFn);
+ });
+ }
+ }
+ }
+ });
+
+ // Clean up
+ taskQueue = [];
+}
+
+// ==============================================================================
+// == Export ==
+// ==============================================================================
+
+function setMessageGlobalConfig(config: ConfigOptions) {
+ defaultGlobalConfig = {
+ ...defaultGlobalConfig,
+ ...config,
+ };
+
+ // Trigger sync for it
+ act(() => {
+ message?.sync?.();
+ });
+}
+
+function open(config: ArgsProps): MessageType {
+ const result = wrapPromiseFn((resolve) => {
+ let closeFn: VoidFunction;
+
+ const task: OpenTask = {
+ type: 'open',
+ config,
+ resolve,
+ setCloseFn: (fn) => {
+ closeFn = fn;
+ },
+ };
+ taskQueue.push(task);
+
+ return () => {
+ if (closeFn) {
+ act(() => {
+ closeFn();
+ });
+ } else {
+ task.skipped = true;
+ }
+ };
+ });
+
+ flushNotice();
+
+ return result;
+}
+
+function typeOpen(type: NoticeType, args: Parameters): MessageType {
+ // Warning if exist theme
+ if (process.env.NODE_ENV !== 'production') {
+ warnContext('message');
+ }
+
+ const result = wrapPromiseFn((resolve) => {
+ let closeFn: VoidFunction;
+
+ const task: TypeTask = {
+ type,
+ args,
+ resolve,
+ setCloseFn: (fn) => {
+ closeFn = fn;
+ },
+ };
+
+ taskQueue.push(task);
+
+ return () => {
+ if (closeFn) {
+ act(() => {
+ closeFn();
+ });
+ } else {
+ task.skipped = true;
+ }
+ };
+ });
+
+ flushNotice();
+
+ return result;
+}
+
+function destroy(key: React.Key) {
+ taskQueue.push({
+ type: 'destroy',
+ key,
+ });
+ flushNotice();
+}
+
+interface BaseMethods {
+ open: (config: ArgsProps) => MessageType;
+ destroy: (key?: React.Key) => void;
+ config: typeof setMessageGlobalConfig;
+ useMessage: typeof useMessage;
+ /** @private Internal Component. Do not use in your production. */
+ _InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel;
+}
+
+interface MessageMethods {
+ info: TypeOpen;
+ success: TypeOpen;
+ error: TypeOpen;
+ warning: TypeOpen;
+ loading: TypeOpen;
+}
+
+const methods: (keyof MessageMethods)[] = ['success', 'info', 'warning', 'error', 'loading'];
+
+const baseStaticMethods: BaseMethods = {
+ open,
+// @ts-ignore
+ destroy,
+ config: setMessageGlobalConfig,
+ useMessage,
+ _InternalPanelDoNotUseOrYouWillBeFired: PurePanel,
+};
+
+const staticMethods = baseStaticMethods as MessageMethods & BaseMethods;
+
+methods.forEach((type: keyof MessageMethods) => {
+ staticMethods[type] = (...args: Parameters) => typeOpen(type, args);
+});
+
+// ==============================================================================
+// == 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;
+ };
+}
+
+/** @internal Only Work in test env */
+// eslint-disable-next-line import/no-mutable-exports
+export let actDestroy = noop;
+
+if (process.env.NODE_ENV === 'test') {
+ actDestroy = () => {
+ message = null;
+ };
+}
+
+export default staticMethods;
diff --git a/packages/meta/src/message/index.zh-CN.md b/packages/meta/src/message/index.zh-CN.md
new file mode 100644
index 0000000..e5a5d22
--- /dev/null
+++ b/packages/meta/src/message/index.zh-CN.md
@@ -0,0 +1,143 @@
+---
+category: Components
+subtitle: 全局提示
+group: 反馈
+noinstant: true
+title: Message 全局提示
+demo:
+ cols: 2
+---
+
+全局展示操作反馈信息。
+
+## 何时使用
+
+- 可提供成功、警告和错误等反馈信息。
+- 顶部居中显示并自动消失,是一种不打断用户操作的轻量级提示方式。
+
+## 代码演示
+
+
+Hooks 调用(推荐)
+其他提示类型
+修改延时
+加载中
+Promise 接口
+自定义样式
+更新消息内容
+静态方法(不推荐)
+_InternalPanelDoNotUseOrYouWillBeFired
+组件 Token
+
+## API
+
+通用属性参考:[通用属性](/docs/react/common-props)
+
+组件提供了一些静态方法,使用方式和参数如下:
+
+- `message.success(content, [duration], onClose)`
+- `message.error(content, [duration], onClose)`
+- `message.info(content, [duration], onClose)`
+- `message.warning(content, [duration], onClose)`
+- `message.loading(content, [duration], onClose)`
+
+| 参数 | 说明 | 类型 | 默认值 |
+| -------- | ------------------------------------------- | ------------------- | ------ |
+| content | 提示内容 | ReactNode \| config | - |
+| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭 | number | 3 |
+| onClose | 关闭时触发的回调函数 | function | - |
+
+组件同时提供 promise 接口。
+
+- `message[level](content, [duration]).then(afterClose)`
+- `message[level](content, [duration], onClose).then(afterClose)`
+
+其中 `message[level]` 是组件已经提供的静态方法。`then` 接口返回值是 Promise。
+
+也可以对象的形式传递参数:
+
+- `message.open(config)`
+- `message.success(config)`
+- `message.error(config)`
+- `message.info(config)`
+- `message.warning(config)`
+- `message.loading(config)`
+
+`config` 对象属性如下:
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| className | 自定义 CSS class | string | - |
+| content | 提示内容 | ReactNode | - |
+| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭 | number | 3 |
+| icon | 自定义图标 | ReactNode | - |
+| key | 当前提示的唯一标志 | string \| number | - |
+| style | 自定义内联样式 | [CSSProperties](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e434515761b36830c3e58a970abf5186f005adac/types/react/index.d.ts#L794) | - |
+| onClick | 点击 message 时触发的回调函数 | function | - |
+| onClose | 关闭时触发的回调函数 | function | - |
+
+### 全局方法
+
+还提供了全局配置和全局销毁方法:
+
+- `message.config(options)`
+- `message.destroy()`
+
+> 也可通过 `message.destroy(key)` 来关闭一条消息。
+
+#### message.config
+
+> 当你使用 `ConfigProvider` 进行全局化配置时,系统会默认自动开启 RTL 模式。(4.3.0+)
+>
+> 当你想单独使用,可通过如下设置开启 RTL 模式。
+
+```js
+message.config({
+ top: 100,
+ duration: 2,
+ maxCount: 3,
+ rtl: true,
+ prefixCls: 'my-message',
+});
+```
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| duration | 默认自动关闭延时,单位秒 | number | 3 | |
+| getContainer | 配置渲染节点的输出位置,但依旧为全屏展示 | () => HTMLElement | () => document.body | |
+| maxCount | 最大显示数,超过限制时,最早的消息会被自动关闭 | number | - | |
+| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 4.5.0 |
+| rtl | 是否开启 RTL 模式 | boolean | false | |
+| top | 消息距离顶部的位置 | number | 8 | |
+
+## 主题变量(Design Token)
+
+## FAQ
+
+### 为什么 message 不能获取 context、redux 的内容和 ConfigProvider 的 `locale/prefixCls/theme` 等配置?
+
+直接调用 message 方法,antd 会通过 `ReactDOM.render` 动态创建新的 React 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
+
+当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `message.useMessage` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
+
+```js
+const [api, contextHolder] = message.useMessage();
+
+return (
+
+ {/* contextHolder 在 Context1 内,它可以获得 Context1 的 context */}
+ {contextHolder}
+
+ {/* contextHolder 在 Context2 外,因而不会获得 Context2 的 context */}
+
+
+);
+```
+
+**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
+
+> 可通过 [App 包裹组件](/components/app-cn) 简化 `useMessage` 等方法需要手动植入 contextHolder 的问题。
+
+### 静态方法如何设置 prefixCls ?
+
+你可以通过 [`ConfigProvider.config`](/components/config-provider-cn#configproviderconfig-4130) 进行设置。
diff --git a/packages/meta/src/message/interface.ts b/packages/meta/src/message/interface.ts
new file mode 100644
index 0000000..78a9bd0
--- /dev/null
+++ b/packages/meta/src/message/interface.ts
@@ -0,0 +1,47 @@
+import type * as React from 'react';
+
+export type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
+
+export interface ConfigOptions {
+ top?: number;
+ duration?: number;
+ prefixCls?: string;
+ getContainer?: () => HTMLElement;
+ transitionName?: string;
+ maxCount?: number;
+ rtl?: boolean;
+}
+
+export interface ArgsProps {
+ content: React.ReactNode;
+ duration?: number;
+ type?: NoticeType;
+ onClose?: () => void;
+ icon?: React.ReactNode;
+ key?: string | number;
+ style?: React.CSSProperties;
+ className?: string;
+ onClick?: (e: React.MouseEvent) => void;
+}
+
+export type JointContent = React.ReactNode | ArgsProps;
+
+export interface MessageType extends PromiseLike {
+ (): void;
+}
+
+export type TypeOpen = (
+ content: JointContent,
+ duration?: number | VoidFunction, // Also can use onClose directly
+ onClose?: VoidFunction,
+) => MessageType;
+
+export interface MessageInstance {
+ info: TypeOpen;
+ success: TypeOpen;
+ error: TypeOpen;
+ warning: TypeOpen;
+ loading: TypeOpen;
+ open(args: ArgsProps): MessageType;
+ destroy(key?: React.Key): void;
+}
diff --git a/packages/meta/src/message/style/index.ts b/packages/meta/src/message/style/index.ts
new file mode 100644
index 0000000..94c8264
--- /dev/null
+++ b/packages/meta/src/message/style/index.ts
@@ -0,0 +1,213 @@
+// deps-lint-skip-all
+import type { CSSProperties } from 'react';
+import type { CSSObject } from '@ant-design/cssinjs';
+import { Keyframes } from '@ant-design/cssinjs';
+
+import { CONTAINER_MAX_OFFSET } from '../../_util/hooks/useZIndex';
+import { resetComponent } from '../../style';
+import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
+import { genStyleHooks, mergeToken } from '../../theme/internal';
+
+/** Component only token. Which will handle additional calculation of alias token */
+export interface ComponentToken {
+ // Component token here
+ /**
+ * @desc 提示框 z-index
+ * @descEN z-index of Message
+ */
+ zIndexPopup: number;
+ /**
+ * @desc 提示框背景色
+ * @descEN Background color of Message
+ */
+ contentBg: string;
+ /**
+ * @desc 提示框内边距
+ * @descEN Padding of Message
+ */
+ contentPadding: CSSProperties['padding'];
+}
+// @ts-ignore
+interface MessageToken extends FullToken<'Message'> {
+ // Custom token here
+ height: number;
+}
+
+const genMessageStyle: GenerateStyle = (token) => {
+ const {
+ componentCls,
+ iconCls,
+ boxShadow,
+ colorText,
+ colorSuccess,
+ colorError,
+ colorWarning,
+ colorInfo,
+ fontSizeLG,
+ motionEaseInOutCirc,
+ motionDurationSlow,
+ marginXS,
+ paddingXS,
+ borderRadiusLG,
+ // @ts-ignore
+ zIndexPopup,
+ // Custom token
+ // @ts-ignore
+ contentPadding,
+ // @ts-ignore
+ contentBg,
+ } = token;
+
+ const noticeCls = `${componentCls}-notice`;
+
+ const messageMoveIn = new Keyframes('MessageMoveIn', {
+ '0%': {
+ padding: 0,
+ transform: 'translateY(-100%)',
+ opacity: 0,
+ },
+
+ '100%': {
+ padding: paddingXS,
+ transform: 'translateY(0)',
+ opacity: 1,
+ },
+ });
+
+ const messageMoveOut = new Keyframes('MessageMoveOut', {
+ '0%': {
+ maxHeight: token.height,
+ padding: paddingXS,
+ opacity: 1,
+ },
+ '100%': {
+ maxHeight: 0,
+ padding: 0,
+ opacity: 0,
+ },
+ });
+
+ const noticeStyle: CSSObject = {
+ padding: paddingXS,
+ textAlign: 'center',
+
+ [`${componentCls}-custom-content > ${iconCls}`]: {
+ verticalAlign: 'text-bottom',
+ marginInlineEnd: marginXS, // affected by ltr or rtl
+ fontSize: fontSizeLG,
+ },
+
+ [`${noticeCls}-content`]: {
+ display: 'inline-block',
+ padding: contentPadding,
+ background: contentBg,
+ borderRadius: borderRadiusLG,
+ boxShadow,
+ pointerEvents: 'all',
+ },
+
+ [`${componentCls}-success > ${iconCls}`]: {
+ color: colorSuccess,
+ },
+ [`${componentCls}-error > ${iconCls}`]: {
+ color: colorError,
+ },
+ [`${componentCls}-warning > ${iconCls}`]: {
+ color: colorWarning,
+ },
+ [`${componentCls}-info > ${iconCls},
+ ${componentCls}-loading > ${iconCls}`]: {
+ color: colorInfo,
+ },
+ };
+
+ return [
+ // ============================ Holder ============================
+ {
+ [componentCls]: {
+ ...resetComponent(token),
+ color: colorText,
+ position: 'fixed',
+ top: marginXS,
+ width: '100%',
+ pointerEvents: 'none',
+ zIndex: zIndexPopup,
+
+ [`${componentCls}-move-up`]: {
+ animationFillMode: 'forwards',
+ },
+ [`
+ ${componentCls}-move-up-appear,
+ ${componentCls}-move-up-enter
+ `]: {
+ animationName: messageMoveIn,
+ animationDuration: motionDurationSlow,
+ animationPlayState: 'paused',
+ animationTimingFunction: motionEaseInOutCirc,
+ },
+ [`
+ ${componentCls}-move-up-appear${componentCls}-move-up-appear-active,
+ ${componentCls}-move-up-enter${componentCls}-move-up-enter-active
+ `]: {
+ animationPlayState: 'running',
+ },
+ [`${componentCls}-move-up-leave`]: {
+ animationName: messageMoveOut,
+ animationDuration: motionDurationSlow,
+ animationPlayState: 'paused',
+ animationTimingFunction: motionEaseInOutCirc,
+ },
+ [`${componentCls}-move-up-leave${componentCls}-move-up-leave-active`]: {
+ animationPlayState: 'running',
+ },
+ '&-rtl': {
+ direction: 'rtl',
+ span: {
+ direction: 'rtl',
+ },
+ },
+ },
+ },
+
+ // ============================ Notice ============================
+ {
+ [componentCls]: {
+ [`${noticeCls}-wrapper`]: {
+ ...noticeStyle,
+ },
+ },
+ },
+
+ // ============================= Pure =============================
+ {
+ [`${componentCls}-notice-pure-panel`]: {
+ ...noticeStyle,
+ padding: 0,
+ textAlign: 'start',
+ },
+ },
+ ];
+};
+// @ts-ignore
+export const prepareComponentToken: GetDefaultToken<'Message'> = (token) => ({
+ zIndexPopup: token.zIndexPopupBase + CONTAINER_MAX_OFFSET + 10,
+ contentBg: token.colorBgElevated,
+ contentPadding: `${(token.controlHeightLG - token.fontSize * token.lineHeight) / 2}px ${
+ token.paddingSM
+ }px`,
+});
+
+// ============================== Export ==============================
+// @ts-ignore
+export default genStyleHooks(
+ // @ts-ignore
+ 'Message',
+ (token) => {
+ // Gen-style functions here
+ const combinedToken = mergeToken(token, {
+ height: 150,
+ });
+ return [genMessageStyle(combinedToken)];
+ },
+ prepareComponentToken,
+);
diff --git a/packages/meta/src/message/useMessage.tsx b/packages/meta/src/message/useMessage.tsx
new file mode 100644
index 0000000..f29dbf8
--- /dev/null
+++ b/packages/meta/src/message/useMessage.tsx
@@ -0,0 +1,250 @@
+import * as React from 'react';
+import type { FC, PropsWithChildren } from 'react';
+import CloseOutlined from '@ant-design/icons/CloseOutlined';
+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,
+ ConfigOptions,
+ MessageInstance,
+ MessageType,
+ NoticeType,
+ TypeOpen,
+} from './interface';
+import { PureContent } from './PurePanel';
+import useStyle from './style';
+import { getMotion, wrapPromiseFn } from './util';
+import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
+
+const DEFAULT_OFFSET = 8;
+const DEFAULT_DURATION = 3;
+
+// ==============================================================================
+// == Holder ==
+// ==============================================================================
+type HolderProps = ConfigOptions & {
+ onAllRemoved?: VoidFunction;
+};
+
+interface HolderRef extends NotificationAPI {
+ prefixCls: string;
+ message?: ComponentStyleConfig;
+}
+
+const Wrapper: FC> = ({ children, prefixCls }) => {
+ const rootCls = useCSSVarCls(prefixCls);
+ const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
+ return wrapCSSVar(
+
+ {children}
+ ,
+ );
+};
+
+const renderNotifications: RcNotificationConfig['renderNotifications'] = (
+ node,
+ { prefixCls, key },
+) => (
+
+ {node}
+
+);
+
+const Holder = React.forwardRef((props, ref) => {
+ const {
+ top,
+ prefixCls: staticPrefixCls,
+ getContainer: staticGetContainer,
+ maxCount,
+ duration = DEFAULT_DURATION,
+ rtl,
+ transitionName,
+ onAllRemoved,
+ } = props;
+ const { getPrefixCls, getPopupContainer, message } = React.useContext(ConfigContext);
+
+ const prefixCls = staticPrefixCls || getPrefixCls('message');
+
+ // =============================== Style ===============================
+ const getStyle = (): React.CSSProperties => ({
+ left: '50%',
+ transform: 'translateX(-50%)',
+ top: top ?? DEFAULT_OFFSET,
+ });
+
+ const getClassName = () => classNames({ [`${prefixCls}-rtl`]: rtl });
+
+ // ============================== Motion ===============================
+ const getNotificationMotion = () => getMotion(prefixCls, transitionName);
+
+ // ============================ Close Icon =============================
+ const mergedCloseIcon = (
+
+
+
+ );
+
+ // ============================== Origin ===============================
+ const [api, holder] = useRcNotification({
+ prefixCls,
+ style: getStyle,
+ className: getClassName,
+ motion: getNotificationMotion,
+ closable: false,
+ closeIcon: mergedCloseIcon,
+ duration,
+ getContainer: () => staticGetContainer?.() || getPopupContainer?.() || document.body,
+ maxCount,
+ onAllRemoved,
+ renderNotifications,
+ });
+
+ // ================================ Ref ================================
+ React.useImperativeHandle(ref, () => ({
+ ...api,
+ prefixCls,
+ message,
+ }));
+
+ return holder;
+});
+
+// ==============================================================================
+// == Hook ==
+// ==============================================================================
+let keyIndex = 0;
+
+export function useInternalMessage(
+ messageConfig?: HolderProps,
+): readonly [MessageInstance, React.ReactElement] {
+ const holderRef = React.useRef(null);
+
+ const warning = devUseWarning('Message');
+
+ // ================================ API ================================
+ const wrapAPI = React.useMemo(() => {
+ // Wrap with notification content
+
+ // >>> close
+ const close = (key: React.Key) => {
+ holderRef.current?.close(key);
+ };
+
+ // >>> Open
+ const open = (config: ArgsProps): MessageType => {
+ 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.',
+ );
+
+ const fakeResult: any = () => {};
+ fakeResult.then = () => {};
+ return fakeResult;
+ }
+
+ const { open: originOpen, prefixCls, message } = holderRef.current;
+ const noticePrefixCls = `${prefixCls}-notice`;
+
+ const { content, icon, type, key, className, style, onClose, ...restConfig } = config;
+
+ let mergedKey: React.Key = key!;
+ if (mergedKey === undefined || mergedKey === null) {
+ keyIndex += 1;
+ mergedKey = `antd-message-${keyIndex}`;
+ }
+
+ return wrapPromiseFn((resolve) => {
+ originOpen({
+ ...restConfig,
+ key: mergedKey,
+ content: (
+
+ {content}
+
+ ),
+ placement: 'top',
+ className: classNames(
+ type && `${noticePrefixCls}-${type}`,
+ className,
+ message?.className,
+ ),
+ style: { ...message?.style, ...style },
+ onClose: () => {
+ onClose?.();
+ resolve();
+ },
+ });
+
+ // Return close function
+ return () => {
+ close(mergedKey);
+ };
+ });
+ };
+
+ // >>> destroy
+ const destroy = (key?: React.Key) => {
+ if (key !== undefined) {
+ close(key);
+ } else {
+ holderRef.current?.destroy();
+ }
+ };
+
+ const clone = {
+ open,
+ destroy,
+ } as MessageInstance;
+
+ const keys: NoticeType[] = ['info', 'success', 'warning', 'error', 'loading'];
+ keys.forEach((type) => {
+ const typeOpen: TypeOpen = (jointContent, duration, onClose) => {
+ let config: ArgsProps;
+ if (jointContent && typeof jointContent === 'object' && 'content' in jointContent) {
+ config = jointContent;
+ } else {
+ config = {
+ content: jointContent,
+ };
+ }
+
+ // Params
+ let mergedDuration: number | undefined;
+ let mergedOnClose: VoidFunction | undefined;
+ if (typeof duration === 'function') {
+ mergedOnClose = duration;
+ } else {
+ mergedDuration = duration;
+ mergedOnClose = onClose;
+ }
+
+ const mergedConfig = {
+ onClose: mergedOnClose,
+ duration: mergedDuration,
+ ...config,
+ type,
+ };
+
+ return open(mergedConfig);
+ };
+
+ clone[type] = typeOpen;
+ });
+
+ return clone;
+ }, []);
+
+ // ============================== Return ===============================
+ return [wrapAPI, ] as const;
+}
+
+export default function useMessage(messageConfig?: ConfigOptions) {
+ return useInternalMessage(messageConfig);
+}
diff --git a/packages/meta/src/message/util.ts b/packages/meta/src/message/util.ts
new file mode 100644
index 0000000..0ad2619
--- /dev/null
+++ b/packages/meta/src/message/util.ts
@@ -0,0 +1,28 @@
+import type { CSSMotionProps } from 'rc-motion';
+
+export function getMotion(prefixCls: string, transitionName?: string): CSSMotionProps {
+ return {
+ motionName: transitionName ?? `${prefixCls}-move-up`,
+ };
+}
+
+/** Wrap message open with promise like function */
+export function wrapPromiseFn(openFn: (resolve: VoidFunction) => VoidFunction) {
+ let closeFn: VoidFunction;
+
+ const closePromise = new Promise((resolve) => {
+ closeFn = openFn(() => {
+ resolve(true);
+ });
+ });
+
+ const result: any = () => {
+ closeFn?.();
+ };
+
+ result.then = (filled: VoidFunction, rejected: VoidFunction) =>
+ closePromise.then(filled, rejected);
+ result.promise = closePromise;
+
+ return result;
+}
diff --git a/packages/meta/src/popover/PurePanel.tsx b/packages/meta/src/popover/PurePanel.tsx
new file mode 100644
index 0000000..fe76c7c
--- /dev/null
+++ b/packages/meta/src/popover/PurePanel.tsx
@@ -0,0 +1,82 @@
+import * as React from 'react';
+import classNames from 'classnames';
+import { Popup } from 'rc-tooltip';
+
+import type { PopoverProps } from '.';
+import { getRenderPropValue } from '../_util/getRenderPropValue';
+import { ConfigContext } from '../config-provider';
+import useStyle from './style';
+
+export const getOverlay = (
+ prefixCls?: string,
+ title?: PopoverProps['title'],
+ content?: PopoverProps['content'],
+) => {
+ if (!title && !content) {
+ return null;
+ }
+ return (
+ <>
+ {title && {getRenderPropValue(title)}
}
+ {getRenderPropValue(content)}
+ >
+ );
+};
+
+export interface PurePanelProps extends Omit {
+ children?: React.ReactNode;
+}
+
+interface RawPurePanelProps extends PopoverProps {
+ hashId: string;
+}
+
+export const RawPurePanel: React.FC = (props) => {
+ const {
+ hashId,
+ prefixCls,
+ className,
+ style,
+ placement = 'top',
+ title,
+ content,
+ children,
+ } = props;
+
+ return (
+
+
+
+ {children || getOverlay(prefixCls, title, content)}
+
+
+ );
+};
+
+const PurePanel: React.FC = (props) => {
+ const { prefixCls: customizePrefixCls, className, ...restProps } = props;
+ const { getPrefixCls } = React.useContext(ConfigContext);
+
+ const prefixCls = getPrefixCls('popover', customizePrefixCls);
+ const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
+
+ return wrapCSSVar(
+ ,
+ );
+};
+
+export default PurePanel;
diff --git a/packages/meta/src/popover/__tests__/__snapshots__/demo-extend.test.ts.snap b/packages/meta/src/popover/__tests__/__snapshots__/demo-extend.test.ts.snap
new file mode 100644
index 0000000..5100bf3
--- /dev/null
+++ b/packages/meta/src/popover/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -0,0 +1,1760 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders components/popover/demo/arrow.tsx extend context correctly 1`] = `
+Array [
+ ,
+
+
+
+
+ TL
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ Top
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ TR
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+
+
+ LT
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ Left
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ LB
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+
+
+ RT
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ Right
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ RB
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+
+
+ BL
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ Bottom
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ BR
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
,
+]
+`;
+
+exports[`renders components/popover/demo/arrow.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/popover/demo/basic.tsx extend context correctly 1`] = `
+Array [
+
+
+ Hover me
+
+ ,
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+]
+`;
+
+exports[`renders components/popover/demo/basic.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/popover/demo/component-token.tsx extend context correctly 1`] = `
+Array [
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+]
+`;
+
+exports[`renders components/popover/demo/component-token.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/popover/demo/control.tsx extend context correctly 1`] = `
+Array [
+
+
+ Click me
+
+ ,
+ ,
+]
+`;
+
+exports[`renders components/popover/demo/control.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/popover/demo/hover-with-click.tsx extend context correctly 1`] = `
+Array [
+
+
+ Hover and click / 悬停并单击
+
+ ,
+ ,
+
+
+
+
+
+ Hover title
+
+
+
+ This is hover content.
+
+
+
+
+
,
+]
+`;
+
+exports[`renders components/popover/demo/hover-with-click.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/popover/demo/placement.tsx extend context correctly 1`] = `
+
+
+
+
+ TL
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ Top
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ TR
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+
+
+ LT
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ Left
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ LB
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+
+
+ RT
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ Right
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ RB
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+
+
+ BL
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ Bottom
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+ BR
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders components/popover/demo/placement.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/popover/demo/render-panel.tsx extend context correctly 1`] = `
+Array [
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+]
+`;
+
+exports[`renders components/popover/demo/render-panel.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/popover/demo/triggerType.tsx extend context correctly 1`] = `
+
+
+
+
+ Hover me
+
+
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+
+
+ Focus me
+
+
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+
+
+ Click me
+
+
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders components/popover/demo/triggerType.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/popover/demo/wireframe.tsx extend context correctly 1`] = `
+Array [
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+]
+`;
+
+exports[`renders components/popover/demo/wireframe.tsx extend context correctly 2`] = `[]`;
diff --git a/packages/meta/src/popover/__tests__/__snapshots__/demo.test.tsx.snap b/packages/meta/src/popover/__tests__/__snapshots__/demo.test.tsx.snap
new file mode 100644
index 0000000..cd623d1
--- /dev/null
+++ b/packages/meta/src/popover/__tests__/__snapshots__/demo.test.tsx.snap
@@ -0,0 +1,613 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders components/popover/demo/arrow.tsx correctly 1`] = `
+Array [
+ ,
+
+
+
+
+ TL
+
+
+
+
+ Top
+
+
+
+
+ TR
+
+
+
+
+
+
+ LT
+
+
+
+
+ Left
+
+
+
+
+ LB
+
+
+
+
+
+
+ RT
+
+
+
+
+ Right
+
+
+
+
+ RB
+
+
+
+
+
+
+ BL
+
+
+
+
+ Bottom
+
+
+
+
+ BR
+
+
+
+
,
+]
+`;
+
+exports[`renders components/popover/demo/basic.tsx correctly 1`] = `
+
+
+ Hover me
+
+
+`;
+
+exports[`renders components/popover/demo/component-token.tsx correctly 1`] = `
+Array [
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+]
+`;
+
+exports[`renders components/popover/demo/control.tsx correctly 1`] = `
+
+
+ Click me
+
+
+`;
+
+exports[`renders components/popover/demo/hover-with-click.tsx correctly 1`] = `
+
+
+ Hover and click / 悬停并单击
+
+
+`;
+
+exports[`renders components/popover/demo/placement.tsx correctly 1`] = `
+
+
+
+
+ TL
+
+
+
+
+ Top
+
+
+
+
+ TR
+
+
+
+
+
+
+ LT
+
+
+
+
+ Left
+
+
+
+
+ LB
+
+
+
+
+
+
+ RT
+
+
+
+
+ Right
+
+
+
+
+ RB
+
+
+
+
+
+
+ BL
+
+
+
+
+ Bottom
+
+
+
+
+ BR
+
+
+
+
+`;
+
+exports[`renders components/popover/demo/render-panel.tsx correctly 1`] = `
+Array [
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+]
+`;
+
+exports[`renders components/popover/demo/triggerType.tsx correctly 1`] = `
+
+
+
+
+ Hover me
+
+
+
+
+
+
+ Focus me
+
+
+
+
+
+
+ Click me
+
+
+
+
+`;
+
+exports[`renders components/popover/demo/wireframe.tsx correctly 1`] = `
+Array [
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+
+
+
+
+
+ Title
+
+
+
+
+ Content
+
+
+ Content
+
+
+
+
+
+
,
+]
+`;
diff --git a/packages/meta/src/popover/__tests__/__snapshots__/index.test.tsx.snap b/packages/meta/src/popover/__tests__/__snapshots__/index.test.tsx.snap
new file mode 100644
index 0000000..b2ed21c
--- /dev/null
+++ b/packages/meta/src/popover/__tests__/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Popover should be rendered correctly in RTL direction 1`] = `
+Array [
+
+ show me your Rtl demo
+ ,
+ ,
+]
+`;
+
+exports[`Popover shows content for render functions 1`] = `"
"`;
diff --git a/packages/meta/src/popover/__tests__/demo-extend.test.ts b/packages/meta/src/popover/__tests__/demo-extend.test.ts
new file mode 100644
index 0000000..3967cec
--- /dev/null
+++ b/packages/meta/src/popover/__tests__/demo-extend.test.ts
@@ -0,0 +1,3 @@
+import { extendTest } from '../../../tests/shared/demoTest';
+
+extendTest('popover');
diff --git a/packages/meta/src/popover/__tests__/demo.test.tsx b/packages/meta/src/popover/__tests__/demo.test.tsx
new file mode 100644
index 0000000..0bc5e27
--- /dev/null
+++ b/packages/meta/src/popover/__tests__/demo.test.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react';
+
+import demoTest, { rootPropsTest } from '../../../tests/shared/demoTest';
+
+demoTest('popover', {
+ testRootProps: false,
+});
+
+rootPropsTest(
+ 'popover',
+ (Popover, props) => (
+
+
+
+ ),
+ { findRootElements: () => document.querySelector('.ant-popover')! },
+);
diff --git a/packages/meta/src/popover/__tests__/image.test.ts b/packages/meta/src/popover/__tests__/image.test.ts
new file mode 100644
index 0000000..cf5ab59
--- /dev/null
+++ b/packages/meta/src/popover/__tests__/image.test.ts
@@ -0,0 +1,5 @@
+import { imageDemoTest } from '../../../tests/shared/imageTest';
+
+describe('Popover image', () => {
+ imageDemoTest('popover');
+});
diff --git a/packages/meta/src/popover/__tests__/index.test.tsx b/packages/meta/src/popover/__tests__/index.test.tsx
new file mode 100644
index 0000000..d3bfd0d
--- /dev/null
+++ b/packages/meta/src/popover/__tests__/index.test.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+
+import Popover from '..';
+import mountTest from '../../../tests/shared/mountTest';
+import { fireEvent, render } from '../../../tests/utils';
+import ConfigProvider from '../../config-provider';
+import type { TooltipRef } from '../../tooltip';
+
+const { _InternalPanelDoNotUseOrYouWillBeFired: InternalPanelDoNotUseOrYouWillBeFired } = Popover;
+
+describe('Popover', () => {
+ mountTest(Popover);
+
+ it('should show overlay when trigger is clicked', () => {
+ const ref = React.createRef();
+ const { container } = render(
+
+ show me your code
+ ,
+ );
+ expect(container.querySelector('.ant-popover-inner-content')).toBeFalsy();
+ fireEvent.click(container.querySelector('span')!);
+ expect(container.querySelector('.ant-popover-inner-content')).toBeTruthy();
+ });
+
+ it('shows content for render functions', () => {
+ const renderTitle = () => 'some-title';
+ const renderContent = () => 'some-content';
+ const ref = React.createRef();
+ const { container } = render(
+
+ show me your code
+ ,
+ );
+ fireEvent.click(container.querySelector('span')!);
+ const popup = document.querySelector('.ant-popover')!;
+ expect(popup).not.toBe(null);
+ expect(popup.innerHTML).toContain('some-title');
+ expect(popup.innerHTML).toContain('some-content');
+ expect(popup.innerHTML).toMatchSnapshot();
+ });
+
+ it('handles empty title/content props safely', () => {
+ const { container } = render(
+
+ show me your code
+ ,
+ );
+ fireEvent.click(container.querySelector('span')!);
+
+ const popup = document.querySelector('.ant-popover');
+ expect(popup).toBe(null);
+ });
+
+ it('should not render popover when the title & content props is empty', () => {
+ const { container } = render(
+
+ show me your code
+ ,
+ );
+ fireEvent.click(container.querySelector('span')!);
+
+ const popup = document.querySelector('.ant-popover');
+ expect(popup).toBe(null);
+ });
+
+ it('props#overlay do not warn anymore', () => {
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ const overlay = jest.fn();
+ render(
+
+ show me your code
+ ,
+ );
+
+ expect(errorSpy).not.toHaveBeenCalled();
+ expect(overlay).not.toHaveBeenCalled();
+ });
+
+ it(`should be rendered correctly in RTL direction`, () => {
+ const { container } = render(
+
+
+ show me your Rtl demo
+
+ ,
+ );
+ expect(Array.from(container.children)).toMatchSnapshot();
+ });
+
+ it('should right work when content is null & title is null', () => {
+ expect(() => {
+ render( );
+ }).not.toThrow();
+ });
+});
diff --git a/packages/meta/src/popover/demo/arrow.md b/packages/meta/src/popover/demo/arrow.md
new file mode 100644
index 0000000..a470a0e
--- /dev/null
+++ b/packages/meta/src/popover/demo/arrow.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+通过 `arrow` 属性隐藏箭头。
+
+## en-US
+
+Hide arrow by `arrow`.
diff --git a/packages/meta/src/popover/demo/arrow.tsx b/packages/meta/src/popover/demo/arrow.tsx
new file mode 100644
index 0000000..201f101
--- /dev/null
+++ b/packages/meta/src/popover/demo/arrow.tsx
@@ -0,0 +1,89 @@
+import React, { useMemo, useState } from 'react';
+import { Button, ConfigProvider, Popover, Segmented } from 'antd';
+
+const text = Title ;
+
+const buttonWidth = 80;
+
+const content = (
+
+);
+
+const App: React.FC = () => {
+ const [arrow, setArrow] = useState('Show');
+
+ const mergedArrow = useMemo(() => {
+ if (arrow === 'Hide') {
+ return false;
+ }
+
+ if (arrow === 'Show') {
+ return true;
+ }
+
+ return {
+ pointAtCenter: true,
+ };
+ }, [arrow]);
+
+ return (
+
+ setArrow(val)}
+ style={{ marginBottom: 24 }}
+ />
+
+
+
+ TL
+
+
+ Top
+
+
+ TR
+
+
+
+
+ LT
+
+
+ Left
+
+
+ LB
+
+
+
+
+ RT
+
+
+ Right
+
+
+ RB
+
+
+
+
+ BL
+
+
+ Bottom
+
+
+ BR
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/packages/meta/src/popover/demo/basic.md b/packages/meta/src/popover/demo/basic.md
new file mode 100644
index 0000000..2954cf6
--- /dev/null
+++ b/packages/meta/src/popover/demo/basic.md
@@ -0,0 +1,13 @@
+## zh-CN
+
+最简单的用法,浮层的大小由内容区域决定。
+
+## en-US
+
+The most basic example. The size of the floating layer depends on the contents region.
+
+
diff --git a/packages/meta/src/popover/demo/basic.tsx b/packages/meta/src/popover/demo/basic.tsx
new file mode 100644
index 0000000..2838b61
--- /dev/null
+++ b/packages/meta/src/popover/demo/basic.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Button, Popover } from 'antd';
+
+const content = (
+
+);
+
+const App: React.FC = () => (
+
+ Hover me
+
+);
+
+export default App;
diff --git a/packages/meta/src/popover/demo/component-token.md b/packages/meta/src/popover/demo/component-token.md
new file mode 100644
index 0000000..de91480
--- /dev/null
+++ b/packages/meta/src/popover/demo/component-token.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+Component Token Debug.
+
+## en-US
+
+Component Token Debug.
diff --git a/packages/meta/src/popover/demo/component-token.tsx b/packages/meta/src/popover/demo/component-token.tsx
new file mode 100644
index 0000000..701300e
--- /dev/null
+++ b/packages/meta/src/popover/demo/component-token.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { ConfigProvider, Popover } from 'antd';
+
+const { _InternalPanelDoNotUseOrYouWillBeFired: InternalPopover } = Popover;
+
+const content = (
+
+);
+
+const App: React.FC = () => (
+
+
+
+
+);
+
+export default App;
diff --git a/packages/meta/src/popover/demo/control.md b/packages/meta/src/popover/demo/control.md
new file mode 100644
index 0000000..3c46666
--- /dev/null
+++ b/packages/meta/src/popover/demo/control.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+使用 `open` 属性控制浮层显示。
+
+## en-US
+
+Use `open` prop to control the display of the card.
diff --git a/packages/meta/src/popover/demo/control.tsx b/packages/meta/src/popover/demo/control.tsx
new file mode 100644
index 0000000..ae245f9
--- /dev/null
+++ b/packages/meta/src/popover/demo/control.tsx
@@ -0,0 +1,28 @@
+import React, { useState } from 'react';
+import { Button, Popover } from 'antd';
+
+const App: React.FC = () => {
+ const [open, setOpen] = useState(false);
+
+ const hide = () => {
+ setOpen(false);
+ };
+
+ const handleOpenChange = (newOpen: boolean) => {
+ setOpen(newOpen);
+ };
+
+ return (
+ Close}
+ title="Title"
+ trigger="click"
+ open={open}
+ onOpenChange={handleOpenChange}
+ >
+ Click me
+
+ );
+};
+
+export default App;
diff --git a/packages/meta/src/popover/demo/hover-with-click.md b/packages/meta/src/popover/demo/hover-with-click.md
new file mode 100644
index 0000000..5d49e5a
--- /dev/null
+++ b/packages/meta/src/popover/demo/hover-with-click.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+以下示例显示如何创建可悬停和单击的弹出窗口。
+
+## en-US
+
+The following example shows how to create a popover which can be hovered and clicked.
diff --git a/packages/meta/src/popover/demo/hover-with-click.tsx b/packages/meta/src/popover/demo/hover-with-click.tsx
new file mode 100644
index 0000000..bb39ac9
--- /dev/null
+++ b/packages/meta/src/popover/demo/hover-with-click.tsx
@@ -0,0 +1,52 @@
+import React, { useState } from 'react';
+import { Button, Popover } from 'antd';
+
+const App: React.FC = () => {
+ const [clicked, setClicked] = useState(false);
+ const [hovered, setHovered] = useState(false);
+
+ const hide = () => {
+ setClicked(false);
+ setHovered(false);
+ };
+
+ const handleHoverChange = (open: boolean) => {
+ setHovered(open);
+ setClicked(false);
+ };
+
+ const handleClickChange = (open: boolean) => {
+ setHovered(false);
+ setClicked(open);
+ };
+
+ const hoverContent = This is hover content.
;
+ const clickContent = This is click content.
;
+ return (
+
+
+ {clickContent}
+ Close
+
+ }
+ title="Click title"
+ trigger="click"
+ open={clicked}
+ onOpenChange={handleClickChange}
+ >
+