组件效果
- 支持可配置title、是否显示关闭icon
- 传入关闭回调,关闭alert时触发的事件
- 类型四种可选针对四种不同的场景
组件结构
src/components/Alert/alert.tsx
- 基础样式
alert+动态type组成组件的样式 - 当不显示关闭icon并且
visible为true时,会三秒后关闭 - 在icon组件的
onClick事件中执行onClose属性 Transition组件包裹Alert组件,使Alert组件向顶部过渡
import { FC, useEffect, useRef, useState } from 'react'
import { AlertProps } from './types'
import classNames from 'classnames'
import { Icon } from '../Icon'
import { Transition } from '../Transition'
export const Alert: FC<AlertProps> = (props) => {
const { title, children, closable, onClose, type } = props
const classes = classNames('alert', {
[`alert-${type}`]: type,
})
const [visible, setVisible] = useState(true)
const timer = useRef<NodeJS.Timeout>()
const handleClick = () => {
setVisible(false)
onClose && onClose()
}
useEffect(() => {
if (visible && !closable) {
timer.current = setTimeout(() => {
setVisible(false)
}, 3000)
}
return () => {
clearTimeout(timer.current)
}
}, [closable, visible])
return (
<Transition in={visible} animation="zoom-in-top" timeout={300} wrapper>
<div className={classes}>
{title && <h4 className="alert-title">{title}</h4>}
<p className="alert-message">{children}</p>
{closable && (
<i>
<Icon
icon="times"
className="window-close"
size="lg"
onClick={handleClick}
data-testid="test-icon"
/>
</i>
)}
</div>
</Transition>
)
}
Alert.defaultProps = {
closable: true,
type: 'primary',
}
组件类型
src/components/Alert/types.ts
export type AlertType = 'success' | 'primary' | 'warning' | 'danger'
export interface AlertProps {
/**
* 标题
*/
title: string
/**
* 是否可以关闭
*/
closable?: boolean
/**
* 关闭回调
*/
onClose?: () => void
children?: React.ReactNode
/**
* 弹窗类型
*/
type: AlertType
}
组件样式
src/components/Alert/_style.scss
- 将设置颜色、背景色抽离成公共mixin
alert-color,不同type的对应不同的颜色
.alert {
// 设置宽度
width: $alert-width;
// 设置颜色
@include alert-color($primary, $white);
// 设置圆角
border-radius: $alert-border-radius;
// 设置边框宽度
border-width: $border-width;
// 设置边框样式
border-style: solid;
// 设置内边距
padding: $btn-padding-y $btn-padding-x;
// 设置外边距
margin: auto;
// 设置标题样式
.alert-title {
// 设置底部边距
margin-bottom: 0;
// 设置顶部边距
margin-top: 0;
// 设置行高
line-height: $alert-line-height;
// 设置字体大小
font-size: $alert-title-font-size;
}
// 设置消息样式
.alert-message {
// 设置顶部边距
margin-top: 0;
// 设置底部边距
margin-bottom: 0;
// 设置行高
line-height: $alert-line-height;
// 设置字体大小
font-size: $alert-message-font-size;
}
// 设置定位
position: relative;
// 设置最后一个图标样式
> i:last-child {
// 设置定位
position: absolute;
// 设置右边距
right: $alert-icon-right;
// 设置上边距
top: $alert-icon-top;
// 设置字体样式
font-style: normal;
// 设置鼠标样式
cursor: pointer;
// 设置字体粗细
font-weight: $alert-icon-font-weight;
// 设置字体大小
font-size: $alert-icon-font-size;
}
}
.alert-success {
@include alert-color($success, $white);
}
.alert-danger {
@include alert-color($danger, $white);
}
.alert-warning {
@include alert-color($warning, $white);
}
@mixin alert-color($background, $color) {
color: $color;
background-color: $background;
}
组件测试
/* eslint-disable testing-library/no-node-access */
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { AlertProps } from './types'
import { Alert } from './alert'
const testAlertProp: AlertProps = {
title: 'testAlert',
closable: true,
type: 'primary',
onClose: jest.fn(),
}
const testSuccessAlertProp: AlertProps = {
title: 'testSuccessAlert',
closable: false,
type: 'success',
}
describe('test Alert component', () => {
it('should render the correct default Alert', async () => {
// 渲染一个Alert组件
render(<Alert {...testAlertProp}>Alert</Alert>)
// 查询Alert组件
const element = screen.queryByText('Alert')
// 断言Alert组件被渲染
expect(element).toBeInTheDocument()
// 断言Alert组件的标签名称为P
expect(element?.tagName).toEqual('P')
// 断言Alert组件的class为alert-message
expect(element).toHaveClass('alert-message')
// 查询Alert组件的title
const titleElement = screen.queryByText('testAlert')
// 断言titleElement被渲染
expect(titleElement).toBeInTheDocument()
// 断言titleElement的class为alert-title
expect(titleElement).toHaveClass('alert-title')
// 断言titleElement的父节点为element的父节点
expect(titleElement?.parentNode).toBe(element?.parentNode)
// 获取test-icon的元素
const iconElement = screen.getByTestId('test-icon')
// 触发点击事件
fireEvent.click(iconElement)
// 断言testAlertProp.onClose被调用
expect(testAlertProp.onClose).toHaveBeenCalled()
// 等待元素不在文档中
await waitFor(() => {
expect(element).not.toBeInTheDocument()
})
})
// 测试根据不同的props渲染正确的组件
it('should render the correct component based on different props', async () => {
// 渲染一个Alert组件,传入testSuccessAlertProp和Success作为参数
render(<Alert {...testSuccessAlertProp}>Success</Alert>)
// 查询文本为Success的元素
const element = screen.queryByText('Success')
// 预期元素存在
expect(element).toBeInTheDocument()
// 预期元素的父节点有alert-success类名
expect(element?.parentNode).toHaveClass('alert alert-success')
// 查询文本为testSuccessAlert的元素
const titleElement = screen.queryByText('testSuccessAlert')
// 预期元素存在
expect(titleElement).toBeInTheDocument()
// 预期元素有alert-title类名
expect(titleElement).toHaveClass('alert-title')
// 预期元素的父节点是element的父节点
expect(titleElement?.parentNode).toBe(element?.parentNode)
// 等待3秒,预期element不存在
await waitFor(
() => {
expect(element).not.toBeInTheDocument()
},
// 测试用例的超时时间应该略大于组件中的定时器时间,以确保元素在预期的时间内被移除,同时留出一些额外的时间来处理可能的延迟
{ timeout: 3400 }
)
})
})