【UI组件库】button组件

环境配置

  1. 确定安装了typescript、create-react-app、Node、npm
  2. 终端安装:npx create-react-app 项目名 --typescript如:npx create-react-app carad --typescript--typescript是给react项目添加TS支持。
  3. npx可以避免安装全局模块,可以调用项目内部安装的模块
  4. create-react-app创建的项目中自带ESLint的检查。但是我们用TS编写的代码,ESLint默认不开启对TS的检查,我们需要手动添加配置文件,.vscode文件夹中的settings.json中的代码:
{
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    { "language": "typescript", "autoFix": true },
    { "language": "typescriptreact", "autoFix": true }
  ]
}
  1. create-react-app创建的项目不支持sass,安装一个:npm install node-sass --save

色彩体系

  1. 可以在中国色找一些你喜欢的颜色作为本组件色彩体系
  2. 系统色板:基础色板+中性色板
  3. 产品色板:品牌色 + 功能色板
  4. 新建src/styles/_variables.scss,并配置颜色变量
  5. 不同浏览器元素不一致,比如每个浏览器的margin、padding等的默认值不统一,使用normalize.css来解决,他让元素在跨浏览器中保持一致。

normalize.css能做什么:保护有用的浏览器的默认样式,而不是完全去掉默认样式。为大多数元素提供一般化的样式。修复浏览器自身的bug并保证各种浏览器的一致性。优化CSS可用性。用详细的注释或文字来解释源码。

  1. 新建src/styles/_reboot.scss文件,直接将normalize.css的内容拷贝了过来,一些样式用我们定义的变量(在src/styles/_variables.scss中新增自定义的变量)替换了。

scss的@import与css的import不同,后者每次调用时都会创建一个额外的HTML请求,前者将文件包含在css中,不需要额外的HTML请求。
_reboot.scss、_variables.scss文件名都以_开头,他告诉scss不要将这些文件编译到css文件中,这些文件只能当作模块导入,不能单独使用。导入时不用添加_

  1. 在index.tsx中引入:import './styles/index.scss'

button

在这里插入图片描述
我们要给button添加不同的className,如btn-lg,btn-small等,使用classnames这个小工具:

  1. 安装:npm install classnames --savenpm install @types/classnames --save
  2. 导入:import classNames from 'classnames'
  3. 使用:他接收多个参数,参数为true才会以空格隔开拼接到classname,若为对象,当对象的值为true时才将键作为classname。具体如下:
    在这里插入图片描述
  4. 按钮分为普通按钮和link形式的a标签,针对不可点击的情况(disabled),button有disabled属性,可以直接使用,但是a标签没有,在本组件中,如果给a标签设置了disabled,那么我们在classname中加入disabled,后期通过样式来控制
import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react'
import classNames from 'classnames'
// 定义按钮的样式、尺寸、属性
export enum ButtonSize{
  Large = 'lg',
  Small = 'sm'
}
export enum ButtonType{
  Primary = 'primary',
  Default = 'default',
  Danger = 'danger',
  Link = 'link'
}
interface BaseButtonProps {
  className?: string;
  disabled?: boolean;/**设置 Button 的禁用 */
  size?: ButtonSize;/**设置 Button 的尺寸 */
  btnType?: ButtonType;/**设置 Button 的类型 */
  children: React.ReactNode;
  href?: string;
}

export const Button: FC<BaseButtonProps> = (props) => {
  // 函数式组件中,props会作为参数传进来
  const { 
    btnType,
    disabled,
    size,
    children,
    href
  } = props// 从props中取出外界给我们传入的属性
  
  // 根据属性添加classname: btn, btn-lg, btn-primary
  const classes = classNames('btn', {
    [`btn-${btnType}`]: btnType,
    [`btn-${size}`]: size,
    // 别忘了我们说过的特殊disabled属性,button天然支持这个属性,而a链接没有这么一个属性,
    // 所以我们需要用样式来模拟这个属性,所以我们给a链接添加一个特殊的class,然后把它们填充到button和a元素上。
    // a链接没有disabled属性,他的disabled直接添加到classname中:
	'disabled': (btnType === 'link') && disabled// type为link(标识a标签)并提供了disabled属性
  })
  // 展示的内容
  if (btnType === 'link' && href ) {
    return (
      <a
        className={classes}
        href={href}
        {...restProps}
      >
        {children}
      </a>
    )
  } else {
    return (
      <button
        className={classes}
        disabled={disabled}
        {...restProps}
      >
        {children}
      </button>
    )
  }
}

Button.defaultProps = {
  disabled: false,
  btnType: 'default',
}

export default Button;

使用:在app.tsc中导入:import Button, {ButtonType, ButtonSize} from ./components/Button/button.tsx

<Button disabled>hello</Button>
<Button btnType={ButtonType.Primary} size={ButtonSize.Large}>hello</Button>
<Button btnType={ButtonType.Link} href="http://www.baidu.com">baidu link</Button>

然后我们就能看到下面的按钮啦~
在这里插入图片描述

样式

基本样式

我们先来实现button的基本样式:

  1. 在src/styles/_variables.scss中为button添加一些变量
  2. 新建src/components/Button/_style.scss,里面的代码如下:
.btn {// button的基本样式
  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;// transparent透明
  padding: $btn-padding-y $btn-padding-x;
  font-size: $btn-font-size;
  border-radius: $border-radius;
  box-shadow: $btn-box-shadow;
  cursor: pointer;// 鼠标样式:手状
  transition: $btn-transition;

  // 禁用时的样式
  &.disabled,// 类名中有disabled
  &[disabled] {// 有disabled这个属性
    cursor: not-allowed;// 鼠标变成不可点击状态
    opacity: $btn-disabled-opacity;// 改变不透明度
    box-shadow: none;
    > * {// 给所有子元素设置
      pointer-events: none;// 不接收鼠标事件
    }
  }
}
  1. 在src/styles/index.scss中导入上面那个_style.scss:@import "../components/Button/style";

然后就得到了下面这些button,在禁用状态(disabled)下透明度变了
在这里插入图片描述

样式升级

在这里插入图片描述
在这里插入图片描述
思路:
(1)size不同时展示不同的大小,通过设置字体大小、padding、圆角尺寸来实现。
(2)type不同时展示不同的样式,通过设置背景颜色、边框、颜色、hover状态下的背景颜色、hover状态下的边框、hover状态下的颜色来实现。
(3)button的disabled状态和链接的disabled状态不同
当classname为btn-lg时要设置字体大小、padding、圆角尺寸,当classname为btn-sm时要设置字体大小、padding、圆角尺寸,那我们可以使用混合。
新建src/styles/_mixin.scss,并写以下代码,然后在src/styles/index.scss中引入mixin.scss

@mixin button-size($padding-y, $padding-x, $font-size, $border-raduis) {
  padding: $padding-y $padding-x;
  font-size: $font-size;
  border-radius: $border-raduis;
}

@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 {// hover状态
    color: $hover-color;
    background: $hover-background;
    border-color: $hover-border;    
  }
  &:focus,// focus状态
  &.focus {// focus类
    color: $hover-color;
    background: $hover-background;
    border-color: $hover-border;    
  }
  &:disabled,
  &.disabled {// 在disabled状态下,hover和focus都不改变样式
    color: $color;
    background: $background;
    border-color: $border;    
  }
}

src/components/Button/_style.scss中新增代码:

.btn-lg {
  @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg);
}
.btn-sm {
  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm);
}

.btn-primary {
  @include button-style($primary, $primary, $white)
}
.btn-danger {
  @include button-style($danger, $danger, $white)
}

.btn-default {
  @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;
  }
}

然后我们就可以得到以下样式啦
在这里插入图片描述

完善

我们这里的按钮应该具备button和a标签的属性,TS中&将多个类型合并为一个类型:

/*
ButtonHTMLAttributes<HTMLElement>: 获取所有的button属性
AnchorHTMLAttributes<HTMLElement>: 获取所有的a链接的属性
 */
type NativeButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>

有些属性在button上是必须的,但是在a链接上是可选的,所以我们将所有的属性设为可选的,TS的Partial<T>会将所有属性设为可选的,T是泛型

export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>

我们在定义classname的时候,需要将用户自定义的classname也添加进来。还需要取出剩下的属性定义在我们的标签上。

export const Button: FC<ButtonProps> = (props) => {
  const { 
    btnType,
    className,
    disabled,
    size,
    children,
    href,
    ...restProps// 取出剩下的属性
  } = props
  // 添加classname: btn, btn-lg, btn-primary
  // className是用户自定义的类名, 也需要添加进来
  const classes = classNames('btn', className, {
    [`btn-${btnType}`]: btnType,
    [`btn-${size}`]: size,
    'disabled': (btnType === 'link') && disabled
  })
  // 展示的内容
  if (btnType === 'link' && href ) {
    return (
      <a
        className={classes}
        href={href}
        {...restProps}
      >
        {children}
      </a>
    )
  } else {
    return (
      <button
        className={classes}
        disabled={disabled}
        {...restProps}
      >
        {children}
      </button>
    )
  }
}

button.tsc代码如下:

import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react'
import classNames from 'classnames'
export type ButtonSize = 'lg' | 'sm'
export type ButtonType = 'primary' | 'default' | 'danger' | 'link'

interface BaseButtonProps {
  className?: string;
  disabled?: boolean;/**设置 Button 的禁用 */
  size?: ButtonSize;/**设置 Button 的尺寸 */
  btnType?: ButtonType;/**设置 Button 的类型 */
  children: React.ReactNode;
  href?: string;
}

type NativeButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>

export const Button: FC<ButtonProps> = (props) => {
  const { 
    btnType,
    className,
    disabled,
    size,
    children,
    href,
    ...restProps
  } = props
  const classes = classNames('btn', className, {
    [`btn-${btnType}`]: btnType,
    [`btn-${size}`]: size,
    'disabled': (btnType === 'link') && disabled
  })
  if (btnType === 'link' && href ) {
    return (
      <a
        className={classes}
        href={href}
        {...restProps}
      >
        {children}
      </a>
    )
  } else {
    return (
      <button
        className={classes}
        disabled={disabled}
        {...restProps}
      >
        {children}
      </button>
    )
  }
}
Button.defaultProps = {
  disabled: false,
  btnType: 'default'
}

export default Button;

通用测试框架Jest

  1. 断言:判断测试出来的值是否是预期结果。
  2. Jest提供了各种API让我们来判断结果是否符合预期
  3. jest.test.js中测试代码如下:
//第一个参数是本次测试的名字
test('test common matcher', () => {
  expect( 2 + 2 ).toBe(4)
  expect(2 + 2).not.toBe(5)
})

test('test to be true or false', () => {
  expect(1).toBeTruthy()
  expect(0).toBeFalsy()
})

test('test number', () => {
  expect(4).toBeGreaterThan(3)
  expect(2).toBeLessThan(3)
})

test('test object', () => {
  expect({name: 'viking'}).toEqual({name: 'viking'})
})

create-react-app已经内置了Jest:npx jest jest.test.js直接运行jest.test.js文件。我们不希望每次运行时都写这个命令,那么我们可以使用npx jest jest.test.js --watch让Jest一直运行

react测试工具——react-testing-library

  1. 查看项目的package.json:react-script的版本在3.3.0之上,说明自动搭载了test-library和jest-dom,就不用再安装test-library和jest-dom的依赖了,如果版本在3.3.0之下,手动安装test-library的依赖:npm install --save-dev @testing-library/react,手动安装和jest-dom的依赖:npm install --save-dev @testing-library/jest-dom
  2. 将上述安装好的库以自定义扩展的形式装上去,create-react-app支持Testsetup文件,用于存放每次测试前的全局通用配置,我们每次运行jestnpm run test时,就可以先运行Testsetup文件。src/setupTests.ts代码如下:import '@testing-library/jest-dom/extend-expect';
  3. Jest会将以下三类文件看成测试文件:
    (1)__tests__文件夹下的.js.ts结尾的文件
    (2).test.js.test.ts结尾的文件
    (3).spec.js.spec.ts结尾的文件
  4. button.test.tsx中的代码:
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button, { ButtonProps } from './button'

test('our first react test case', () => {
  const wrapper = render(<Button>Nice</Button>)// 使用button组件
  const element = wrapper.getByText('Nice')// 使用getByText找element
  expect(element).toBeTruthy()// 输出应该为true
})
/*
正常情况下,点击按钮,调用函数
我们在测试时,如何追踪函数是否被调用呢,Jest给我们提供了Mock Functions,用于模拟代码中函数的真实实现,捕捉函数的调用,
调用了多少次,被谁调用等。比如代码中点击按钮后会发送异步请求,测试时不可能真的发一次异步请求,等着返回结果,
此时可以使用Mock Functions捕获函数的调用
jest.fn():创建出一个被监控的模拟函数
 */
const defaultProps = {
  onClick: jest.fn()
}

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

const disabledProps: ButtonProps = {
  disabled: true,
  onClick: jest.fn(),
}
//describe对测试分类,第一个参数依旧是测试名称
describe('test Button component', () => {
  //这里可以写it,也可以写test,都一样
  it('should render the correct default button', () => {
    //将defaultProps添加到button上
    const wrapper = render(<Button {...defaultProps}>Nice</Button>)
    //wrapper.getByText('Nice')是一个HTML,不是button,as HTMLButtonElement使用类型断言,让它变成button
    const element = wrapper.getByText('Nice') as HTMLButtonElement
    expect(element).toBeInTheDocument()// 判断组件是否在文档中
    expect(element.tagName).toEqual('BUTTON')// tagName都是大写的
    expect(element).toHaveClass('btn btn-default')// 测试是否有这些类名
    expect(element.disabled).toBeFalsy()//
    fireEvent.click(element)// 测试点击后能否触发。fireEvent用于触发用户事件。在element上面调用点击事件click方法
    expect(defaultProps.onClick).toHaveBeenCalled()// 期望结果:defaultProps上的onClick被调用
  })
  it('should render the correct component based on different props', () => {
    const wrapper = render(<Button {...testProps}>Nice</Button>)
    const element = wrapper.getByText('Nice')
    expect(element).toBeInTheDocument()
    expect(element).toHaveClass('btn-primary btn-lg klass')
  })
  it('should render a link when btnType equals link and href is provided', () => {
    const wrapper = render(<Button btnType='link' href="http://dummyurl">Link</Button>)
    const element = wrapper.getByText('Link')
    expect(element).toBeInTheDocument()
    expect(element.tagName).toEqual('A')
    expect(element).toHaveClass('btn btn-link')
  })
  it('should render disabled button when disabled set to true', () => {
    const wrapper = render(<Button {...disabledProps}>Nice</Button>)
    const element = wrapper.getByText('Nice') as HTMLButtonElement
    expect(element).toBeInTheDocument()
    expect(element.disabled).toBeTruthy()
    fireEvent.click(element)
    expect(disabledProps.onClick).not.toHaveBeenCalled()
  })
})
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值