diff --git a/components/message/__docs__/adaptor/index.jsx b/components/message/__docs__/adaptor/index.jsx deleted file mode 100644 index 4ddb0487e7..0000000000 --- a/components/message/__docs__/adaptor/index.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { Message } from '@alifd/next'; -import { Types } from '@alifd/adaptor-helper'; - -export default { - name: 'Message', - editor: () => ({ - props: [{ - name: 'level', - label: 'Shape', - type: Types.enum, - options: ['inline', 'toast', 'addon'], - default: 'inline' - }, { - name: 'size', - type: Types.enum, - options: ['large', 'medium'], - default: 'medium' - }, { - name: 'state', - label: 'Status', - type: Types.enum, - options: ['success', 'warning', 'error', 'notice', 'help', 'loading'], - default: 'success', - }, { - name: 'closable', - label: 'Close Included', - type: Types.bool, - default: false - }, { - name: 'width', - type: Types.number, - default: 400 - }, { - name: 'title', - type: Types.string, - default: 'Title' - }], - data: { - default: 'This item already has the label "travel", you can add a new label.' - } - }), - adaptor: ({ level, size, state, closable, width, title, data, style, ...others}) => { - return ( - {data} - ); - } -}; diff --git a/components/message/__docs__/adaptor/index.tsx b/components/message/__docs__/adaptor/index.tsx new file mode 100644 index 0000000000..760df2494f --- /dev/null +++ b/components/message/__docs__/adaptor/index.tsx @@ -0,0 +1,86 @@ +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: () => ({ + props: [ + { + name: 'level', + label: 'Shape', + type: Types.enum, + options: ['inline', 'toast', 'addon'], + default: 'inline', + }, + { + name: 'size', + type: Types.enum, + options: ['large', 'medium'], + default: 'medium', + }, + { + name: 'state', + label: 'Status', + type: Types.enum, + options: ['success', 'warning', 'error', 'notice', 'help', 'loading'], + default: 'success', + }, + { + name: 'closable', + label: 'Close Included', + type: Types.bool, + default: false, + }, + { + name: 'width', + type: Types.number, + default: 400, + }, + { + name: 'title', + type: Types.string, + default: 'Title', + }, + ], + data: { + default: 'This item already has the label "travel", you can add a new label.', + }, + }), + adaptor: ({ + level, + size, + state, + closable, + width, + title, + data, + style, + ...others + }: AdaptorProps) => { + return ( + + {data} + + ); + }, +}; diff --git a/components/message/__docs__/demo/size-shape/index.tsx b/components/message/__docs__/demo/size-shape/index.tsx index a21b1a1e8e..7c62c4da25 100644 --- a/components/message/__docs__/demo/size-shape/index.tsx +++ b/components/message/__docs__/demo/size-shape/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Message, Radio } from '@alifd/next'; -const types = ['success', 'warning', 'error', 'notice', 'help', 'loading']; +const types = ['success', 'warning', 'error', 'notice', 'help', 'loading'] as const; const sizeList = [ { value: 'medium', @@ -27,21 +27,21 @@ const shapeList = [ label: 'toast', }, ]; - +interface State { + size: 'medium' | 'large'; + shape: 'inline' | 'addon' | 'toast'; +} class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - size: 'medium', - shape: 'inline', - }; - } + state: State = { + size: 'medium', + shape: 'inline', + }; - handleSize = size => { + 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.jsx b/components/message/__docs__/theme/index.jsx deleted file mode 100644 index ede5845d6b..0000000000 --- a/components/message/__docs__/theme/index.jsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import '../../../demo-helper/style'; -import '../../style'; -import { Demo, DemoGroup, DemoHead, initDemo } from '../../../demo-helper'; -import Message from '../../index'; - -const i18nMap = { - 'en-us': { - title: 'Title', - content: 'This item already has the label "travel", You can add a new label.' - }, - 'zh-cn': { - title: '标题', - content: '现在不是一个买房的低点,建议慎重考虑。' - } -}; - -const shapes = ['inline', 'addon', 'toast']; -const types = ['success', 'warning', 'error', 'notice', 'help', 'loading']; - -const toFirstUpperCase = (str) => str && (str.substring(0, 1).toUpperCase() + str.substring(1)); - -class FunctionDemo extends React.Component { - constructor(props) { - super(props); - this.state = { - demoFunction: { - hasTitle: { - label: '有无标题', - value: 'true', - enum: [{ - label: '有', - value: 'true' - }, { - label: '无', - value: 'false' - }] - }, - closeable: { - label: '有无关闭按钮', - value: 'false', - enum: [{ - label: '有', - value: 'true' - }, { - label: '无', - value: 'false' - }] - } - } - }; - - this.onFunctionChange = this.onFunctionChange.bind(this); - } - - onFunctionChange(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 => ( - - {i18n.content} - - )); - return ({children}); - }); - return ( - - - {content} - - ); - }); - - return ( - - {newChildren} - - ); - } -} - -function render(i18n) { - ReactDOM.render(( -
- -
- ), document.getElementById('container')); -} - -window.renderDemo = function (lang) { - lang = lang || 'en-us'; - render(i18nMap[lang]); -}; -window.renderDemo(); -initDemo('message'); diff --git a/components/message/__docs__/theme/index.tsx b/components/message/__docs__/theme/index.tsx new file mode 100644 index 0000000000..236914b5b8 --- /dev/null +++ b/components/message/__docs__/theme/index.tsx @@ -0,0 +1,143 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import '../../../demo-helper/style'; +import '../../style'; +import { + Demo, + DemoGroup, + DemoHead, + initDemo, + type DemoFunctionDefineForObject, +} from '../../../demo-helper'; +import Message from '../../index'; + +const i18nMap = { + 'en-us': { + title: 'Title', + content: 'This item already has the label "travel", You can add a new label.', + }, + 'zh-cn': { + title: '标题', + content: '现在不是一个买房的低点,建议慎重考虑。', + }, +}; + +const shapes = ['inline', 'addon', 'toast'] as const; +const types = ['success', 'warning', 'error', 'notice', 'help', 'loading'] as const; + +const toFirstUpperCase = (str: string) => + str && str.substring(0, 1).toUpperCase() + str.substring(1); + +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: { + hasTitle: { + label: '有无标题', + value: 'true', + enum: [ + { + label: '有', + value: 'true', + }, + { + label: '无', + value: 'false', + }, + ], + }, + closeable: { + label: '有无关闭按钮', + value: 'false', + enum: [ + { + label: '有', + value: 'true', + }, + { + label: '无', + value: 'false', + }, + ], + }, + }, + }; + + this.onFunctionChange = this.onFunctionChange.bind(this); + } + + onFunctionChange(demoFunction: State['demoFunction']) { + this.setState({ + demoFunction, + }); + } + + render() { + const { i18n } = this.props; + const { demoFunction } = this.state; + const title = demoFunction.hasTitle.value === 'true' ? i18n.title : null; + const closeable = demoFunction.closeable.value === 'true'; + + const newChildren = shapes.map(shape => { + const content = types.map(type => { + const children = (['large', 'medium'] as const).map(size => ( + + {i18n.content} + + )); + return ( + + {children} + + ); + }); + return ( + + + {content} + + ); + }); + + return ( + + {newChildren} + + ); + } +} + +function render(i18n: I18N) { + ReactDOM.render( +
+ +
, + document.getElementById('container') + ); +} + +window.renderDemo = function (lang) { + lang = lang || 'en-us'; + render(i18nMap[lang]); +}; +window.renderDemo(); +initDemo('message'); diff --git a/components/message/__tests__/a11y-spec.js b/components/message/__tests__/a11y-spec.tsx similarity index 77% rename from components/message/__tests__/a11y-spec.js rename to components/message/__tests__/a11y-spec.tsx index 049fcf0571..f103325169 100644 --- a/components/message/__tests__/a11y-spec.js +++ 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.js b/components/message/__tests__/index-spec.js deleted file mode 100644 index 47ab1c7dd5..0000000000 --- a/components/message/__tests__/index-spec.js +++ /dev/null @@ -1,370 +0,0 @@ -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(); - }); - - it('should receive style prop', () => { - const wrapper = mount(); - assert(wrapper.prop('style').color === 'red'); - wrapper.unmount(); - }); - - it('should have type class', () => { - const types = ['success', 'warning', 'error', 'notice', 'help', 'loading']; - 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(); - }); - }); - - it('should have shape class', () => { - const shapes = ['inline', 'addon', 'toast']; - shapes.forEach(shape => { - const wrapper = mount(); - assert(wrapper.find(`.next-message.next-${shape}`).length === 1); - wrapper.unmount(); - }); - }); - - it('should have size class', () => { - const sizes = ['medium', 'large']; - sizes.forEach(size => { - const wrapper = mount(); - assert(wrapper.find(`.next-message.next-${size}`).length === 1); - wrapper.unmount(); - }); - }); - - it('should show title if you pass it', () => { - const wrapper = mount(); - assert(wrapper.find('.next-message-title').text() === 'title'); - wrapper.unmount(); - }); - - it('should show content if you pass it', () => { - const wrapper = mount(content); - assert(wrapper.find('.next-message-content').text() === 'content'); - wrapper.unmount(); - }); - - it('should not show icon if set iconType to false', () => { - const wrapper = mount(); - assert(wrapper.find(Icon).length === 0); - wrapper.unmount(); - }); - - it('should custom icon type', () => { - const wrapper = mount(); - assert(wrapper.find(Icon).prop('type') === 'smile'); - wrapper.unmount(); - }); - - 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 or hide under control', () => { - const wrapper = mount(); - assert(wrapper.find('.next-message').length === 0); - wrapper.setProps({ - visible: true, - }); - assert(wrapper.find('.next-message').length === 1); - wrapper.unmount(); - }); -}); -describe('toast', done => { - it('should render nowrap message when content too long[Overlay case]', done => { - 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); - - Message.hide(); - - setTimeout(done, 500); - }); - - it('should render message when only pass content string', done => { - Message.show('content'); - assert(document.querySelector('.next-overlay-wrapper .next-message').innerText.trim() === 'content'); - - Message.hide(); - setTimeout(done, 500); - }); - - it('should render message when only pass content react element', done => { - Message.show(content); - assert(document.querySelector('.next-overlay-wrapper .next-message-title i').innerText.trim() === 'content'); - - Message.hide(); - - setTimeout(done, 500); - }); - - it('should render message when pass config object', done => { - Message.show({ - type: 'warning', - content: 'content', - }); - assert( - document.querySelector('.next-overlay-wrapper .next-message.next-message-warning').innerText.trim() === - 'content' - ); - - Message.hide(); - setTimeout(done, 500); - }); - - it('should close message after duration and call afterClose method', done => { - let called = false; - - Message.show({ - content: 'content', - duration: 100, - afterClose: () => { - called = true; - }, - }); - setTimeout(() => { - assert(document.querySelector('.next-overlay-wrapper .next-message') === null); - assert(called); - done(); - }, 1000); - }); - - it('should show message all the time when set duration to 0', done => { - Message.show({ - content: 'content', - duration: 0, - }); - - setTimeout(() => { - assert(document.querySelector('.next-overlay-wrapper .next-message.next-message') !== null); - Message.hide(); - }, 500); - - setTimeout(done, 1000); - }); - - it('should hide message when call hide method', done => { - let called = false; - - Message.show({ - content: 'content', - duration: 100, - afterClose: () => { - called = true; - }, - }); - - setTimeout(() => { - Message.hide(); - }, 500); - - setTimeout(() => { - assert(document.querySelector('.next-overlay-wrapper .next-message') === null); - assert(called); - done(); - }, 1000); - }); - - it('should hide message when click this close link', done => { - // it will trigger page reload when click `
` in ie9 or ie10 of karma web driver, even if its href has been set to `javascript:;` - // and then it will case test failed, so skip this case. - 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); - }); -}); - -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( - - -
- {methods.map(method => ( - - - - ))} -
-
-
- ); - const innerBtn = document.querySelectorAll('.near-message .near-message-content .near-btn-primary'); - 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); - - // wrapper.unmount(); - // }); - // }); -}); - -describe('toast quick-calling', () => { - const avaliableMethods = ['success', 'warning', 'error', 'notice', 'help', 'loading']; - - 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); - }); - } -}); -describe('Message v2', () => { - it('should support config to open multiple instance', done => { - 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); - }); -}); diff --git a/components/message/__tests__/index-spec.tsx b/components/message/__tests__/index-spec.tsx new file mode 100644 index 0000000000..fed1d9cfde --- /dev/null +++ b/components/message/__tests__/index-spec.tsx @@ -0,0 +1,275 @@ +import React from 'react'; +import Button from '../../button'; +import ConfigProvider from '../../config-provider'; +import { env } from '../../util'; +import Message from '../index'; +import '../style'; + +describe('Message', () => { + it('should receive className prop', () => { + cy.mount(); + cy.get('.next-message.custom').should('have.length', 1); + }); + + it('should receive style prop', () => { + 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'] as const; + types.forEach(type => { + 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'] as const; + shapes.forEach(shape => { + cy.mount(); + cy.get(`.next-message.next-${shape}`).should('have.length', 1); + }); + }); + + it('should have size class', () => { + const sizes = ['medium', 'large'] as const; + sizes.forEach(size => { + cy.mount(); + cy.get(`.next-message.next-${size}`).should('have.length', 1); + }); + }); + + it('should show title if you pass it', () => { + cy.mount(); + cy.get('.next-message-title').should('have.text', 'title'); + }); + + it('should show content if you pass it', () => { + cy.mount(content); + cy.get('.next-message-content').should('have.text', 'content'); + }); + + it('should not show icon if set iconType to false', () => { + cy.mount(); + cy.get('.next-icon').should('have.length', 0); + }); + + it('should custom icon type', () => { + cy.mount(); + cy.get('.next-icon').should('have.class', 'next-icon-smile'); + }); + + 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', () => { + cy.mount().as( + 'Message' + ); + cy.get('.next-message').should('have.length', 0); + cy.rerender('Message', { visible: true }); + cy.get('.next-message').should('have.length', 1); + }); +}); +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); + 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(); + }); + + it('should render message when only pass content string', () => { + Message.show('content'); + cy.get('.next-overlay-wrapper .next-message').should('have.text', 'content'); + Message.hide(); + }); + + it('should render message when only pass content react element', () => { + Message.show(content); + cy.get('.next-overlay-wrapper .next-message-title i').should('have.text', 'content'); + Message.hide(); + }); + + it('should render message when pass config object', () => { + Message.show({ + type: 'warning', + content: 'content', + }); + cy.get('.next-overlay-wrapper .next-message.next-message-warning').should( + 'have.text', + 'content' + ); + Message.hide(); + }); + + it('should close message after duration and call afterClose method', () => { + const afterClose = cy.spy(); + Message.show({ + content: 'content', + duration: 100, + afterClose, + }); + 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', () => { + cy.clock(); + Message.show({ + content: 'content', + duration: 0, + }); + // 验证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', () => { + const afterClose = cy.spy(); + Message.show({ + content: 'content', + afterClose, + }); + 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 => { + // it will trigger page reload when click `
` in ie9 or ie10 of karma web driver, even if its href has been set to `javascript:;` + // and then it will case test failed, so skip this case. + if (env.ieVersion === 9 || env.ieVersion === 10) { + return done(); + } + Message.show({ + content: 'content', + duration: 0, + closeable: true, + }); + 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.prefix > nearest ConfigProvider.prefix > further ConfigProvider.prefix', () => { + const methods = ['success', 'warning', 'error', 'notice', 'help', 'loading'] as const; + cy.mount( + + +
+ {methods.map(method => ( + + + + ))} +
+
+
+ ); + cy.get('.near-message .near-message-content .near-btn-primary').should( + 'have.length', + methods.length + ); + }); + + 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(); + + 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'] as const; + + for (const method of avaliableMethods) { + it(`render ${method}`, done => { + 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', () => { + Message.config({}); + 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.js b/components/message/__tests__/issue-spec.js deleted file mode 100644 index 11a0980b1c..0000000000 --- a/components/message/__tests__/issue-spec.js +++ /dev/null @@ -1,63 +0,0 @@ -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( - - 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); - }); -}); diff --git a/components/message/__tests__/issue-spec.tsx b/components/message/__tests__/issue-spec.tsx new file mode 100644 index 0000000000..5f4902b6b5 --- /dev/null +++ b/components/message/__tests__/issue-spec.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import Message from '../index'; +import '../style'; + +describe('Message issues', () => { + // Fix: https://github.com/alibaba-fusion/next/issues/3910 + // rest message symbol icon 'vertical-align: top' + it('should align icon & content', () => { + cy.mount( + + content + + ); + 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.d.ts b/components/message/index.d.ts deleted file mode 100644 index d2be25d863..0000000000 --- a/components/message/index.d.ts +++ /dev/null @@ -1,148 +0,0 @@ -/// - -import React from 'react'; -import { CommonProps } from '../util'; -import { OverlayProps } from '../overlay'; -interface HTMLAttributesWeak extends React.HTMLAttributes { - title?: any; -} - -export interface MessageProps extends HTMLAttributesWeak, CommonProps { - /** - * 反馈类型 - */ - type?: 'success' | 'warning' | 'error' | 'notice' | 'help' | 'loading'; - - /** - * 反馈外观 - */ - shape?: 'inline' | 'addon' | 'toast'; - - /** - * 反馈大小 - */ - size?: 'medium' | 'large'; - - /** - * 标题 - */ - title?: React.ReactNode; - - /** - * 内容,非函数式调用下使用 - */ - children?: React.ReactNode; - - /** - * 默认是否显示 - */ - defaultVisible?: boolean; - - /** - * 当前是否显示 - */ - visible?: boolean; - - /** - * 显示的图标类型,会覆盖内部设置的IconType,传false不显示图标 - */ - iconType?: string | false; - - /** - * 显示关闭按钮 - */ - closeable?: boolean; - - /** - * 关闭按钮的回调 - */ - onClose?: () => void; - - /** - * 关闭之后调用的函数 - */ - afterClose?: () => void; - - /** - * 是否开启展开收起动画 - */ - animation?: boolean; -} - -export interface MessageQuickProps extends Omit, CommonProps { - /** - * 反馈类型 - */ - type?: 'success' | 'warning' | 'error' | 'notice' | 'help' | 'loading'; - - /** - * 反馈大小 - */ - size?: 'medium' | 'large'; - /** - * 标题 - */ - title?: React.ReactNode; - - /** - * 内容,函数式调用下使用 - */ - content?: React.ReactNode; - /** - * 弹层相对于参照元素的定位, 详见开发指南的[定位部分](#定位) - */ - align?: string | boolean; - - /** - * 弹层相对于参照元素定位的微调 - */ - offset?: Array; - /** - * 是否显示遮罩 - */ - hasMask?: boolean; - - /** - * 显示持续时间,0表示一直存在,以毫秒为单位 - */ - duration?: number; - timeoutId?: string; - - /** - * 显示关闭按钮 - */ - closeable?: boolean; - - /** - * 关闭按钮的回调 - */ - onClose?: () => void; - - /** - * 关闭之后调用的函数 - */ - afterClose?: () => void; - - /** - * 是否开启展开收起动画 - */ - animation?: boolean; - /** - * 透传到弹层组件的属性对象 - */ - overlayProps?: OverlayProps; -} - -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; -} diff --git a/components/message/index.jsx b/components/message/index.tsx similarity index 68% rename from components/message/index.jsx rename to components/message/index.tsx index 4e0c1122ce..2612a0ec83 100644 --- a/components/message/index.jsx +++ 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.jsx b/components/message/message.tsx similarity index 74% rename from components/message/message.jsx rename to components/message/message.tsx index f3f9a1a73f..f20d80d162 100644 --- a/components/message/message.jsx +++ 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-', @@ -86,10 +59,13 @@ class Message extends Component { }; state = { - visible: typeof this.props.visible === 'undefined' ? this.props.defaultVisible : this.props.visible, + visible: + typeof this.props.visible === 'undefined' + ? this.props.defaultVisible + : this.props.visible, }; - static getDerivedStateFromProps(props) { + static getDerivedStateFromProps(props: MessageProps) { if ('visible' in props) { return { visible: props.visible, @@ -105,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, @@ -120,11 +94,8 @@ class Message extends Component { size, title, children, - defaultVisible, - visible: propsVisible, iconType: icon, closeable, - onClose, afterClose, animation, rtl, @@ -133,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`; @@ -144,15 +114,21 @@ class Message extends Component { [`${prefix}${size}`]: size, [`${prefix}title-content`]: !!title, [`${prefix}only-content`]: !title && !!children, - [className]: className, + [className!]: className, }); const newChildren = visible ? ( -
+
{closeable ? ( diff --git a/components/message/mobile/index.jsx b/components/message/mobile/index.tsx similarity index 100% rename from components/message/mobile/index.jsx rename to components/message/mobile/index.tsx diff --git a/components/message/style.js b/components/message/style.ts similarity index 100% rename from components/message/style.js rename to components/message/style.ts diff --git a/components/message/toast.jsx b/components/message/toast.tsx similarity index 69% rename from components/message/toast.jsx rename to components/message/toast.tsx index b759d27efc..0faf7465bf 100644 --- a/components/message/toast.jsx +++ 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() { + 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,18 +146,19 @@ const create = props => { destroyed = true; }; + // eslint-disable-next-line react/no-deprecated ReactDOM.render( { - myRef = ref; + myRef = ref!; }} /> , div, - function() { + function () { mask = myRef; if (mask && destroyed) { destroy(); @@ -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.jsx b/components/message/toast2.tsx similarity index 80% rename from components/message/toast2.jsx rename to components/message/toast2.tsx index 6d6ae926e5..278c4750e5 100644 --- a/components/message/toast2.jsx +++ 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 new file mode 100644 index 0000000000..f36af9d9ec --- /dev/null +++ b/components/message/types.ts @@ -0,0 +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'; + +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; + /** + * 是否显示遮罩 + * @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; +} + +export type OpenProps = string | React.ReactElement | MessageQuickProps; + +export interface MessageWrapperItem extends MessageQuickProps { + timer?: ReturnType; + key: string; +} +export interface MessageWrapperProps { + prefix?: MessageQuickProps['prefix']; + dataSource: Array; +}