diff --git a/components/message/__docs__/adaptor/index.tsx b/components/message/__docs__/adaptor/index.tsx index ae19fab16c..760df2494f 100644 --- a/components/message/__docs__/adaptor/index.tsx +++ b/components/message/__docs__/adaptor/index.tsx @@ -2,6 +2,17 @@ import React from 'react'; import { Message } from '@alifd/next'; import { Types } from '@alifd/adaptor-helper'; +interface AdaptorProps { + level: 'inline' | 'toast' | 'addon'; + size: 'large' | 'medium'; + state: 'success' | 'warning' | 'error' | 'notice' | 'help' | 'loading'; + closable: boolean; + width: number; + title: string; + data: string; + style: React.CSSProperties; +} + export default { name: 'Message', editor: () => ({ @@ -47,7 +58,17 @@ export default { default: 'This item already has the label "travel", you can add a new label.', }, }), - adaptor: ({ level, size, state, closable, width, title, data, style, ...others }) => { + adaptor: ({ + level, + size, + state, + closable, + width, + title, + data, + style, + ...others + }: AdaptorProps) => { return ( { + handleSize = (size: State['size']) => { this.setState({ size }); }; - handleShape = shape => { + handleShape = (shape: State['shape']) => { this.setState({ shape }); }; diff --git a/components/message/__docs__/demo/withContext/index.tsx b/components/message/__docs__/demo/withContext/index.tsx index a3a0455584..2575ea009b 100644 --- a/components/message/__docs__/demo/withContext/index.tsx +++ b/components/message/__docs__/demo/withContext/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; + ReactDOM.render( 点击查看 Message.withContext Demo diff --git a/components/message/__docs__/index.en-us.md b/components/message/__docs__/index.en-us.md index 5856fa2876..db4817bdce 100644 --- a/components/message/__docs__/index.en-us.md +++ b/components/message/__docs__/index.en-us.md @@ -13,51 +13,52 @@ ### Message -| Param | Descripiton | Type | Default Value | -| -------------- | ---------------------------------------------------------------------------------- | --------- | --------- | -| size | size of message

**option**:
'medium', 'large' | Enum | 'medium' | -| type | type of message

**option**:
'success', 'warning', 'error', 'notice', 'help', 'loading' | Enum | 'success' | -| shape | shape of message

**option**:
'inline', 'addon', 'toast' | Enum | 'inline' | -| title | title of message | ReactNode | - | -| children | content of message | ReactNode | - | -| defaultVisible | whether the message is visible in default | Boolean | true | -| visible | whether the message is visible currently | Boolean | - | -| iconType | type of icon, overriding the internally type of icon | String | - | -| closeable | whether to show the close button | Boolean | false | -| onClose | callback function triggered when close

**signatures**:
Function() => void | Function | () => {} | -| afterClose | callback function triggered after closed

**signatures**:
Function() => void | Function | () => {} | -| animation | whether to enable expand and collapse animation | Boolean | true | - - +| Param | Description | Type | Default Value | Required | +| -------------- | ---------------------------------------------------- | -------------------------------------------------------------------- | ------------- | -------- | +| type | Type of message | 'success' \| 'warning' \| 'error' \| 'notice' \| 'help' \| 'loading' | 'success' | | +| shape | Shape of message | 'inline' \| 'addon' \| 'toast' | 'inline' | | +| size | Size of message | 'medium' \| 'large' | 'medium' | | +| title | Title of message | React.ReactNode | - | | +| children | Content of message | React.ReactNode | - | | +| defaultVisible | Whether the message is visible in default | boolean | false | | +| visible | Whether the message is visible currently | boolean | - | | +| iconType | Type of icon, overriding the internally type of icon | string \| false | - | | +| closeable | Whether to show the close button | boolean | false | | +| onClose | Callback function triggered when close | () => void | () =\> \{\} | | +| afterClose | Callback function triggered after closed | () => void | () =\> \{\} | | +| animation | Whether to enable expand and collapse animation | boolean | true | | ### Message.show `Message.show(props)` provides a singleton call with the following configuration parameters (inheriting `Overlay` configuration): -| Param | Descripiton | Type | Default Value | -| ------------ | --------------------------------------------------------------------------------------------------- | --------- | --------- | -| type | type of message | String | 'success' | -| title | title of message | ReactNode | - | -| content | content of message | ReactNode | - | -| duration | show duration, 0 means always present, in milliseconds | Number | 3000 | -| align | alignment reference Overlay | String | 'tc tc' | -| offset | offset after positioned | Array | [0, 0] | -| hasMask | whether to have a mask | Boolean | false | -| closeable | whether to show the close button | Boolean | false | -| afterClose | callback function triggered after closed | Function | - | -| overlayProps | props of Overlay | Object | - | - -Example: - ```js Message.show({ type: 'error', title: 'Error', content: 'Please contact admin feedback!', - hasMask: true + hasMask: true, }); ``` +| Param | Description | Type | Default Value | Required | +| ------------ | ------------------------------------------------------ | -------------------------------------------------------------------- | ------------- | -------- | +| type | Type of message | 'success' \| 'warning' \| 'error' \| 'notice' \| 'help' \| 'loading' | 'success' | | +| size | Size of message | 'medium' \| 'large' | 'medium' | | +| title | Title of message | React.ReactNode | - | | +| content | Content of message | React.ReactNode | - | | +| align | Alignment reference Overlay | string \| boolean | 'tc tc' | | +| offset | Offset after positioned | Array\ | [0, 0] | | +| hasMask | Whether to have a mask | boolean | false | | +| duration | Show duration, 0 means always present, in milliseconds | number | 3000 | | +| closeable | Whether to show the close button | boolean | false | | +| onClose | Callback function triggered when close | () => void | () =\> \{\} | | +| afterClose | Callback function triggered after closed | () => void | () =\> \{\} | | +| animation | Whether to enable expand and collapse animation | boolean | true | | +| overlayProps | Props of Overlay | OverlayProps | - | | + + + ### Message.hide `Message.hide()` provides a quick way to close the message. @@ -74,14 +75,12 @@ Message.success('message content'); // or Message.success({ title: 'message content', - duration: 1000 + duration: 1000, }); ``` - - ## ARIA and KeyBoard -`Description`: This component needs to be used in conjunction with other components to be prompted. Refer to the above `accessibility` \ No newline at end of file +`Description`: This component needs to be used in conjunction with other components to be prompted. Refer to the above `accessibility` diff --git a/components/message/__docs__/index.md b/components/message/__docs__/index.md index a78639a481..a564c21237 100644 --- a/components/message/__docs__/index.md +++ b/components/message/__docs__/index.md @@ -18,51 +18,52 @@ ### Message -| 参数 | 说明 | 类型 | 默认值 | -| -------------- | ------------------------------------------------------------------------------------- | -------------- | --------- | -| size | 反馈大小

**可选值**:
'medium', 'large' | Enum | 'medium' | -| type | 反馈类型

**可选值**:
'success', 'warning', 'error', 'notice', 'help', 'loading' | Enum | 'success' | -| shape | 反馈外观

**可选值**:
'inline', 'addon', 'toast' | Enum | 'inline' | -| title | 标题 | ReactNode | - | -| children | 内容 | ReactNode | - | -| defaultVisible | 默认是否显示 | Boolean | true | -| visible | 当前是否显示 | Boolean | - | -| iconType | 显示的图标类型,会覆盖内部设置的IconType,传false不显示图标 | String/Boolean | - | -| closeable | 显示关闭按钮 | Boolean | false | -| onClose | 关闭按钮的回调

**签名**:
Function() => void | Function | () => {} | -| afterClose | 关闭之后调用的函数

**签名**:
Function() => void | Function | () => {} | -| animation | 是否开启展开收起动画 | Boolean | true | - - +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| -------------- | ----------------------------------------------------------- | -------------------------------------------------------------------- | ----------- | -------- | +| type | 反馈类型 | 'success' \| 'warning' \| 'error' \| 'notice' \| 'help' \| 'loading' | 'success' | | +| shape | 反馈外观 | 'inline' \| 'addon' \| 'toast' | 'inline' | | +| size | 反馈大小 | 'medium' \| 'large' | 'medium' | | +| title | 标题 | React.ReactNode | - | | +| children | 内容,非函数式调用下使用 | React.ReactNode | - | | +| defaultVisible | 默认是否显示 | boolean | false | | +| visible | 当前是否显示 | boolean | - | | +| iconType | 显示的图标类型,会覆盖内部设置的IconType,传false不显示图标 | string \| false | - | | +| closeable | 显示关闭按钮 | boolean | false | | +| onClose | 关闭按钮的回调 | () => void | () =\> \{\} | | +| afterClose | 关闭之后调用的函数 | () => void | () =\> \{\} | | +| animation | 是否开启展开收起动画 | boolean | true | | ### Message.show -`Message.show(props)` 提供一个单例的调用方式,配置参数如下(继承 `Overlay` 的配置): - -| 参数 | 说明 | 类型 | 默认值 | -| ------------ | --------------------- | --------- | --------- | -| type | 反馈类型 | String | 'success' | -| title | 反馈标题 | ReactNode | - | -| content | 反馈内容 | ReactNode | - | -| duration | 显示持续时间,0表示一直存在,以毫秒为单位 | Number | 3000 | -| align | 对齐方式,参考Overlay | String | 'tc tc' | -| offset | 对齐之后的偏移位置 | Array | [0, 0] | -| hasMask | 是否带有遮罩 | Boolean | false | -| closeable | 显示关闭按钮 | Boolean | false | -| afterClose | 关闭事件的回调函数 | Function | - | -| overlayProps | 透传到弹层的属性对象 | Object | - | - -示例: +Message.show(props) 提供一个单例的调用方式,配置参数如下(继承 Overlay 的配置): ```js Message.show({ type: 'error', title: '错误', content: '请联系相关人员反馈!', - hasMask: true + hasMask: true, }); ``` +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ------------ | ----------------------------------------- | -------------------------------------------------------------------- | ----------- | -------- | +| type | 反馈类型 | 'success' \| 'warning' \| 'error' \| 'notice' \| 'help' \| 'loading' | 'success' | | +| size | 反馈大小 | 'medium' \| 'large' | 'medium' | | +| title | 标题 | React.ReactNode | - | | +| content | 内容,函数式调用下使用 | React.ReactNode | - | | +| align | 弹层对齐方式,详情见 Overlay align | string \| boolean | 'tc tc' | | +| offset | 弹层相对于参照元素定位的微调 | Array\ | [0, 0] | | +| hasMask | 是否显示遮罩 | boolean | false | | +| duration | 显示持续时间,0表示一直存在,以毫秒为单位 | number | 3000 | | +| closeable | 显示关闭按钮 | boolean | false | | +| onClose | 关闭按钮的回调 | () => void | () =\> \{\} | | +| afterClose | 关闭之后调用的函数 | () => void | () =\> \{\} | | +| animation | 是否开启展开收起动画 | boolean | true | | +| overlayProps | 透传到弹层组件的属性对象 | OverlayProps | - | | + + + ### Message.hide `Message.hide()` 提供关闭反馈弹层的快捷方法。 @@ -79,7 +80,7 @@ Message.success('反馈内容'); // 或者 Message.success({ title: '反馈内容', - duration: 1000 + duration: 1000, }); ``` @@ -93,17 +94,17 @@ Message.success({ ```js Message.config({ - top: 100, - duration: 2000, - maxCount: 3, + top: 100, + duration: 2000, + maxCount: 3, }); ``` -| 参数 | 说明 | 类型 | 默认值 | | -| -------- | ---------------- | ------ | ---- | --- | -| duration | 默认自动关闭延时,单位毫秒 | Number | 3000 | | -| top | 消息距离顶部的位置 | Number | 8 | | -| maxCount | 最多同时出现的个数, 默认不限制 | Number | - | | +| 参数 | 说明 | 类型 | 默认值 | | +| -------- | ------------------------------ | ------ | ------ | --- | +| duration | 默认自动关闭延时,单位毫秒 | Number | 3000 | | +| top | 消息距离顶部的位置 | Number | 8 | | +| maxCount | 最多同时出现的个数, 默认不限制 | Number | - | | ```js const instance = Message.success('this is a message'); diff --git a/components/message/__docs__/theme/index.tsx b/components/message/__docs__/theme/index.tsx index fb8f8e0bba..236914b5b8 100644 --- a/components/message/__docs__/theme/index.tsx +++ b/components/message/__docs__/theme/index.tsx @@ -2,7 +2,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import '../../../demo-helper/style'; import '../../style'; -import { Demo, DemoGroup, DemoHead, initDemo } from '../../../demo-helper'; +import { + Demo, + DemoGroup, + DemoHead, + initDemo, + type DemoFunctionDefineForObject, +} from '../../../demo-helper'; import Message from '../../index'; const i18nMap = { @@ -16,13 +22,21 @@ const i18nMap = { }, }; -const shapes = ['inline', 'addon', 'toast']; -const types = ['success', 'warning', 'error', 'notice', 'help', 'loading']; +const shapes = ['inline', 'addon', 'toast'] as const; +const types = ['success', 'warning', 'error', 'notice', 'help', 'loading'] as const; -const toFirstUpperCase = str => str && str.substring(0, 1).toUpperCase() + str.substring(1); +const toFirstUpperCase = (str: string) => + str && str.substring(0, 1).toUpperCase() + str.substring(1); -class FunctionDemo extends React.Component { - constructor(props) { +type I18N = (typeof i18nMap)[keyof typeof i18nMap]; +interface Props { + i18n: I18N; +} +interface State { + demoFunction: Record; +} +class FunctionDemo extends React.Component { + constructor(props: Props) { super(props); this.state = { demoFunction: { @@ -60,27 +74,21 @@ class FunctionDemo extends React.Component { this.onFunctionChange = this.onFunctionChange.bind(this); } - onFunctionChange(demoFunction) { + onFunctionChange(demoFunction: State['demoFunction']) { this.setState({ demoFunction, }); } render() { - // eslint-disable-next-line const { i18n } = this.props; const { demoFunction } = this.state; const title = demoFunction.hasTitle.value === 'true' ? i18n.title : null; const closeable = demoFunction.closeable.value === 'true'; - const style = { - lineHeight: 1.7, - margin: 0, - }; - const newChildren = shapes.map(shape => { const content = types.map(type => { - const children = ['large', 'medium'].map(size => ( + const children = (['large', 'medium'] as const).map(size => ( diff --git a/components/message/__tests__/a11y-spec.tsx b/components/message/__tests__/a11y-spec.tsx index 049fcf0571..f103325169 100644 --- a/components/message/__tests__/a11y-spec.tsx +++ b/components/message/__tests__/a11y-spec.tsx @@ -1,26 +1,11 @@ import React from 'react'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import Message from '../index'; import '../style'; -import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate'; +import { testReact } from '../../util/__tests__/a11y/validate'; -Enzyme.configure({ adapter: new Adapter() }); - -/* eslint-disable no-undef, react/jsx-filename-extension */ describe('Message A11y', () => { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - it('should not have any violations when various types', async () => { - wrapper = await testReact( + await testReact(
Content Content Content Content @@ -42,11 +27,10 @@ describe('Message A11y', () => {
); - return wrapper; }); it('should not have any violations when various shapes', async () => { - wrapper = await testReact( + await testReact(
Content Content Content Content @@ -59,11 +43,10 @@ describe('Message A11y', () => {
); - return wrapper; }); it('should not have any violations when various sizes', async () => { - wrapper = await testReact( + await testReact(
Content Content Content Content @@ -73,18 +56,15 @@ describe('Message A11y', () => {
); - return wrapper; }); it('should not have any violations when closable', async () => { - wrapper = await testReact( + await testReact(
Content Content Content Content
); - - return wrapper; }); }); diff --git a/components/message/__tests__/index-spec.tsx b/components/message/__tests__/index-spec.tsx index 776019f3dd..fed1d9cfde 100644 --- a/components/message/__tests__/index-spec.tsx +++ b/components/message/__tests__/index-spec.tsx @@ -1,241 +1,157 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import Icon from '../../icon'; import Button from '../../button'; import ConfigProvider from '../../config-provider'; import { env } from '../../util'; import Message from '../index'; import '../style'; -/* eslint-disable react/jsx-filename-extension */ -/* global describe it afterEach */ -Enzyme.configure({ adapter: new Adapter() }); - -const render = element => { - let inc; - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(element, container, function () { - inc = this; - }); - return { - setProps: props => { - const clonedElement = React.cloneElement(element, props); - ReactDOM.render(clonedElement, container); - }, - unmount: () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - }, - instance: () => { - return inc; - }, - find: selector => { - return container.querySelectorAll(selector); - }, - }; -}; - describe('Message', () => { it('should receive className prop', () => { - const wrapper = mount(); - assert(wrapper.find('.next-message.custom').length === 1); - wrapper.unmount(); + cy.mount(); + cy.get('.next-message.custom').should('have.length', 1); }); it('should receive style prop', () => { - const wrapper = mount(); - assert(wrapper.prop('style').color === 'red'); - wrapper.unmount(); + cy.mount(); + cy.get('.next-message').should('have.css', 'color', 'rgb(255, 0, 0)'); }); it('should have type class', () => { - const types = ['success', 'warning', 'error', 'notice', 'help', 'loading']; + const types = ['success', 'warning', 'error', 'notice', 'help', 'loading'] as const; types.forEach(type => { - const wrapper = mount(); - assert(wrapper.find(`.next-message.next-message-${type}`).length === 1); - assert(wrapper.find(Icon).hasClass('next-message-symbol-icon')); - wrapper.unmount(); + cy.mount(); + cy.get(`.next-message.next-message-${type}`).should('have.length', 1); + cy.get('.next-icon').should('have.class', 'next-message-symbol-icon'); }); }); it('should have shape class', () => { - const shapes = ['inline', 'addon', 'toast']; + const shapes = ['inline', 'addon', 'toast'] as const; shapes.forEach(shape => { - const wrapper = mount(); - assert(wrapper.find(`.next-message.next-${shape}`).length === 1); - wrapper.unmount(); + cy.mount(); + cy.get(`.next-message.next-${shape}`).should('have.length', 1); }); }); it('should have size class', () => { - const sizes = ['medium', 'large']; + const sizes = ['medium', 'large'] as const; sizes.forEach(size => { - const wrapper = mount(); - assert(wrapper.find(`.next-message.next-${size}`).length === 1); - wrapper.unmount(); + cy.mount(); + cy.get(`.next-message.next-${size}`).should('have.length', 1); }); }); it('should show title if you pass it', () => { - const wrapper = mount(); - assert(wrapper.find('.next-message-title').text() === 'title'); - wrapper.unmount(); + cy.mount(); + cy.get('.next-message-title').should('have.text', 'title'); }); it('should show content if you pass it', () => { - const wrapper = mount(content); - assert(wrapper.find('.next-message-content').text() === 'content'); - wrapper.unmount(); + cy.mount(content); + cy.get('.next-message-content').should('have.text', 'content'); }); it('should not show icon if set iconType to false', () => { - const wrapper = mount(); - assert(wrapper.find(Icon).length === 0); - wrapper.unmount(); + cy.mount(); + cy.get('.next-icon').should('have.length', 0); }); it('should custom icon type', () => { - const wrapper = mount(); - assert(wrapper.find(Icon).prop('type') === 'smile'); - wrapper.unmount(); + cy.mount(); + cy.get('.next-icon').should('have.class', 'next-icon-smile'); }); - it('should show close link if set closeable to true', done => { - const wrapper = mount(); - const closeLink = wrapper.find('.next-message-close'); - assert(closeLink.length === 1); - closeLink.simulate('click'); - setTimeout(() => { - assert(wrapper.find('.next-message').length === 0); - wrapper.unmount(); - done(); - }, 500); + it('should show close link if set closeable to true', () => { + cy.mount(); + cy.get('.next-message-close').should('have.length', 1); + cy.get('.next-message-close').click(); + cy.get('.next-message').should('have.length', 0); }); it('should show or hide under control', () => { - const wrapper = mount( - + cy.mount().as( + 'Message' ); - assert(wrapper.find('.next-message').length === 0); - wrapper.setProps({ - visible: true, - }); - assert(wrapper.find('.next-message').length === 1); - wrapper.unmount(); + cy.get('.next-message').should('have.length', 0); + cy.rerender('Message', { visible: true }); + cy.get('.next-message').should('have.length', 1); }); }); -describe('toast', done => { - it('should render nowrap message when content too long[Overlay case]', done => { +describe('toast', () => { + it('should render nowrap message when content too long[Overlay case]', () => { const content = 'content content content content content content content content content content content content content content content content content content content content'; Message.show(content); - - const dom = document.querySelector('.next-overlay-wrapper .next-message'); - - assert(dom.innerText.trim() === content); - assert(dom.offsetWidth > 200); - + cy.get('.next-overlay-wrapper').find('.next-message').should('have.text', content); + cy.get('.next-overlay-wrapper') + .find('.next-message') + .should($toast => { + // access the native DOM element + expect($toast.get(0).innerText.trim()).to.eq(content); + expect($toast.get(0).offsetWidth).to.be.greaterThan(200); + }); Message.hide(); - - setTimeout(done, 500); }); - it('should render message when only pass content string', done => { + it('should render message when only pass content string', () => { Message.show('content'); - assert( - document.querySelector('.next-overlay-wrapper .next-message').innerText.trim() === - 'content' - ); - + cy.get('.next-overlay-wrapper .next-message').should('have.text', 'content'); Message.hide(); - setTimeout(done, 500); }); - it('should render message when only pass content react element', done => { + it('should render message when only pass content react element', () => { Message.show(content); - assert( - document - .querySelector('.next-overlay-wrapper .next-message-title i') - .innerText.trim() === 'content' - ); - + cy.get('.next-overlay-wrapper .next-message-title i').should('have.text', 'content'); Message.hide(); - - setTimeout(done, 500); }); - it('should render message when pass config object', done => { + it('should render message when pass config object', () => { Message.show({ type: 'warning', content: 'content', }); - assert( - document - .querySelector('.next-overlay-wrapper .next-message.next-message-warning') - .innerText.trim() === 'content' + cy.get('.next-overlay-wrapper .next-message.next-message-warning').should( + 'have.text', + 'content' ); - Message.hide(); - setTimeout(done, 500); }); - it('should close message after duration and call afterClose method', done => { - let called = false; - + it('should close message after duration and call afterClose method', () => { + const afterClose = cy.spy(); Message.show({ content: 'content', duration: 100, - afterClose: () => { - called = true; - }, + afterClose, }); - setTimeout(() => { - assert(document.querySelector('.next-overlay-wrapper .next-message') === null); - assert(called); - done(); - }, 1000); + cy.get('.next-overlay-wrapper .next-message', { timeout: 500 }).should('not.exist'); + cy.wrap(afterClose).should('be.calledOnce'); }); - it('should show message all the time when set duration to 0', done => { + it('should show message all the time when set duration to 0', () => { + cy.clock(); Message.show({ content: 'content', duration: 0, }); - - setTimeout(() => { - assert( - document.querySelector('.next-overlay-wrapper .next-message.next-message') !== null - ); - Message.hide(); - }, 500); - - setTimeout(done, 1000); + // 验证duration为0的配置是否生效,若不生效则3000ms后会自动消失 + cy.tick(3500); + cy.get('.next-overlay-wrapper .next-message') + .should('exist') + .then(() => { + Message.hide(); + }); }); - it('should hide message when call hide method', done => { - let called = false; - + it('should hide message when call hide method', () => { + const afterClose = cy.spy(); Message.show({ content: 'content', - duration: 100, - afterClose: () => { - called = true; - }, + afterClose, }); - - setTimeout(() => { - Message.hide(); - }, 500); - - setTimeout(() => { - assert(document.querySelector('.next-overlay-wrapper .next-message') === null); - assert(called); - done(); - }, 1000); + Message.hide(); + cy.get('.next-overlay-wrapper .next-message').should('not.exist'); + cy.wrap(afterClose).should('be.calledOnce'); }); it('should hide message when click this close link', done => { @@ -244,49 +160,23 @@ describe('toast', done => { if (env.ieVersion === 9 || env.ieVersion === 10) { return done(); } - Message.show({ content: 'content', duration: 0, closeable: true, }); - - setTimeout(() => { - const closeLink = document.querySelector('.next-message-close'); - closeLink.click(); - }, 500); - - setTimeout(() => { - assert(document.querySelector('.next-overlay-wrapper .next-message') === null); - done(); - }, 1000); + cy.get('.next-message-close').click(); + cy.get('.next-overlay-wrapper .next-message').should('not.exist'); + done(); }); }); describe('should support configProvider', () => { - it('normal should obey: self.locale > nearest ConfigProvider.locale > further ConfigProvider.locale', () => { - const methods = ['success', 'warning', 'error', 'notice', 'help', 'loading']; - const wrapper = render( - - + it('normal should obey: self.prefix > nearest ConfigProvider.prefix > further ConfigProvider.prefix', () => { + const methods = ['success', 'warning', 'error', 'notice', 'help', 'loading'] as const; + cy.mount( + +
{methods.map(method => ( @@ -297,91 +187,89 @@ describe('should support configProvider', () => { ); - const innerBtn = document.querySelectorAll( - '.near-message .near-message-content .near-btn-primary' + cy.get('.near-message .near-message-content .near-btn-primary').should( + 'have.length', + methods.length ); - assert(innerBtn.length === methods.length); - wrapper.unmount(); }); - // it('quick-calling should use root context\'s state if its exists', () => { - - // ConfigProvider.initLocales({ - // 'zh-cn': zhCN - // }); - // ConfigProvider.setLanguage('zh-cn'); - - // const methods = ['success', 'warning', 'error', 'notice', 'help', 'loading']; - // methods.forEach(method => { - // const wrapper = render( - // - // - // , - // hasMask: true - // }); - // }}> - // OK - // - // - // - // ); - - // const btn = document.querySelector('button'); - // ReactTestUtils.Simulate.click(btn); - // const icon = document.querySelector('.far-icon.far-message-symbol'); - // const innerBtn = document.querySelector('.far-message-content .far-btn-primary'); - - // assert(icon); - // assert(innerBtn); + it('Message.withContext should use nearest context instead of root context', () => { + // 清除缓存, 防止其他包含configProvider测试case影响 + ConfigProvider.clearCache(); + const BeforeFix = () => { + return ( + + ); + }; + const AfterFix = Message.withContext(({ contextMessage }) => { + return ( +
+ +
+ ); + }); + cy.mount( + + +
+ + +
+
+
+ ); + cy.get('.next-btn-primary').click(); + cy.get('.root-message'); + cy.get('.next-message').should('not.exist'); + Message.hide(); - // wrapper.unmount(); - // }); - // }); + cy.get('.next-btn-normal').click(); + cy.get('.next-message'); + cy.get('.root-message').should('not.exist'); + Message.hide(); + }); }); describe('toast quick-calling', () => { - const avaliableMethods = ['success', 'warning', 'error', 'notice', 'help', 'loading']; + const avaliableMethods = ['success', 'warning', 'error', 'notice', 'help', 'loading'] as const; for (const method of avaliableMethods) { it(`render ${method}`, done => { - Message.show('content'); - assert( - document.querySelector('.next-overlay-wrapper .next-message').innerText.trim() === - 'content' - ); - setTimeout(() => { - Message.hide(); - done(); - }, 500); + Message[method]('content'); + cy.get('.next-overlay-wrapper .next-message').should('have.text', 'content'); + Message.hide(); + done(); }); } }); describe('Message v2', () => { - it('should support config to open multiple instance', done => { + it('should support config to open multiple instance', () => { Message.config({}); - const instance1 = Message.show('content'); - const instance2 = Message.success('content'); - assert(document.querySelectorAll('.next-message-wrapper-v2 .next-message').length === 2); - Message.destory(); - setTimeout(done, 500); + Message.show('content'); + Message.success('content'); + cy.get('.next-message-wrapper-v2 .next-message') + .should('have.length', 2) + .then(() => { + Message.destory(); + }); }); }); diff --git a/components/message/__tests__/issue-spec.tsx b/components/message/__tests__/issue-spec.tsx index 973bf393b5..5f4902b6b5 100644 --- a/components/message/__tests__/issue-spec.tsx +++ b/components/message/__tests__/issue-spec.tsx @@ -1,63 +1,33 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; import Message from '../index'; import '../style'; -/* eslint-disable react/jsx-filename-extension */ -/* global describe it afterEach */ -Enzyme.configure({ adapter: new Adapter() }); -const render = element => { - let inc; - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(element, container, function () { - inc = this; - }); - return { - setProps: props => { - const clonedElement = React.cloneElement(element, props); - ReactDOM.render(clonedElement, container); - }, - unmount: () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - }, - instance: () => { - return inc; - }, - find: selector => { - return container.querySelectorAll(selector); - }, - }; -}; - describe('Message issues', () => { - let wrapper; - - afterEach(() => { - wrapper && wrapper.unmount(); - }); - // Fix: https://github.com/alibaba-fusion/next/issues/3910 // rest message symbol icon 'vertical-align: top' it('should align icon & content', () => { - wrapper = render( + cy.mount( content ); - assert(wrapper.find('.next-message').length === 1); - const icon = wrapper.find('.next-message-symbol-icon')[0]; - const content = wrapper.find('.next-message-content')[0]; - assert(icon); - assert(content); - const iconPos = icon.getBoundingClientRect(); - const contentPos = content.getBoundingClientRect(); - assert(iconPos.height); - assert(iconPos.height === contentPos.height); - assert(iconPos.y === contentPos.y); + cy.get('.next-message').should('have.length', 1); + cy.get('.next-message').get('.next-message-symbol-icon').should('exist'); + cy.get('.next-message').get('.next-message-content').should('exist'); + let iconPos: DOMRect; + cy.get('.next-message') + .get('.next-message-symbol-icon') + .should(node => { + iconPos = node.get(0).getBoundingClientRect(); + expect(iconPos.height).to.exist; + }); + let contentPos: DOMRect; + cy.get('.next-message') + .get('.next-message-content') + .should(node => { + contentPos = node.get(0).getBoundingClientRect(); + expect(iconPos.height).to.equal(contentPos.height); + expect(iconPos.y).to.equal(contentPos.y); + }); }); }); diff --git a/components/message/index.tsx b/components/message/index.tsx index 4e0c1122ce..2612a0ec83 100644 --- a/components/message/index.tsx +++ b/components/message/index.tsx @@ -2,22 +2,26 @@ import ConfigProvider from '../config-provider'; import Message from './message'; import toast, { withContext } from './toast'; import message from './toast2'; +import { assignSubComponent } from '../util/component'; -Message.show = toast.show; -Message.success = toast.success; -Message.warning = toast.warning; -Message.error = toast.error; -Message.notice = toast.notice; -Message.help = toast.help; -Message.loading = toast.loading; -Message.hide = toast.hide; -Message.withContext = withContext; - -const MessageProvider = ConfigProvider.config(Message, { +const WithSubMessage = assignSubComponent(Message, { + show: toast.show, + success: toast.success, + warning: toast.warning, + error: toast.error, + notice: toast.notice, + help: toast.help, + loading: toast.loading, + hide: toast.hide, + withContext, +}); +const MessageProvider = ConfigProvider.config(WithSubMessage, { componentName: 'Message', }); export default MessageProvider; +export type { MessageProps, MessageQuickProps } from './types'; +export type { ContextMessage } from './toast'; let openV2 = false; // 调用 config 开启 v2 版本的 message diff --git a/components/message/message.tsx b/components/message/message.tsx index 0cfca2c058..f20d80d162 100644 --- a/components/message/message.tsx +++ b/components/message/message.tsx @@ -7,69 +7,42 @@ import Icon from '../icon'; import Animate from '../animate'; import ConfigProvider from '../config-provider'; import { obj } from '../util'; +import type { MessageProps } from './types'; +import type * as toast2 from './toast2'; + +type Toast2 = typeof toast2.default; const noop = () => {}; /** * Message */ -class Message extends Component { +class Message extends Component { static propTypes = { prefix: PropTypes.string, pure: PropTypes.bool, className: PropTypes.string, style: PropTypes.object, - /** - * 反馈类型 - */ type: PropTypes.oneOf(['success', 'warning', 'error', 'notice', 'help', 'loading']), - /** - * 反馈外观 - */ shape: PropTypes.oneOf(['inline', 'addon', 'toast']), - /** - * 反馈大小 - */ size: PropTypes.oneOf(['medium', 'large']), - /** - * 标题 - */ title: PropTypes.node, - /** - * 内容 - */ children: PropTypes.node, - /** - * 默认是否显示 - */ defaultVisible: PropTypes.bool, - /** - * 当前是否显示 - */ visible: PropTypes.bool, - /** - * 显示的图标类型,会覆盖内部设置的IconType,传false不显示图标 - */ iconType: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - /** - * 显示关闭按钮 - */ closeable: PropTypes.bool, - /** - * 关闭按钮的回调 - */ onClose: PropTypes.func, - /** - * 关闭之后调用的函数 - */ afterClose: PropTypes.func, - /** - * 是否开启展开收起动画 - */ animation: PropTypes.bool, locale: PropTypes.object, rtl: PropTypes.bool, }; + // config 方法被调用后,Message组件才会挂上 open、close、destory方法 + static config: (config: toast2.MessageConfig) => void; + static open: Toast2['open']; + static close: Toast2['close']; + static destory: Toast2['destory']; static defaultProps = { prefix: 'next-', @@ -92,7 +65,7 @@ class Message extends Component { : this.props.visible, }; - static getDerivedStateFromProps(props) { + static getDerivedStateFromProps(props: MessageProps) { if ('visible' in props) { return { visible: props.visible, @@ -108,14 +81,12 @@ class Message extends Component { visible: false, }); } - this.props.onClose(false); + this.props.onClose!(); }; render() { - /* eslint-disable no-unused-vars */ const { prefix, - pure, className, style, type, @@ -123,11 +94,8 @@ class Message extends Component { size, title, children, - defaultVisible, - visible: propsVisible, iconType: icon, closeable, - onClose, afterClose, animation, rtl, @@ -136,7 +104,6 @@ class Message extends Component { const others = { ...obj.pickOthers(Object.keys(Message.propTypes), this.props), }; - /* eslint-enable */ const { visible } = this.state; const messagePrefix = `${prefix}message`; @@ -147,7 +114,7 @@ class Message extends Component { [`${prefix}${size}`]: size, [`${prefix}title-content`]: !!title, [`${prefix}only-content`]: !title && !!children, - [className]: className, + [className!]: className, }); const newChildren = visible ? ( @@ -161,7 +128,7 @@ class Message extends Component { {closeable ? (
diff --git a/components/message/toast.tsx b/components/message/toast.tsx index 918a536ccb..0faf7465bf 100644 --- a/components/message/toast.tsx +++ b/components/message/toast.tsx @@ -1,17 +1,22 @@ -import React from 'react'; +import React, { type JSXElementConstructor } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import Overlay from '../overlay'; import ConfigProvider from '../config-provider'; import { guid } from '../util'; import Message from './message'; +import type { OpenProps, MessageQuickProps } from './types'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyProps = any; +type ConfigMask = InstanceType; const { config } = ConfigProvider; -let instance; -const timeouts = {}; +let instance: { destroy: () => void; component: ConfigMask | null } | null; +const timeouts: Record> = {}; -class Mask extends React.Component { +class Mask extends React.Component { static contextTypes = { prefix: PropTypes.string, }; @@ -53,10 +58,10 @@ class Mask extends React.Component { componentWillUnmount() { const { timeoutId } = this.props; - if (timeoutId in timeouts) { - const timeout = timeouts[timeoutId]; + if (timeoutId! in timeouts) { + const timeout = timeouts[timeoutId!]; clearTimeout(timeout); - delete timeouts[timeoutId]; + delete timeouts[timeoutId!]; } } @@ -71,7 +76,6 @@ class Mask extends React.Component { }; render() { - /* eslint-disable no-unused-vars */ const { prefix, type, @@ -88,7 +92,6 @@ class Mask extends React.Component { style, ...others } = this.props; - /* eslint-enable */ const { visible } = this.state; return ( { - /* eslint-disable no-unused-vars */ +const create = (props: MessageQuickProps) => { const { duration, afterClose, contextConfig, ...others } = props; - /* eslint-enable no-unused-vars */ - const div = document.createElement('div'); document.body.appendChild(div); const closeChain = function () { + // eslint-disable-next-line react/no-deprecated ReactDOM.unmountComponentAtNode(div); document.body.removeChild(div); afterClose && afterClose(); @@ -136,9 +137,8 @@ const create = props => { let newContext = contextConfig; if (!newContext) newContext = ConfigProvider.getContext(); - - let mask, - myRef, + let mask: ConfigMask | null = null, + myRef: ConfigMask, destroyed = false; const destroy = () => { const inc = mask && mask.getInstance(); @@ -146,13 +146,14 @@ const create = props => { destroyed = true; }; + // eslint-disable-next-line react/no-deprecated ReactDOM.render( { - myRef = ref; + myRef = ref!; }} /> , @@ -171,13 +172,17 @@ const create = props => { }; }; -function handleConfig(config, type) { - let newConfig = {}; +function isObject(obj: unknown) { + return {}.toString.call(obj) === '[object Object]'; +} + +function handleConfig(config: OpenProps, type?: MessageQuickProps['type']) { + let newConfig: MessageQuickProps = {}; if (typeof config === 'string' || React.isValidElement(config)) { newConfig.title = config; } else if (isObject(config)) { - newConfig = { ...config }; + newConfig = { ...config } as MessageQuickProps; } if (typeof newConfig.duration !== 'number') { newConfig.duration = 3000; @@ -189,41 +194,35 @@ function handleConfig(config, type) { return newConfig; } -function isObject(obj) { - return {}.toString.call(obj) === '[object Object]'; +function close() { + if (instance) { + instance.destroy(); + instance = null; + } } -function open(config, type) { +function open(config: OpenProps, type?: MessageQuickProps['type']) { close(); config = handleConfig(config, type); const timeoutId = guid(); instance = create({ ...config, timeoutId }); - if (config.duration > 0) { + if (config.duration! > 0) { const timeout = setTimeout(close, config.duration); timeouts[timeoutId] = timeout; } } -function close() { - if (instance) { - instance.destroy(); - instance = null; - } -} - /** * 创建提示弹层 - * @exportName show - * @param {Object} props 属性对象 + * @param config - 属性对象 */ -function show(config) { +function show(config: OpenProps) { open(config); } /** * 关闭提示弹层 - * @exportName hide */ function hide() { close(); @@ -231,55 +230,49 @@ function hide() { /** * 创建成功提示弹层 - * @exportName success - * @param {Object} props 属性对象 + * @param config - 属性对象 */ -function success(config) { +function success(config: OpenProps) { open(config, 'success'); } /** * 创建警告提示弹层 - * @exportName warning - * @param {Object} props 属性对象 + * @param config - 属性对象 */ -function warning(config) { +function warning(config: OpenProps) { open(config, 'warning'); } /** * 创建错误提示弹层 - * @exportName error - * @param {Object} props 属性对象 + * @param config - 属性对象 */ -function error(config) { +function error(config: OpenProps) { open(config, 'error'); } /** * 创建帮助提示弹层 - * @exportName help - * @param {Object} props 属性对象 + * @param config - 属性对象 */ -function help(config) { +function help(config: OpenProps) { open(config, 'help'); } /** * 创建加载中提示弹层 - * @exportName loading - * @param {Object} props 属性对象 + * @param config - 属性对象 */ -function loading(config) { +function loading(config: OpenProps) { open(config, 'loading'); } /** * 创建通知提示弹层 - * @exportName notice - * @param {Object} props 属性对象 + * @param config - 属性对象 */ -function notice(config) { +function notice(config: OpenProps) { open(config, 'notice'); } @@ -294,13 +287,32 @@ export default { notice, }; -export const withContext = WrappedComponent => { - const HOC = props => { +export interface ContextMessage { + show: (config?: MessageQuickProps) => void; + hide: () => void; + confirm: (config?: MessageQuickProps) => void; + success: (config?: MessageQuickProps) => void; + warning: (config?: MessageQuickProps) => void; + error: (config?: MessageQuickProps) => void; + help: (config?: MessageQuickProps) => void; + loading: (config?: MessageQuickProps) => void; + notice: (config?: MessageQuickProps) => void; +} +export interface WithContextMessageProps { + contextMessage: ContextMessage; +} + +export const withContext =

( + WrappedComponent: JSXElementConstructor

& C +) => { + type Props = React.JSX.LibraryManagedAttributes>; + const HOC = (props: Props) => { return ( {contextConfig => ( show({ ...config, contextConfig }), hide, diff --git a/components/message/toast2.tsx b/components/message/toast2.tsx index 6d6ae926e5..278c4750e5 100644 --- a/components/message/toast2.tsx +++ b/components/message/toast2.tsx @@ -4,17 +4,23 @@ import ConfigProvider from '../config-provider'; import Animate from '../animate'; import Message from './message'; import { obj, log, guid } from '../util'; +import type { + OpenProps, + MessageQuickProps, + MessageWrapperProps, + MessageWrapperItem, +} from './types'; const config = { top: 8, maxCount: 0, duration: 3000, }; +export type MessageConfig = Partial; -const MessageWrapper = props => { - // eslint-disable-next-line +const MessageWrapper = (props: MessageWrapperProps) => { const { prefix = 'next-', dataSource = [] } = props; - const [, forceUpdate] = useState(); + const [, forceUpdate] = useState>(); dataSource.forEach(i => { if (!i.timer) { @@ -67,10 +73,10 @@ const MessageWrapper = props => { const ConfigedMessages = ConfigProvider.config(MessageWrapper); -let messageRootNode; -let messageList = []; +let messageRootNode: HTMLDivElement | null; +let messageList: MessageWrapperProps['dataSource'] = []; -const createMessage = props => { +const createMessage = (props: MessageQuickProps & { key?: string }) => { const { key = guid('message-'), ...others } = props; if (!messageRootNode) { messageRootNode = document.createElement('div'); @@ -79,7 +85,7 @@ const createMessage = props => { const { maxCount, duration } = config; - const item = { + const item: MessageWrapperItem = { key, duration, ...others, @@ -91,6 +97,7 @@ const createMessage = props => { messageList.shift(); } + // eslint-disable-next-line react/no-deprecated ReactDOM.render( @@ -109,6 +116,7 @@ const createMessage = props => { typeof item.onClose === 'function' && item.onClose(); messageList.splice(idx, 1); + // eslint-disable-next-line react/no-deprecated ReactDOM.render( @@ -120,7 +128,7 @@ const createMessage = props => { }; }; -function close(key) { +function close(key?: string) { if (key) { const index = messageList.findIndex(item => item.key === key); messageList.splice(index, 1); @@ -129,6 +137,7 @@ function close(key) { } if (messageRootNode) { + // eslint-disable-next-line react/no-deprecated ReactDOM.render( @@ -138,13 +147,13 @@ function close(key) { } } -function handleConfig(config, type) { - let newConfig = {}; +function handleConfig(config: OpenProps, type?: MessageQuickProps['type']) { + let newConfig: MessageQuickProps = {}; if (typeof config === 'string' || React.isValidElement(config)) { newConfig.title = config; } else if (obj.typeOf(config) === 'Object') { - newConfig = { ...config }; + newConfig = { ...config } as MessageQuickProps; } if (type) { @@ -154,8 +163,8 @@ function handleConfig(config, type) { return newConfig; } -function open(type) { - return config => { +function open(type?: MessageQuickProps['type']) { + return (config: OpenProps) => { config = handleConfig(config, type); return createMessage(config); }; @@ -164,8 +173,9 @@ function open(type) { function destory() { if (!messageRootNode) return; if (messageRootNode) { + // eslint-disable-next-line react/no-deprecated ReactDOM.unmountComponentAtNode(messageRootNode); - messageRootNode.parentNode.removeChild(messageRootNode); + messageRootNode.parentNode!.removeChild(messageRootNode); messageRootNode = null; } } @@ -180,7 +190,7 @@ export default { notice: open('notice'), close, destory, - config(...args) { + config(...args: MessageConfig[]) { if (!useState) { log.warning('need react version > 16.8.0'); return; diff --git a/components/message/types.ts b/components/message/types.ts index d2be25d863..f36af9d9ec 100644 --- a/components/message/types.ts +++ b/components/message/types.ts @@ -1,148 +1,226 @@ -/// +import type React from 'react'; +import type { CommonProps } from '../util'; +import type { OverlayProps } from '../overlay'; +import type { ConsumerState } from '../config-provider/consumer'; +import type { Locale } from '../locale/types'; -import React from 'react'; -import { CommonProps } from '../util'; -import { OverlayProps } from '../overlay'; -interface HTMLAttributesWeak extends React.HTMLAttributes { - title?: any; -} +type HTMLAttributesWeak = Omit, 'title'>; +/** + * @api Message + */ export interface MessageProps extends HTMLAttributesWeak, CommonProps { /** * 反馈类型 + * @en type of message + * @defaultValue 'success' */ type?: 'success' | 'warning' | 'error' | 'notice' | 'help' | 'loading'; /** * 反馈外观 + * @en shape of message + * @defaultValue 'inline' */ shape?: 'inline' | 'addon' | 'toast'; /** * 反馈大小 + * @en size of message + * @defaultValue 'medium' */ size?: 'medium' | 'large'; /** * 标题 + * @en title of message */ title?: React.ReactNode; /** * 内容,非函数式调用下使用 + * @en content of message */ children?: React.ReactNode; /** * 默认是否显示 + * @en whether the message is visible in default + * @defaultValue false */ defaultVisible?: boolean; /** * 当前是否显示 + * @en whether the message is visible currently */ visible?: boolean; /** * 显示的图标类型,会覆盖内部设置的IconType,传false不显示图标 + * @en type of icon, overriding the internally type of icon */ iconType?: string | false; /** * 显示关闭按钮 + * @en whether to show the close button + * @defaultValue false */ closeable?: boolean; /** * 关闭按钮的回调 + * @en callback function triggered when close + * @defaultValue () =\> \{\} */ onClose?: () => void; /** * 关闭之后调用的函数 + * @en callback function triggered after closed + * @defaultValue () =\> \{\} */ afterClose?: () => void; /** * 是否开启展开收起动画 + * @en whether to enable expand and collapse animation + * @defaultValue true */ animation?: boolean; + /** + * 多语言文案 + * @en Locale + * @skip + */ + locale?: Locale['Message']; } +/** + * @api Message.show + * @remarks Message.show(props) 提供一个单例的调用方式,配置参数如下(继承 Overlay 的配置): + * + * ```js + * Message.show({ + * type: 'error', + * title: '错误', + * content: '请联系相关人员反馈!', + * hasMask: true + * }); + * ``` + * - + * `Message.show(props)` provides a singleton call with the following configuration parameters (inheriting `Overlay` configuration): + * ```js + * Message.show({ + * type: 'error', + * title: 'Error', + * content: 'Please contact admin feedback!', + * hasMask: true + * }); + * ``` + */ export interface MessageQuickProps extends Omit, CommonProps { /** * 反馈类型 + * @en type of message + * @defaultValue 'success' */ type?: 'success' | 'warning' | 'error' | 'notice' | 'help' | 'loading'; /** * 反馈大小 + * @en size of message + * @defaultValue 'medium' */ size?: 'medium' | 'large'; /** * 标题 + * @en title of message */ title?: React.ReactNode; /** * 内容,函数式调用下使用 + * @en content of message */ content?: React.ReactNode; /** - * 弹层相对于参照元素的定位, 详见开发指南的[定位部分](#定位) + * 弹层对齐方式,详情见 Overlay align + * @en alignment reference Overlay + * @defaultValue 'tc tc' */ align?: string | boolean; /** * 弹层相对于参照元素定位的微调 + * @en offset after positioned + * @defaultValue [0, 0] */ - offset?: Array; + offset?: Array; /** * 是否显示遮罩 + * @en whether to have a mask + * @defaultValue false */ hasMask?: boolean; /** * 显示持续时间,0表示一直存在,以毫秒为单位 + * @en show duration, 0 means always present, in milliseconds + * @defaultValue 3000 */ duration?: number; + /** + * @skip + */ timeoutId?: string; /** * 显示关闭按钮 + * @en whether to show the close button + * @defaultValue false */ closeable?: boolean; /** * 关闭按钮的回调 + * @en callback function triggered when close + * @defaultValue () =\> \{\} */ onClose?: () => void; /** * 关闭之后调用的函数 + * @en callback function triggered after closed + * @defaultValue () =\> \{\} */ afterClose?: () => void; /** * 是否开启展开收起动画 + * @en whether to enable expand and collapse animation + * @defaultValue true */ animation?: boolean; /** * 透传到弹层组件的属性对象 + * @en props of Overlay */ overlayProps?: OverlayProps; + /** + * @skip + */ + contextConfig?: ConsumerState; } -type OpenProps = string | React.ReactElement | MessageQuickProps; - -export default class Message extends React.Component { - static show(props: OpenProps): void; - static hide(): void; - static success(props: OpenProps): void; - static warning(props: OpenProps): void; - static error(props: OpenProps): void; - static help(props: OpenProps): void; - static loading(props: OpenProps): void; - static notice(props: OpenProps): void; - static config(props: OpenProps): void; +export type OpenProps = string | React.ReactElement | MessageQuickProps; + +export interface MessageWrapperItem extends MessageQuickProps { + timer?: ReturnType; + key: string; +} +export interface MessageWrapperProps { + prefix?: MessageQuickProps['prefix']; + dataSource: Array; }