【02】实现 Button 按钮组件

150 阅读1分钟

实现 Button 按钮组件

本系列从0到1实现一个 1:1 的 Ant Design Mobile UI 库,学习大佬们是如何编写一个 web Mobile 组件库的。 组件库文档地址:uab-ui-mobile.netlify.app image.png

实现代码:

import React, { forwardRef, useState, useRef, useImperativeHandle } from 'react'
import classNames from 'classnames'
import { isPromise } from '../utils/validate'
import { mergeProps } from '../utils/with-default-props'
import { NativeProps, withNativeProps } from '../utils/native-props'

const classPrefix = `uabm-button`

type NativeButtonProps = React.DetailedHTMLProps<
  React.ButtonHTMLAttributes<HTMLButtonElement>,
  HTMLButtonElement
>

export type ButtonProps = {
  block?: boolean
  fill?: 'solid' | 'outline' | 'none'
  type?: 'submit' | 'reset' | 'button'
  size?: 'mini' | 'small' | 'middle' | 'large'
  shape?: 'default' | 'rounded' | 'rectangular'
  color?: 'default' | 'primary' | 'success' | 'warning' | 'danger'
  loading?: boolean | 'auto'
  loadingText?: string
  loadingIcon?: React.ReactNode
  disabled?: boolean
  children?: React.ReactNode
  onClick?: (
    event: React.MouseEvent<HTMLButtonElement | MouseEvent>
  ) => void | Promise<void> | unknown
} & Pick<NativeButtonProps, 'onMouseDown' | 'onMouseUp' | 'onTouchStart' | 'onTouchEnd'> &
  NativeProps<
    | '--text-color'
    | '--background-color'
    | '--border-radius'
    | '--border-width'
    | '--border-style'
    | '--border-color'
  >

export type ButtonRef = {
  nativeElement: HTMLButtonElement | null
}

const defaultProps: ButtonProps = {
  block: false,
  fill: 'solid',
  type: 'button',
  size: 'middle',
  shape: 'default',
  color: 'default',
  loading: false,
  loadingIcon: <div>...</div>,
}

export const Button = forwardRef<ButtonRef, ButtonProps>((p, ref) => {
  const props = mergeProps(defaultProps, p)
  const [innerLoading, setInnerLoading] = useState(false)
  const nativeElementRef = useRef<HTMLButtonElement>(null)
  const loading = props.loading === 'auto' ? innerLoading : props.loading
  const disabled = props.disabled || loading

  // useImperativeHandle 和 forwardRef 同时使用,用于减少暴露给父组件的属性
  useImperativeHandle(ref, () => ({
    get nativeElement() {
      return nativeElementRef.current
    },
  }))

  const handleClick: React.MouseEventHandler<HTMLButtonElement> = async e => {
    if (!props.onClick) return

    const promise = props.onClick(e)

    if (isPromise(promise)) {
      try {
        setInnerLoading(true)
        await promise
        setInnerLoading(false)
      } catch (e) {
        setInnerLoading(false)
        throw e
      }
    }
  }

  return withNativeProps(
    props,
    <button
      ref={nativeElementRef}
      type={props.type}
      disabled={disabled}
      className={classNames(classPrefix, {
        [`${classPrefix}-${props.color}`]: !!props.color,
        [`${classPrefix}-block`]: props.block,
        [`${classPrefix}-disabled`]: disabled,
        [`${classPrefix}-loading`]: loading,
        [`${classPrefix}-${props.size}`]: props.size && props.size !== 'middle',
        [`${classPrefix}-fill-${props.fill}`]: props.fill && props.fill !== 'solid',
        [`${classPrefix}-shape-${props.shape}`]: props.shape && props.shape !== 'default',
      })}
      onClick={handleClick}
      onMouseDown={props.onMouseDown}
      onMouseUp={props.onMouseUp}
      onTouchStart={props.onTouchStart}
      onTouchEnd={props.onTouchEnd}
    >
      {loading ? (
        <div className={`${classPrefix}-loading-wrapper`}>
          {props.loadingIcon}
          {props.loadingText}
        </div>
      ) : (
        <span>{props.children}</span>
      )}
    </button>
  )
})

Button 属性

block

image.png

使用:

<Button block>Block Button</Button>

实际产物:

<button class="uabm-button-block">Block Button</button>

<style>
.uabm-button-block {
    display: block;
    width: 100%;
}
</style>

color

image.png

使用:

// 一共4种颜色 primary | success | danger | warning
<Button color='primary'>Primary</Button>

实际产物:

<button class="uabm-button uabm-button-primary">Block Button</button>

<style>
:root {
    /* 全局公用 css 主题色变量 */
    --uabm-color-primary: #1677ff;
}
.uabm-button {
    --background-color: #ffffff;
    --text-color: #333333;
    
    color: var(--text-color);                      /* 按钮字体颜色 */
    background-color: var(--background-color);     /* 按钮背景颜色 */
}

.uabm-button-primary {
    --text-color: #ffffff;                         /* 黑色字体变白色 */
    --background-color: var(--uabm-color-primary); /* 白色背景色变蓝色 */
}
</style>

disabled

image.png

使用:

<Button disabled>禁用按钮</Button>

实际产物:

<button class="uabm-button-disabled">Block Button</button>

<style>
.uabm-button-disabled {
    cursor: not-allowed;        /* 显示不可点击光标 */
    opacity: 0.4;               /* 40% 不透明度 */
}

/* 下面作用是让按钮不显示点击效果 */
.uabm-button-disabled::before { /* 按钮上方的遮罩层 */
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: #000;
    opacity: 0;
}
.uabm-button-disabled:active::before { /* 点击按钮出现 8% 透明度遮罩层动画效果 */
    opacity: 0.08;
}
.uabm-button-disabled:active::before { /* 当按钮被禁用时不显示点击动画效果 */
    display: none;
}
</style>

fill

image.png

使用:

<Button fill='outline'>outline</Button>
<Button fill='none'>none</Button>

实际产物:

<button class="uabm-button uabm-button-fill-outline">outline</button>
<button class="uabm-button uabm-button-fill-none">none</button>

<style>
.uabm-button {
    /* 设置 css 变量 */
    --background-color: #ffffff;
    --border-color: #eeeeee;
    
    color: var(--text-color);
    background-color: var(--background-color);
    border-radius: var(--border-radius);
}
.uabm-button-fill-outline {        /* 当 fill=outline 时改变 css 变量就实现了镂空的效果 */
  --background-color: transparent;
  --border-color: var(--color);
}
.uabm-button-fill-none {           /* 当 fill=none 时 */
  --background-color: transparent; /* 将按钮背景色设置为透明 */
  --border-width: 0px;             /* 将边框大小设置为0 */
}
</style>

loading / loadingText / loadingIcon

image.png

使用:

<Button loading loadingText='正在加载'>Loading</Button>

实际产物:

<button class="uabm-button-disabled"><svg ...></svg>正在加载</button>

/* css 同 disabled,就是在里面增加了一个 svg 图标 */

shape

image.png

使用:

<Button shape='rounded'>Rounded Button</Button>
<Button shape='rectangular'>Rectangular Button</Button>

实际产物:

<button class="uabm-button uabm-button-shape-rounded">Rounded Button</button>
<button class="uabm-button uabm-button-shape-rectangular">Rounded Button</button>

<style>
.uabm-button {
    --border-radius: 6px;        /* 默认边框半径 */
    
    border-radius: var(--border-radius);
}
.uabm-button-shape-rounded {
    --border-radius: 1000px;     /* 边框半径远大于高就是圆形 */
}
.uabm-button-shape-rectangular {
    --border-radius: 0;          /* 边框半径为0就是直角矩形 */
}
</style>

size

image.png

使用:

<Button size='mini'>Mini</Button>
<Button size='small'>Small</Button>
<Button size='middle'>Middle</Button>
<Button size='large'>Large</Button>

实际产物:

<button class="uabm-button-mini">Mini</button>
<button class="uabm-button-small">Small</button>
<button class="">Middle</button> /* 默认大小 */
<button class="uabm-button-large">Large</button>

<style>
.uabm-button-mini {
    padding: 3px 8px;
    font-size: 13px;
}
.uabm-button-small {
    padding: 3px 10px;
    font-size: 15px;
}
.uabm-button-large {
    padding: 11px 16px;
    font-size: 18px;
}
</style>
本文代码代码链接
GitHubgithub.com/uabjs/uab-u…
Giteegitee.com/shunyue/uab…