Antd-Button组件

274 阅读3分钟

Button

一.设计指南

ant-design.antgroup.com/docs/spec/b…

设计指南是很重要的部分,认真看一遍之后,才会懂得到底应该如何设计,布置和排列该组件才能有最好的交互体验。以下是一个简单的设计介绍

在 Ant Design 中我们提供了五种按钮。

  • 主按钮:用于主行动点,一个操作区域只能有一个主按钮。
  • 默认按钮:用于没有主次之分的一组行动点。
  • 虚线按钮:常用于添加操作。
  • 文本按钮:用于最次级的行动点。
  • 链接按钮:一般用于链接,即导航至某位置。

以及四种状态属性与上面配合使用。

  • 危险:删除/移动/修改权限等危险操作,一般需要二次确认。
  • 幽灵:用于背景色比较复杂的地方,常用在首页/产品页等展示场景。
  • 禁用:行动点不可用的时候,一般需要文案解释。
  • 加载中:用于异步操作等待反馈的时候,也可以避免多次提交。

二. 源码研究

使用组件的代码演示:ant-design.antgroup.com/components/…

1.forwardRef(render) 

Call forwardRef() to let your component receive a ref and forward it to a child component:

Parameters 

  • render: The render function for your component. React calls this function with the props and ref that your component received from its parent. The JSX you return will be the output of your component.

Returns 

  • forwardRef returns a React component that you can render in JSX. Unlike React components defined as plain functions, a component returned by forwardRef is also able to receive a ref prop.
const MyInput = forwardRef(function MyInput(props, ref) {

  return (

    <label>

      {props.label}

      <input ref={ref} />

    </label>

  );

});

Button源码使用该方法,使得其父组件能够访问内部子组件(真正button的ref)

const InternalButton: React.ForwardRefRenderFunction<
  HTMLButtonElement | HTMLAnchorElement,
  ButtonProps
> = (props, ref) => {
  const {
    loading = false,
    prefixCls: customizePrefixCls,
    type = 'default',
    danger,
    shape = 'default',
    size: customizeSize,
    styles,
    disabled: customDisabled,
    className,
    rootClassName,
    children,
    icon,
    ghost = false,
    block = false,
    // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
    htmlType = 'button',
    classNames: customClassNames,
    style: customStyle = {},
    ...rest
  } = props;

2.各种判断学习

function getLoadingConfig(loading: BaseButtonProps['loading']): LoadingConfigType {
  //判断OBJECT类型
  if (typeof loading === 'object' && loading) {
    let delay = loading?.delay;
    delay = !Number.isNaN(delay) && typeof delay === 'number' ? delay : 0;  //判断number,NAN的类型也是number
    return {
      loading: delay <= 0,
      delay,
    };
  }

  return {
    loading: !!loading,
    delay: 0,
  };
}

3.关于useMemo

useMemo(calculateValue, dependencies) 

Call useMemo at the top level of your component to cache a calculation between re-renders:

import { useMemo } from 'react';



function TodoList({ todos, tab }) {

  const visibleTodos = useMemo(

    () => filterTodos(todos, tab),

    [todos, tab]

  );

  // ...

}

Parameters 

  • calculateValue: The function calculating the value that you want to cache. It should be pure, should take no arguments, and should return a value of any type. React will call your function during the initial render. On next renders, React will return the same value again if the dependencies have not changed since the last render. Otherwise, it will call calculateValue, return its result, and store it so it can be reused later.
  • dependencies: The list of all reactive values referenced inside of the calculateValue code. Reactive values include props, state, and all the variables and functions declared directly inside your component body.

Returns 

On the initial render, useMemo returns the result of calling calculateValue with no arguments.

During next renders, it will either return an already stored value from the last render (if the dependencies haven’t changed), or call calculateValue again, and return the result that calculateValue has returned.

  const loadingOrDelay = useMemo<LoadingConfigType>(() => getLoadingConfig(loading), [loading]);

  const [innerLoading, setLoading] = useState<boolean>(loadingOrDelay.loading);
  
  ...
  const needInserted = Children.count(children) === 1 && !icon && !isUnBorderedButtonType(type);  //React的Children对象上的方法
  ...
  
    useEffect(() => {
    let delayTimer: ReturnType<typeof setTimeout> | null = null;
    if (loadingOrDelay.delay > 0) {
      delayTimer = setTimeout(() => {
        delayTimer = null;
        setLoading(true);
      }, loadingOrDelay.delay);
    } else {
      setLoading(loadingOrDelay.loading);
    }

    function cleanupTimer() {
      if (delayTimer) {
        clearTimeout(delayTimer); //定时器要及时删除
        delayTimer = null;
      }
    }

    return cleanupTimer;
  }, [loadingOrDelay]);

4.classname

const classes = classNames(
    prefixCls,
    hashId,
    cssVarCls,
    {
      [`${prefixCls}-${shape}`]: shape !== 'default' && shape,
      [`${prefixCls}-${type}`]: type,   //有这个属性才有这个类名
      [`${prefixCls}-${sizeCls}`]: sizeCls,
      [`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType,   //注意0的情况
      [`${prefixCls}-background-ghost`]: ghost && !isUnBorderedButtonType(type),
      [`${prefixCls}-loading`]: innerLoading,
      [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace && !innerLoading,
      [`${prefixCls}-block`]: block,
      [`${prefixCls}-dangerous`]: !!danger,
      [`${prefixCls}-rtl`]: direction === 'rtl',
    },
    compactItemClassnames,
    className,
    rootClassName,
    button?.className,
  );

5. 关于tabindex

tabindex 决定是否聚焦

  • 节点的 tabindex 设置为 -1 时,当前节点使用 tab键 不能聚焦

tabindex 有三个值:0,-N(通常是-1),N(正值)

  • tabindex=0,该元素可以用tab键获取焦点

    • 且访问的顺序是按照元素在文档中的顺序来focus,即使采用了浮动改变了页面中显示的顺序,依然是按照html文档中的顺序来定位
  • tabindex<=-1,该元素用tab键获取不到焦点,但是可以通过js获取

    • 这样就便于我们通过js设置上下左右键的响应事件来focus
    • 取值 -1~-999 之间没有区别,但为了可读性和一致性考虑,推荐使用 -1
  • tabindex>=1,该元素可以用tab键获取焦点,而且优先级大于tabindex=0

    • 不过在tabindex>=1时,数字越小,越先定位到;
    • 如果多个元素拥有相同的 tabindex ,他们的相对顺序按照他们在当前DOM中的先后顺序决定
  if (linkButtonRestProps.href !== undefined) {
    return wrapCSSVar(
      <a
        {...linkButtonRestProps}
        className={classNames(classes, {
          [`${prefixCls}-disabled`]: mergedDisabled,
        })}
        href={mergedDisabled ? undefined : linkButtonRestProps.href}
        style={fullStyle}
        onClick={handleClick}
        ref={buttonRef as React.Ref<HTMLAnchorElement>}
        tabIndex={mergedDisabled ? -1 : 0}
      >
        {iconNode}
        {kids}
      </a>,
    );
  }

6.event的type定义

const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
    const { onClick } = props;
    // FIXME: https://github.com/ant-design/ant-design/issues/30207
    if (innerLoading || mergedDisabled) {
      e.preventDefault();
      return;
    }
    (onClick as React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>)?.(e);
  };

7.Context传值

    Button.Group = Group; 属性是组件的,首字母大写
export interface ButtonGroupProps {
  size?: SizeType;
  style?: React.CSSProperties;
  className?: string;
  prefixCls?: string;
  children?: React.ReactNode;
}

export const GroupSizeContext = React.createContext<SizeType>(undefined);

const ButtonGroup: React.FC<ButtonGroupProps> = (props) => {
  const { getPrefixCls, direction } = React.useContext(ConfigContext);
  const { prefixCls: customizePrefixCls, size, className, ...others } = props;

  ...

  return (
    <GroupSizeContext.Provider value={size}>
      <div {...others} className={classes} />
    </GroupSizeContext.Provider>
  );
}


//Button子组件使用:
const groupSize = useContext(GroupSizeContext);


----------------

//Context是方法的
export const ConfigContext = React.createContext<ConfigConsumerProps>({
  // We provide a default function for Context without provider
  getPrefixCls: defaultGetPrefixCls,
  iconPrefixCls: defaultIconPrefixCls,
});

const defaultGetPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => {
  if (customizePrefixCls) {
    return customizePrefixCls;
  }
  return suffixCls ? `ant-${suffixCls}` : 'ant';
};