W-Design组件库:Button组件

138 阅读5分钟

组件效果

image.png size控制不同尺寸(lg,sm)的按钮,type控制不同类型(primary,default,danger,link)的按钮,继承原生button、a标签的属性、事件

样式

公共样式 src/styles/_variables.scss

$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #6c757d !default;
$gray-700: #495057 !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;

$blue: #0d6efd !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #d63384 !default;
$red: #dc3545 !default;
$orange: #fd7e14 !default;
$yellow: #fadb14 !default;
$green: #52c41a !default;
$teal: #20c997 !default;
$cyan: #17a2b8 !default;

$primary: $blue  !default;
$secondary: $gray-600  !default;
$success: $green  !default;
$info: $cyan  !default;
$warning: $yellow  !default;
$danger: $red  !default;
$light: $gray-100  !default;
$dark: $gray-800  !default;

$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default;
$font-family-base: $font-family-sans-serif  !default;

// 字体大小
$font-size-base: 1rem !default; 
$font-size-lg: $font-size-base * 1.25 !default;
$font-size-sm: $font-size-base * .875 !default;
$font-size-root: null !default;

// 字重
$font-weight-lighter: lighter !default;
$font-weight-light: 300 !default;
$font-weight-normal: 400 !default;
$font-weight-bold: 700 !default;
$font-weight-bolder: bolder !default;
$font-weight-base: $font-weight-normal  !default;

// 行高
$line-height-base: 1.5 !default;
$line-height-lg: 2 !default;
$line-height-sm: 1.25 !default;

// 标题大小
$h1-font-size: $font-size-base * 2.5 !default;
$h2-font-size: $font-size-base * 2 !default;
$h3-font-size: $font-size-base * 1.75 !default;
$h4-font-size: $font-size-base * 1.5 !default;
$h5-font-size: $font-size-base * 1.25 !default;
$h6-font-size: $font-size-base  !default;

// 链接
$link-color: $primary  !default;
$link-decoration: none !default;
$link-hover-color: darken($link-color, 15%) !default;
$link-hover-decoration: underline !default;

// body
$body-bg: $white  !default;
$body-color: $gray-900  !default;
$body-text-align: null !default;

// 边框 和 border radius
$border-width: 1px !default;
$border-color: $gray-300  !default;

$border-radius: .25rem !default;
$border-radius-lg: .3rem !default;
$border-radius-sm: .2rem !default;

// 不同类型的 box shadow
$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;
$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;
$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;
$box-shadow-inset: inset 0 1px 2px rgba($black, .075) !default;

// 按钮
// 按钮基本属性
$btn-font-weight: 400;
$btn-padding-y: .375rem !default;
$btn-padding-x: .75rem !default;
$btn-font-family: $font-family-base  !default;
$btn-font-size: $font-size-base  !default;
$btn-line-height: $line-height-base  !default;

//不同大小按钮的 padding 和 font size
$btn-padding-y-sm: .25rem !default;
$btn-padding-x-sm: .5rem !default;
$btn-font-size-sm: $font-size-sm  !default;

$btn-padding-y-lg: .5rem !default;
$btn-padding-x-lg: 1rem !default;
$btn-font-size-lg: $font-size-lg  !default;

// 按钮边框
$btn-border-width: $border-width  !default;

// 按钮其他
$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;
$btn-disabled-opacity: .65 !default;

// 链接按钮
$btn-link-color: $link-color  !default;
$btn-link-hover-color: $link-hover-color  !default;
$btn-link-disabled-color: $gray-600  !default;


// 按钮 radius
$btn-border-radius: $border-radius  !default;
$btn-border-radius-lg: $border-radius-lg  !default;
$btn-border-radius-sm: $border-radius-sm  !default;

$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;

mixin样式src/styles/_mixin.scss

  1. button-size类通过padding来控制大小,根据size属性来调整大小
  2. button-style类控制基本、houver、focus、disabled的字体、背景、边框颜色,来区分primary、default、danger类型
@mixin button-size($padding-y, $padding-x, $font-size, $border-raduis) {
  // 设置内边距
  padding: $padding-y $padding-x;
  // 设置字体大小
  font-size: $font-size;
  // 设置边框圆角
  border-radius: $border-raduis;
}

// lighten() 函数用于调整颜色的亮度
@mixin button-style($background,
  $border,
  $color,
  $hover-background: lighten($background, 7.5%),
  $hover-border: lighten($border, 10%),
  $hover-color: $color,
) {
  color: $color;
  // 设置背景颜色
  background: $background;
  // 设置边框颜色
  border-color: $border;

  // 鼠标悬停时
  &:hover {
    // 设置颜色
    color: $hover-color;
    // 设置背景颜色
    background: $hover-background;
    // 设置边框颜色
    border-color: $hover-border;
  }

  // 获取焦点时
  &:focus,
  &.focus {
    // 设置颜色
    color: $hover-color;
    // 设置背景颜色
    background: $hover-background;
    // 设置边框颜色
    border-color: $hover-border;
  }

  // 禁用时
  &:disabled,
  &.disabled {
    // 设置颜色
    color: $color;
    // 设置背景颜色
    background: $background;
    // 设置边框颜色
    border-color: $border;
  }
}

src/components/Button/_style.scss
设置button组件的基本、不同大小、不同类型的样式,@include指令用于引入和使用Mixin

// 基本按钮样式
.btn {
  // 设置相对定位以便正确调整大小和位置
  position: relative;

  // 将按钮显示为内联块元素
  display: inline-block;

  // 设置字体粗细、行高等属性
  font-weight: $btn-font-weight;
  line-height: $btn-line-height;
  color: $body-color;

  // 不换行文本内容
  white-space: nowrap;
  text-align: center;
  vertical-align: middle;

  // 去除背景图片
  background-image: none;

  // 设置边框宽度及颜色
  border: $btn-border-width solid transparent;

  // 使用 mixin 生成其他尺寸相关属性(如 padding 和 font size)
  @include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $border-radius);

  // 添加阴影效果
  box-shadow: $btn-box-shadow;

  // 设置鼠标指针类型
  cursor: pointer;

  // 添加过渡动画设置
  transition: $btn-transition;

  // 当禁用时改变样式
  &.disabled,
  &[disabled] {
    // 更改光标为不可用状态
    cursor: not-allowed;

    // 设置透明度
    opacity: $btn-disabled-opacity;

    // 删除阴影效果
    box-shadow: none;

    // 使子元素无法接收点击事件
    >* {
      pointer-events: none;
    }
  }
}

// 大号按钮样式
.btn-lg {
  // 使用大号按钮尺寸 mixin
  @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg);
}

// 小号按钮样式
.btn-sm {
  // 使用小号按钮尺寸 mixin
  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm);
}

// 主要按钮样式
.btn-primary {
  // 使用主要按钮样式 mixin
  @include button-style($primary, $primary, $white);
}

// 危险按钮样式
.btn-danger {
  // 使用危险按钮样式 mixin
  @include button-style($danger, $danger, $white);
}

// 默认按钮样式
.btn-default {
  // 使用默认按钮样式 mixin
  @include button-style($white, $gray-400, $body-color, $white, $primary, $primary);
}

// 链接按钮样式
.btn-link {
  // 设置字体粗细为正常
  font-weight: $font-weight-normal;
  // 设置文字颜色为链接颜色
  color: $btn-link-color;
  // 设置无下划线链接
  text-decoration: $link-decoration;
  // 删除阴影效果
  box-shadow: none;

  // 鼠标悬停时的样式变化
  &:hover {
    color: $btn-link-hover-color;
    text-decoration: $link-hover-decoration;
  }

  // 获取焦点或激活时的样式变化
  &:focus,
  &.focus {
    text-decoration: $link-hover-decoration;
    box-shadow: none;
  }

  // 禁用的链接样式
  &:disabled,
  &.disabled {
    color: $btn-link-disabled-color;
    pointer-events: none;
  }
}

组件

  1. 根据传入的btntype、size生成对应的class,与classNamebtn基础样式合并成
  2. 根据btntype区分link标签和与按钮
import classNames from 'classnames'
import { ButtonProps, ButtonType } from './types'

const Button = (props: ButtonProps) => {
  const { btntype, disabled, size, children, href, className, ...restProps } =
    props

  // 创建一个类名字符串,包含默认的 btn 和传入的 className、btntype 和 size 等条件类名
  const classes = classNames('btn', className, {
    // 为当前按钮添加特定类型的样式,例如 'btn-primary' 或 'btn-link'
    [`btn-${btntype}`]: btntype,
    // 根据尺寸设置不同的样式,例如 'btn-sm' 或 'btn-lg'
    [`btn-${size}`]: size,
    // 如果按钮类型为链接且禁用状态为真,则将此类应用于按钮以表示它已被禁用
    disabled: btntype === ButtonType.Link && disabled,
  })

  // 检查按钮类型是否为链接并且具有 href 属性
  if (btntype === ButtonType.Link && href) {
    // 返回一个 a 标签,其 class 为创建的类名字符串,同时传递 href 和其他未处理的属性
    return (
      <a href={href} className={classes} {...restProps}>
        {children}
      </a>
    )
  } else {
    // 如果不是链接按钮,返回 button 元素,其 class 为创建的类名字符串,同时检查 disabled 属性
    return (
      <button className={classes} disabled={disabled} {...restProps}>
        {children}
      </button>
    )
  }
}

Button.defaultProps = {
  disabled: false,
  btntype: ButtonType.Default,
}
export default Button

组件类型src/components/Button/types.ts

import React from 'react';

// 定义按钮大小枚举
export enum ButtonSize {
  Large = 'lg', // 大号按钮
  Small = 'm', // 小号按钮
}

// 定义按钮样式枚举
export enum ButtonType {
  Primary = 'primary', // 主要按钮样式
  Default = 'default', // 默认按钮样式
  Danger = 'danger',   // 危险按钮样式
  Link = 'link',       // 链接按钮样式
}

// 定义基础按钮属性接口
export interface BaseButtonProps {
  className?: string;      // 类名
  disabled?: boolean;     // 是否禁用按钮
  size?: ButtonSize;      // 按钮尺寸
  btntype?: ButtonType;    // 按钮类型
  children: React.ReactNode;// 子节点内容
  href?: string;          // 如果是 a 标签, 设置跳转的 URL
}

// 创建继承原生按钮属性类型
type NativeButtonProps = BaseButtonProps & 
  React.ButtonHTMLAttributes<HTMLButtonElement>;
  
// 创建继承锚点(a)按钮属性类型
type AnchorButtonProps = BaseButtonProps & 
  React.AnchorHTMLAttributes<HTMLAnchorElement>;

// 定义按钮属性类型
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>;

测试用例

import { fireEvent, render, screen } from '@testing-library/react'
import Button from './button'
import { ButtonProps } from './types'

const defaultProps = {
  onClick: jest.fn(),
}

const testProps: ButtonProps = {
  btnType: 'primary',
  size: 'lg',
  className: 'klass',
}

const disabledProps: ButtonProps = {
  disabled: true,
  onClick: jest.fn(),
}

describe('test Button component', () => {
  // 测试默认按钮
  it('should render the correct default button', () => {
    // 渲染一个带有disabled属性的Button组件
    render(<Button {...defaultProps}>Hello</Button>)
    // 获取文本为'Hello'的元素
    const element = screen.getByText('Hello')
    // 断言该元素存在于文档中
    expect(element).toBeInTheDocument()
    // 断言该元素的标签名称为'BUTTON'
    expect(element.tagName).toEqual('BUTTON')
    // 断言该元素具有'btn btn-default'类名
    expect(element).toHaveClass('btn btn-default')
    // 触发元素的点击事件
    fireEvent.click(element)
    // 断言默认的onClick事件是否被调用
    expect(defaultProps.onClick).toHaveBeenCalled()
  })

  // 测试禁用按钮
  it('should render the correct component based on different props', () => {
    render(<Button {...testProps}>Hello</Button>)
    // 获取文本为'Hello'的元素
    const element = screen.getByText('Hello')
    // 断言元素存在于文档中
    expect(element).toBeInTheDocument()
    // 断言元素的标签名称为'BUTTON'
    expect(element.tagName).toEqual('BUTTON')
    // 断言元素具有类名'btn-primary btn-lg klass'
    expect(element).toHaveClass('btn-primary btn-lg klass')
  })

  // 测试link按钮
  it('should render a link when btnType equals link and href is provided', () => {
    // 渲染一个按钮组件,按钮类型为链接,链接地址为www.baidu.com
    render(
      <Button btnType="link" href="http://www.baidu.com">
        Hello
      </Button>
    )
    // 获取文档中的元素
    const element = screen.getByText('Hello')
    // 断言元素是否在文档中
    expect(element).toBeInTheDocument()
    // 断言元素是否有btn-link类
    expect(element).toHaveClass('btn-link')
    // 断言元素的标签名是否为A
    expect(element.tagName).toEqual('A')
    // 检查href属性
    expect(element).toHaveAttribute('href', 'http://www.baidu.com')
  })

  it('should render disabled button when disabled set to true', () => {
    // 渲染一个带有disabledProps属性的按钮组件
    render(<Button {...disabledProps}>Hello</Button>)
    // 获取按钮元素
    const element = screen.getByText('Hello') as HTMLButtonElement
    // 断言按钮元素存在于文档中
    expect(element).toBeInTheDocument()
    // 断言按钮元素被禁用
    expect(element.disabled).toBeTruthy()
    // 触发按钮元素的点击事件
    fireEvent.click(element)
    // 断言disabledProps的onClick属性未被调用
    expect(disabledProps.onClick).not.toHaveBeenCalled()
  })
})