实现 Button 按钮组件
本系列从0到1实现一个 1:1 的 Ant Design Mobile UI 库,学习大佬们是如何编写一个 web Mobile 组件库的。
组件库文档地址:uab-ui-mobile.netlify.app
实现代码:
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
使用:
<Button block>Block Button</Button>
实际产物:
<button class="uabm-button-block">Block Button</button>
<style>
.uabm-button-block {
display: block;
width: 100%;
}
</style>
color
使用:
// 一共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
使用:
<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
使用:
<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
使用:
<Button loading loadingText='正在加载'>Loading</Button>
实际产物:
<button class="uabm-button-disabled"><svg ...></svg>正在加载</button>
/* css 同 disabled,就是在里面增加了一个 svg 图标 */
shape
使用:
<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
使用:
<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>
| 本文代码 | 代码链接 |
|---|---|
| GitHub | github.com/uabjs/uab-u… |
| Gitee | gitee.com/shunyue/uab… |