组件效果
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
button-size类通过padding来控制大小,根据size属性来调整大小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;
}
}
组件
- 根据传入的
btntype、size生成对应的class,与className、btn基础样式合并成 - 根据
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()
})
})