React-View-UI已经更新了25个组件了,今天来写一下Message组件,这个组件目前还是很常用的,组件效果如下:

组件库文档如下:


具体的交互可以在http://react-view-ui.com:92/#/common/message进行体验。
一共提供了如下的API:

组件主要调用方式如下:
直接调用:
Message.info('This is an info message!')
传入对象调用(扩展API都需要传入对象):
Message.info({
content: 'This is an info message!',
duration: 3000,
position: 'bottom'
clearable: true,
style: {
fontSize: '12px'
}
});
import { CSSProperties } from 'react';
interface MessageProps<T> {
/**
* @description 对象类型传参时的内容
*/
content?: T;
/**
* @description Message类型
*/
type?: 'info' | 'success' | 'warning' | 'error' | 'normal' | 'loading';
/**
* @description 显示时间
* @default 3000ms
*/
duration?: number;
/**
* @description 显示位置
* @default top
*/
position?: 'top' | 'bottom';
/**
* @description 出现可清除按钮
* @default false
*/
clearable?: boolean;
/**
* @description 自定义样式
* @default {}
*/
style?: CSSProperties;
}
export type { MessageProps };
使用者所调用的Message.xxx函数其实就是在调用组件内部所定义的addInstance函数,往页面中不断加入div(Message组件),并在一定时延后自动关闭,这是组件的整体设计思路。
import React, { useState, useEffect, useMemo, useRef, CSSProperties } from 'react';
import ReactDOM from 'react-dom';
import { MessageProps } from './interface';
import './index.module.less';
import {
ExclamationCircleFilled,
CheckCircleFilled,
CloseCircleFilled,
LoadingOutlined,
CloseOutlined,
} from '@ant-design/icons';
let container: HTMLDivElement | null;
let topMessageNum: number = 0;
let bottomMessageNum: number = 0;
function addInstance(
type: 'info' | 'success' | 'warning' | 'error' | 'normal' | 'loading',
props: string | MessageProps<string>,
) {
let style: CSSProperties = {},
duration: number = 3000,
content,
position: 'top' | 'bottom' = 'top',
clearable = false;
if (typeof props === 'object') {
style = props.style || {};
duration = props.duration || 3000;
content = props.content;
position = props.position ? props.position : 'top';
clearable = props.clearable ? props.clearable : false;
} else if (typeof props === 'string') {
content = props;
}
const div = document.createElement('div');
if (container) {
container.appendChild(div);
} else {
container = document.createElement('div');
container.setAttribute('class', 'all-container');
document.body.appendChild(container);
container.appendChild(div);
}
setTimeout(() => {
if (position === 'top') {
topMessageNum--;
} else {
bottomMessageNum--;
}
container?.removeChild(div);
}, duration + 200);
ReactDOM.render(
<Message
style={style}
content={content}
type={type}
duration={duration}
position={position}
clearable={clearable}
/>,
div,
);
}
const Message = (props: MessageProps<string>) => {
const { style, content, type, duration, position, clearable } = props;
const [opac, setOpac] = useState(1);
const messageDom = useRef<any>(null);
useEffect(() => {
if (position === 'top') {
topMessageNum++;
} else {
bottomMessageNum++;
}
setTimeout(() => {
(messageDom.current as HTMLElement).style.transition = '0.2s linear';
(messageDom.current as HTMLElement).style.animation = 'none';
}, 500);
setTimeout(() => {
setOpac(0);
}, duration);
}, []);
useEffect(() => {
const transform = position || 'top';
(messageDom?.current as HTMLElement).style[transform] =
(transform === 'top' ? topMessageNum : bottomMessageNum) * 70 + 'px';
}, [topMessageNum, bottomMessageNum]);
const messageIcon = useMemo(() => {
if (type === 'info') {
return <ExclamationCircleFilled style={{ color: '#1890ff', fontSize: '16px' }} />;
} else if (type === 'error') {
return <CloseCircleFilled style={{ color: '#f53f3f', fontSize: '16px' }} />;
} else if (type === 'normal') {
return <></>;
} else if (type === 'success') {
return <CheckCircleFilled style={{ color: '#19b42a', fontSize: '16px' }} />;
} else if (type === 'warning') {
return <ExclamationCircleFilled style={{ color: '#fa7d00', fontSize: '16px' }} />;
} else if (type === 'loading') {
return <LoadingOutlined style={{ color: '#1890ff', fontSize: '16px' }} />;
}
}, [type]);
return (
<div className="message-container" style={{ opacity: opac, ...style }} ref={messageDom}>
{messageIcon}
<span className="toast-content">{content}</span>
{clearable && <CloseOutlined onClick={() => setOpac(0)} />}
</div>
);
};
Message.info = (props: string | MessageProps<string>) => {
return addInstance('info', props);
};
Message.success = (props: string | MessageProps<string>) => {
return addInstance('success', props);
};
Message.error = (props: string | MessageProps<string>) => {
return addInstance('error', props);
};
Message.normal = (props: string | MessageProps<string>) => {
return addInstance('normal', props);
};
Message.warning = (props: string | MessageProps<string>) => {
return addInstance('warning', props);
};
Message.loading = (props: string | MessageProps<string>) => {
return addInstance('loading', props);
};
export default Message;
Message组件的单元测试代码如下:
import React from 'react';
import Message from '../../Message/index';
import Enzyme from '../setup';
import mountTest from '../mountTest';
const { mount } = Enzyme;
describe('Message', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.runAllTimers();
});
it('test base Message show correctly', () => {
Message.info('this is a test');
expect(document.querySelectorAll('.all-container')).toHaveLength(1);
expect(document.querySelectorAll('.message-container .toast-content')[0].innerHTML).toBe(
'this is a test',
);
});
it('test click five nums Message show num correctly', () => {
for (let i = 0; i < 5; i++) {
Message.info('content');
}
expect(document.querySelectorAll('.all-container')[0].childNodes.length).toBe(5);
});
it('test bottom transform Message show correctly', () => {
Message.info({
content: 'this is a test',
duration: 3000,
position: 'bottom',
});
expect(
document.querySelectorAll('.message-container')[0].getAttribute('style')?.includes('bottom:'),
);
});
it('test clearable Message correctly', () => {
Message.info({
content: 'this is a test',
duration: 3000,
clearable: true,
});
expect(document.querySelectorAll('.message-container')[0].childNodes.length).toBe(3);
});
});
React-View-UI组件库线上链接:http://react-view-ui.com:92/#
github:https://github.com/fengxinhhh/React-View-UI-fs
npm:https://www.npmjs.com/package/react-view-ui
开源不易,欢迎学习和体验,喜欢请多多支持,有问题请留言。