react组件库源码+ 单测解析(Button和ButtonGroup 按钮组件)

前言

之前的组件库相关文章代码:

继续分析react组件源码,代码来源于t-design的button和arco design(飞书的组件库)的ButtonGroup组件。

button组件dom结构解析

以下的RenderTag是指可能是button标签,div标签或者a标签。RenderTag的实现:

  const RenderTag = useMemo(() => {
    if (!tag && href) return 'a';
    if (!tag && disabled) return 'div';
    return tag || 'button';
  }, [tag, href, disabled]);

为啥会有div标签呢?

因为禁用按钮 <button disabled>无法显示 Popup 浮层信息,可通过修改 tag=div 解决这个问题。

然后a标签是为了有按钮可能会跳转页面,不属于button的功能范围了,所以支持了a标签的按钮渲染。下面接着分析一下RenderTag组件上的参数是啥意思。


  <RenderTag
      {...buttonProps}
      href={href}
      type={type}
      ref={ref}
      disabled={disabled || loading}
      className={classNameProps}
      onClick={!disabled && !loading ? onClick : undefined}
    >
      {iconNode}
      {children && <span className={`${classPrefix}-button__text`}>{children}</span>}
      {suffix && <span className={`${classPrefix}-button__suffix`}>{suffix}</span>}
    </RenderTag>

  • buttonProps是什么呢,是透传给button元素(div或者a标签同理)的值,比如title属性。
  • 跳转地址。href 存在时,按钮标签默认使用 <a> 渲染;如果指定了 tag 则使用指定的标签渲染
  • type:按钮类型。可选项:submit/reset/button
  • ref,用来获取RenderTag的Ref的
  • disabled: button的禁用状态
  • onClick:点击事件的注册函数,注意当按钮是disabled和loading态的话,点击是无效的
  • iconNode是icon的组件,比如svg

实现的代码如下:

let iconNode = icon;
if (loading) iconNode = <Loading loading={loading} inheritColor={true} />;

如果是loading态,就渲染Loading组件,这个组件细节我们不用去了解,后面的文章会专门写这个组件。Loading组价你现在看来可以理解为简单的一个转菊花

image.png

  • children一般情况是渲染button文字,当然也可以是ReactNode

  • suffix 右侧内容,可用于定义右侧图标,

button组件到这里就差不多了,最后我们看下其他参数,基本上都是css的表现参数。

  const classNameProps = classNames(
    className,
    [
      `${classPrefix}-button`, // button的基本样式
      // 组件风格,依次为默认色、品牌色、危险色、警告色、成功色。可选项:default/primary/danger/warning/success
      `${classPrefix}-button--theme-${renderTheme}`, 
      // 按钮形式,基础、线框、虚线、文字。可选项:base/outline/dashed/text
      `${classPrefix}-button--variant-${variant}`,
    ],
    {
      // 按钮形状,有 4 种:长方形、正方形、圆角长方形、圆形。可选项:rectangle/square/round/circle
      [`${classPrefix}-button--shape-${shape}`]: shape !== 'rectangle',
      // 是否为幽灵按钮(镂空按钮)
      [`${classPrefix}-button--ghost`]: ghost,
      // 是否是loading态 主要是 设置 cursor: default;
      [`${classPrefix}-is-loading`]: loading,
      // 是否是disabled态,主要是设置颜色
      [`${classPrefix}-is-disabled`]: disabled,
      // 设置按钮的width为100%,display:block
      [`${classPrefix}-size-full-width`]: block,
    },
  );

接着我们看下button的单测,我举一个例子,因为这个组件太简单了,就举一反三了:

import React from 'react';
// render是用来渲染组件的
// fireEvent是用来触发dom事件的
// vi相当于jest,拥有比如vi.fn -> jest.fn,几乎一样的api
import { render, fireEvent, vi } from '@test/utils';
import Button from '../Button';

describe('Button 组件测试', () => {
  const ButtonText = '按钮组件';
  test('create', async () => {
    // 模拟一个click函数
    const clickFn = vi.fn();
    // container是渲染的dom引用
    // queryByText是用来寻找react组件的dom元素的
    const { container, queryByText } = render(<Button onClick={clickFn}>{ButtonText}</Button>);
    
    expect(container.firstChild.classList.contains('t-button--variant-base')).toBeTruthy();
    // 意思是渲染后是否按钮出现在document文档中
    expect(queryByText(ButtonText)).toBeInTheDocument();
    // 触发一次点击事件
    fireEvent.click(container.firstChild);
    // 检测是否点击事件被调用了
    expect(clickFn).toBeCalledTimes(1);
  });
  // 这里主要检测在disabled传参为true的情况下,click事件是否没有被调用
  test('disabled', async () => {
    const clickFn = vi.fn();
    const { container } = render(<Button disabled onClick={clickFn} />);
    expect(container.firstChild.nodeName).toBe('DIV');
    fireEvent.click(container.firstChild);
    expect(clickFn).toBeCalledTimes(0);
  });
});

ButtonGroup按钮原理

我们看下ButtonGroup怎么用

<ButtonGroup>  
    <Button type='primary' icon={<IconStar />} />  
    <Button type='primary' icon={<IconMessage />} />  
    <Button type='primary' icon={<IconSettings />} />  
</ButtonGroup>

效果是啥呢,如下图:

image.png

源码如下:

function Group(props: ButtonGroupProps, ref) {
  const { className, style, children } = props;

  return (
    <div ref={ref} className={classNames} style={style}>
      {children}
    </div>
  );
}

是不是很简单,ButtonGroup主要是帮助下面的button组件完善css样式而已,我们看下css怎么写呢

.btn-group {
  display: inline-block;
}

按钮之间是有一个竖线的,我们的实现借助了:not和:last-child语法

image.png


.btn-group .btn:not(:last-child) {
  border-right: 1px solid rgb(var(--primary-5));
}

接着我们需要把第一个元素右上radius和右下radius置为0,同理,最后一个元素也是如此

.btn-group .btn:first-child {
  border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
}
.btn-group .btn:last-child {
  border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
}
.btn-group .btn:not(:first-child):not(:last-child) {
  border-radius: 0;
}

好了,结束。这个单测其实很简单了,就是测试如果在Group中,这些元素是否有对应css类名。

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
相关推荐
  • 暂无相关文章
  • 评论 抢沙发
    头像
    欢迎您留下宝贵的见解!
    提交
    头像

    昵称

    取消
    昵称表情代码图片

      暂无评论内容