W-Design组件库:Alert 组件

103 阅读3分钟

组件效果

image.png

  1. 支持可配置title、是否显示关闭icon
  2. 传入关闭回调,关闭alert时触发的事件
  3. 类型四种可选针对四种不同的场景

组件结构

src/components/Alert/alert.tsx

  1. 基础样式alert+动态type组成组件的样式
  2. 当不显示关闭icon并且visible为true时,会三秒后关闭
  3. 在icon组件的onClick事件中执行onClose属性
  4. 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

  1. 将设置颜色、背景色抽离成公共mixinalert-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 }
    )
  })
})