目录

🗒 解读 antd 源码系列 - Button 按钮

本篇文章是解读 antd@5.21.6 源码系列的第一篇 - Button 按钮,欢迎您的指正和点赞。

本文主要解读 Button 按钮的源码实现,按钮用于开始一个即时操作。

文档地址

详细代码

Button 包括两个组件:Button 和 Button.Group。

Button

Import

首先来看看 Button.tsx 的模块导入部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, {
  Children,
  createRef,
  useContext,
  useEffect,
  useMemo,
  useState,
} from "react";
import classNames from "classnames";
import omit from "rc-util/lib/omit";
import { composeRef } from "rc-util/lib/ref";

import { devUseWarning } from "../_util/warning";
import Wave from "../_util/wave";
import { ConfigContext } from "../config-provider";
import DisabledContext from "../config-provider/DisabledContext";
import useSize from "../config-provider/hooks/useSize";
import type { SizeType } from "../config-provider/SizeContext";
import { useCompactItemContext } from "../space/Compact";
import Group, { GroupSizeContext } from "./button-group";
import type {
  ButtonColorType,
  ButtonHTMLType,
  ButtonShape,
  ButtonType,
  ButtonVariantType,
} from "./buttonHelpers";
import {
  isTwoCNChar,
  isUnBorderedButtonVariant,
  spaceChildren,
} from "./buttonHelpers";
import IconWrapper from "./IconWrapper";
import LoadingIcon from "./LoadingIcon";
import useStyle from "./style";
import CompactCmp from "./style/compactCmp";

devUseWarning

用于处理 React 组件警告的工具,主要用于在开发环境中输出警告信息,以帮助开发者识别和修复潜在的问题。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import * as React from "react";
/**
 * rcWarning - 警告函数,输出警告信息
 * resetWarned - 重置警告函数,清除已记录的警告
 */
import rcWarning, { resetWarned as rcResetWarned } from "rc-util/lib/warning";

// 占位函数,空函数
export function noop() {}

/**
 * deprecatedWarnList - 存储已发送的警告信息
 * resetWarned - 清除警告状态
 */
let deprecatedWarnList: Record<string, string[]> | null = null;
export function resetWarned() {
  deprecatedWarnList = null;
  rcResetWarned();
}

/**
 * warning - 警告函数
 */
type Warning = (valid: boolean, component: string, message?: string) => void;
// eslint-disable-next-line import/no-mutable-exports
let warning: Warning = noop;
if (process.env.NODE_ENV !== "production") {
  warning = (valid, component, message) => {
    rcWarning(valid, `[antd: ${component}] ${message}`);

    // StrictMode will inject console which will not throw warning in React 17.
    if (process.env.NODE_ENV === "test") {
      resetWarned();
    }
  };
}

/**
 * valid - 有效性
 * deprecated - 过时
 * usage - 用法不当
 * breaking - 破坏性变化
 */
type BaseTypeWarning = (
  valid: boolean,
  /**
   * - deprecated: Some API will be removed in future but still support now.
   * - usage: Some API usage is not correct.
   * - breaking: Breaking change like API is removed.
   */
  type: "deprecated" | "usage" | "breaking",
  message?: string
) => void;
type TypeWarning = BaseTypeWarning & {
  deprecated: (
    valid: boolean,
    oldProp: string,
    newProp: string,
    message?: string
  ) => void;
};

/**
 * strict - 控制警告的聚合方式
 */
export interface WarningContextProps {
  /**
   * @descCN 设置警告等级,设置 `false` 时会将废弃相关信息聚合为单条信息。
   * @descEN Set the warning level. When set to `false`, discard related information will be aggregated into a single message.
   * @since 5.10.0
   */
  strict?: boolean;
}
export const WarningContext = React.createContext<WarningContextProps>({});

/**
 * devUseWarning - 自定义 Hook,仅在开发环境中使用
 */
/**
 * This is a hook but we not named as `useWarning`
 * since this is only used in development.
 * We should always wrap this in `if (process.env.NODE_ENV !== 'production')` condition
 */
export const devUseWarning: (component: string) => TypeWarning =
  process.env.NODE_ENV !== "production"
    ? (component) => {
        const { strict } = React.useContext(WarningContext);

        const typeWarning: TypeWarning = (valid, type, message) => {
          if (!valid) {
            if (strict === false && type === "deprecated") {
              // 是否已存在警告列表
              const existWarning = deprecatedWarnList;

              if (!deprecatedWarnList) {
                deprecatedWarnList = {};
              }

              // 该组件的警告列表(尚不存在则初始化)
              deprecatedWarnList[component] =
                deprecatedWarnList[component] || [];
              // 当前消息是否存在于该组件的警告列表中
              if (!deprecatedWarnList[component].includes(message || "")) {
                deprecatedWarnList[component].push(message || "");
              }

              // Warning for the first time
              // existWarning 不存在,表示这是第一次记录警告,输出一个警告信息到控制台
              if (!existWarning) {
                console.warn(
                  "[antd] There exists deprecated usage in your code:",
                  deprecatedWarnList
                );
              }
            } else {
              warning(valid, component, message);
            }
          }
        };

        // 处理过时属性的警告
        typeWarning.deprecated = (valid, oldProp, newProp, message) => {
          typeWarning(
            valid,
            "deprecated",
            `\`${oldProp}\` is deprecated. Please use \`${newProp}\` instead.${
              message ? ` ${message}` : ""
            }`
          );
        };

        return typeWarning;
      }
    : () => {
        const noopWarning: TypeWarning = () => {};

        noopWarning.deprecated = noop;

        return noopWarning;
      };

export default warning;

Wave

用于在点击时为其子元素添加波纹效果,通常用于需要视觉反馈的交互元素,如按钮、多选框、开关等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import React, { useContext, useRef } from "react";
import classNames from "classnames";
/**
 * isVisible - 判断 DOM 元素是否可见
 * composeRef、supportRef - 处理 ref
 */
import isVisible from "rc-util/lib/Dom/isVisible";
import { composeRef, supportRef } from "rc-util/lib/ref";

import type { ConfigConsumerProps } from "../../config-provider";
import { ConfigContext } from "../../config-provider";
import { cloneElement } from "../reactNode";
import type { WaveComponent } from "./interface";
import useStyle from "./style";
import useWave from "./useWave";

export interface WaveProps {
  disabled?: boolean;
  children?: React.ReactNode;
  component?: WaveComponent;
}

const Wave: React.FC<WaveProps> = (props) => {
  const { children, disabled, component } = props;

  // 前缀类名
  const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
  // 容器元素
  const containerRef = useRef<HTMLElement>(null);

  // ============================== Style ===============================
  // 波纹效果的前缀类名
  const prefixCls = getPrefixCls("wave");
  // 哈希 ID
  const [, hashId] = useStyle(prefixCls);

  // =============================== Wave ===============================
  const showWave = useWave(
    containerRef,
    classNames(prefixCls, hashId),
    component
  );

  // ============================== Effect ==============================
  React.useEffect(() => {
    const node = containerRef.current;
    // 容器元素是否存在、是否为元素节点、是否被禁用
    if (!node || node.nodeType !== 1 || disabled) {
      return;
    }

    // Click handler
    const onClick = (e: MouseEvent) => {
      // Fix radio button click twice
      // 目标元素是否可见、是否被禁用等
      if (
        !isVisible(e.target as HTMLElement) ||
        // No need wave
        !node.getAttribute ||
        node.getAttribute("disabled") ||
        (node as HTMLInputElement).disabled ||
        node.className.includes("disabled") ||
        node.className.includes("-leave")
      ) {
        return;
      }
      // 显示波纹效果
      showWave(e);
    };

    // Bind events
    node.addEventListener("click", onClick, true);
    return () => {
      node.removeEventListener("click", onClick, true);
    };
  }, [disabled]);

  // ============================== Render ==============================
  // 是否为有效的 React 元素
  if (!React.isValidElement(children)) {
    return children ?? null;
  }

  // children 是否支持 ref,来决定使用 composeRef 合并 ref
  const ref = supportRef(children)
    ? composeRef((children as any).ref, containerRef)
    : containerRef;
  // 克隆 children,传递 ref
  return cloneElement(children, { ref });
};

if (process.env.NODE_ENV !== "production") {
  Wave.displayName = "Wave";
}

export default Wave;

ConfigContext

用于管理全局的配置、主题、样式和上下文。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
import * as React from "react";
import { createTheme } from "@ant-design/cssinjs";
import IconContext from "@ant-design/icons/lib/components/Context";
import useMemo from "rc-util/lib/hooks/useMemo";
// merge - 合并对象或数组
import { merge } from "rc-util/lib/utils/set";

import warning, { devUseWarning, WarningContext } from "../_util/warning";
import type { WarningContextProps } from "../_util/warning";
import ValidateMessagesContext from "../form/validateMessagesContext";

// 本地化
import type { Locale } from "../locale";
import LocaleProvider, { ANT_MARK } from "../locale";
import type { LocaleContextProps } from "../locale/context";
import LocaleContext from "../locale/context";
import defaultLocale from "../locale/en_US";

// 默认主题、设计令牌
import { defaultTheme, DesignTokenContext } from "../theme/context";
import defaultSeedToken from "../theme/themes/seed";
import type {
  AlertConfig,
  BadgeConfig,
  ButtonConfig,
  CardConfig,
  CascaderConfig,
  CollapseConfig,
  ComponentStyleConfig,
  ConfigConsumerProps,
  CSPConfig,
  DatePickerConfig,
  DirectionType,
  DrawerConfig,
  FlexConfig,
  FloatButtonGroupConfig,
  FormConfig,
  ImageConfig,
  InputConfig,
  InputNumberConfig,
  ListConfig,
  MentionsConfig,
  MenuConfig,
  ModalConfig,
  NotificationConfig,
  PaginationConfig,
  PopupOverflow,
  RangePickerConfig,
  SelectConfig,
  SpaceConfig,
  SpinConfig,
  TableConfig,
  TabsConfig,
  TagConfig,
  TextAreaConfig,
  Theme,
  ThemeConfig,
  TimePickerConfig,
  TourConfig,
  TransferConfig,
  TreeSelectConfig,
  Variant,
  WaveConfig,
} from "./context";
import {
  ConfigConsumer,
  ConfigContext,
  defaultIconPrefixCls,
  defaultPrefixCls,
  Variants,
} from "./context";
import { registerTheme } from "./cssVariables";
import type { RenderEmptyHandler } from "./defaultRenderEmpty";
import { DisabledContextProvider } from "./DisabledContext";
import useConfig from "./hooks/useConfig";
import useTheme from "./hooks/useTheme";
import MotionWrapper from "./MotionWrapper";
import PropWarning from "./PropWarning";
import type { SizeType } from "./SizeContext";
import SizeContext, { SizeContextProvider } from "./SizeContext";
import useStyle from "./style";

export type { Variant };

export { Variants };

/**
 * Since too many feedback using static method like `Modal.confirm` not getting theme, we record the
 * theme register info here to help developer get warning info.
 */
let existThemeConfig = false;

export const warnContext: (componentName: string) => void =
  process.env.NODE_ENV !== "production"
    ? (componentName: string) => {
        warning(
          !existThemeConfig,
          componentName,
          `Static function can not consume context like dynamic theme. Please use 'App' component instead.`
        );
      }
    : /* istanbul ignore next */
      null!;

export {
  ConfigConsumer,
  ConfigContext,
  defaultPrefixCls,
  defaultIconPrefixCls,
  type ConfigConsumerProps,
  type CSPConfig,
  type DirectionType,
  type RenderEmptyHandler,
  type ThemeConfig,
};

export const configConsumerProps = [
  "getTargetContainer",
  "getPopupContainer",
  "rootPrefixCls",
  "getPrefixCls",
  "renderEmpty",
  "csp",
  "autoInsertSpaceInButton",
  "locale",
];

// These props is used by `useContext` directly in sub component
const PASSED_PROPS: Exclude<
  keyof ConfigConsumerProps,
  "rootPrefixCls" | "getPrefixCls" | "warning"
>[] = [
  "getTargetContainer",
  "getPopupContainer",
  "renderEmpty",
  "input",
  "pagination",
  "form",
  "select",
  "button",
];

export interface ConfigProviderProps {
  getTargetContainer?: () => HTMLElement | Window;
  getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement;
  prefixCls?: string;
  iconPrefixCls?: string;
  children?: React.ReactNode;
  renderEmpty?: RenderEmptyHandler;
  csp?: CSPConfig;
  /** @deprecated Please use `{ button: { autoInsertSpace: boolean }}` instead */
  autoInsertSpaceInButton?: boolean;
  variant?: Variant;
  form?: FormConfig;
  input?: InputConfig;
  inputNumber?: InputNumberConfig;
  textArea?: TextAreaConfig;
  select?: SelectConfig;
  pagination?: PaginationConfig;
  /**
   * @descCN 语言包配置,语言包可到 `antd/locale` 目录下寻找。
   * @descEN Language package setting, you can find the packages in `antd/locale`.
   */
  locale?: Locale;
  componentSize?: SizeType;
  componentDisabled?: boolean;
  /**
   * @descCN 设置布局展示方向。
   * @descEN Set direction of layout.
   * @default ltr
   */
  direction?: DirectionType;
  space?: SpaceConfig;
  splitter?: ComponentStyleConfig;
  /**
   * @descCN 设置 `false` 时关闭虚拟滚动。
   * @descEN Close the virtual scrolling when setting `false`.
   * @default true
   */
  virtual?: boolean;
  /** @deprecated Please use `popupMatchSelectWidth` instead */
  dropdownMatchSelectWidth?: boolean;
  popupMatchSelectWidth?: boolean;
  popupOverflow?: PopupOverflow;
  theme?: ThemeConfig;
  warning?: WarningContextProps;
  alert?: AlertConfig;
  anchor?: ComponentStyleConfig;
  button?: ButtonConfig;
  calendar?: ComponentStyleConfig;
  carousel?: ComponentStyleConfig;
  cascader?: CascaderConfig;
  treeSelect?: TreeSelectConfig;
  collapse?: CollapseConfig;
  divider?: ComponentStyleConfig;
  drawer?: DrawerConfig;
  typography?: ComponentStyleConfig;
  skeleton?: ComponentStyleConfig;
  spin?: SpinConfig;
  segmented?: ComponentStyleConfig;
  statistic?: ComponentStyleConfig;
  steps?: ComponentStyleConfig;
  image?: ImageConfig;
  layout?: ComponentStyleConfig;
  list?: ListConfig;
  mentions?: MentionsConfig;
  modal?: ModalConfig;
  progress?: ComponentStyleConfig;
  result?: ComponentStyleConfig;
  slider?: ComponentStyleConfig;
  breadcrumb?: ComponentStyleConfig;
  menu?: MenuConfig;
  floatButtonGroup?: FloatButtonGroupConfig;
  checkbox?: ComponentStyleConfig;
  descriptions?: ComponentStyleConfig;
  empty?: ComponentStyleConfig;
  badge?: BadgeConfig;
  radio?: ComponentStyleConfig;
  rate?: ComponentStyleConfig;
  switch?: ComponentStyleConfig;
  transfer?: TransferConfig;
  avatar?: ComponentStyleConfig;
  message?: ComponentStyleConfig;
  tag?: TagConfig;
  table?: TableConfig;
  card?: CardConfig;
  tabs?: TabsConfig;
  timeline?: ComponentStyleConfig;
  timePicker?: TimePickerConfig;
  upload?: ComponentStyleConfig;
  notification?: NotificationConfig;
  tree?: ComponentStyleConfig;
  colorPicker?: ComponentStyleConfig;
  datePicker?: DatePickerConfig;
  rangePicker?: RangePickerConfig;
  dropdown?: ComponentStyleConfig;
  flex?: FlexConfig;
  /**
   * Wave is special component which only patch on the effect of component interaction.
   */
  wave?: WaveConfig;
  tour?: TourConfig;
}

interface ProviderChildrenProps extends ConfigProviderProps {
  parentContext: ConfigConsumerProps;
  legacyLocale: Locale;
}

type holderRenderType = (children: React.ReactNode) => React.ReactNode;

let globalPrefixCls: string;
let globalIconPrefixCls: string;
let globalTheme: ThemeConfig;
let globalHolderRender: holderRenderType | undefined;

function getGlobalPrefixCls() {
  return globalPrefixCls || defaultPrefixCls;
}

function getGlobalIconPrefixCls() {
  return globalIconPrefixCls || defaultIconPrefixCls;
}

function isLegacyTheme(theme: Theme | ThemeConfig): theme is Theme {
  return Object.keys(theme).some((key) => key.endsWith("Color"));
}

interface GlobalConfigProps {
  prefixCls?: string;
  iconPrefixCls?: string;
  theme?: Theme | ThemeConfig;
  holderRender?: holderRenderType;
}

const setGlobalConfig = (props: GlobalConfigProps) => {
  const { prefixCls, iconPrefixCls, theme, holderRender } = props;
  if (prefixCls !== undefined) {
    globalPrefixCls = prefixCls;
  }
  if (iconPrefixCls !== undefined) {
    globalIconPrefixCls = iconPrefixCls;
  }
  if ("holderRender" in props) {
    globalHolderRender = holderRender;
  }

  if (theme) {
    if (isLegacyTheme(theme)) {
      warning(
        false,
        "ConfigProvider",
        "`config` of css variable theme is not work in v5. Please use new `theme` config instead."
      );
      registerTheme(getGlobalPrefixCls(), theme);
    } else {
      globalTheme = theme;
    }
  }
};

export const globalConfig = () => ({
  getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => {
    if (customizePrefixCls) {
      return customizePrefixCls;
    }
    return suffixCls
      ? `${getGlobalPrefixCls()}-${suffixCls}`
      : getGlobalPrefixCls();
  },
  getIconPrefixCls: getGlobalIconPrefixCls,
  getRootPrefixCls: () => {
    // If Global prefixCls provided, use this
    if (globalPrefixCls) {
      return globalPrefixCls;
    }

    // Fallback to default prefixCls
    return getGlobalPrefixCls();
  },
  getTheme: () => globalTheme,
  holderRender: globalHolderRender,
});

const ProviderChildren: React.FC<ProviderChildrenProps> = (props) => {
  const {
    children,
    csp: customCsp,
    autoInsertSpaceInButton,
    alert,
    anchor,
    form,
    locale,
    componentSize,
    direction,
    space,
    splitter,
    virtual,
    dropdownMatchSelectWidth,
    popupMatchSelectWidth,
    popupOverflow,
    legacyLocale,
    parentContext,
    iconPrefixCls: customIconPrefixCls,
    theme,
    componentDisabled,
    segmented,
    statistic,
    spin,
    calendar,
    carousel,
    cascader,
    collapse,
    typography,
    checkbox,
    descriptions,
    divider,
    drawer,
    skeleton,
    steps,
    image,
    layout,
    list,
    mentions,
    modal,
    progress,
    result,
    slider,
    breadcrumb,
    menu,
    pagination,
    input,
    textArea,
    empty,
    badge,
    radio,
    rate,
    switch: SWITCH,
    transfer,
    avatar,
    message,
    tag,
    table,
    card,
    tabs,
    timeline,
    timePicker,
    upload,
    notification,
    tree,
    colorPicker,
    datePicker,
    rangePicker,
    flex,
    wave,
    dropdown,
    warning: warningConfig,
    tour,
    floatButtonGroup,
    variant,
    inputNumber,
    treeSelect,
  } = props;

  // =================================== Context ===================================
  const getPrefixCls = React.useCallback(
    (suffixCls: string, customizePrefixCls?: string) => {
      const { prefixCls } = props;

      if (customizePrefixCls) {
        return customizePrefixCls;
      }

      const mergedPrefixCls = prefixCls || parentContext.getPrefixCls("");

      return suffixCls ? `${mergedPrefixCls}-${suffixCls}` : mergedPrefixCls;
    },
    [parentContext.getPrefixCls, props.prefixCls]
  );

  const iconPrefixCls =
    customIconPrefixCls || parentContext.iconPrefixCls || defaultIconPrefixCls;
  const csp = customCsp || parentContext.csp;

  useStyle(iconPrefixCls, csp);

  const mergedTheme = useTheme(theme, parentContext.theme, {
    prefixCls: getPrefixCls(""),
  });

  if (process.env.NODE_ENV !== "production") {
    existThemeConfig = existThemeConfig || !!mergedTheme;
  }

  const baseConfig = {
    csp,
    autoInsertSpaceInButton,
    alert,
    anchor,
    locale: locale || legacyLocale,
    direction,
    space,
    splitter,
    virtual,
    popupMatchSelectWidth: popupMatchSelectWidth ?? dropdownMatchSelectWidth,
    popupOverflow,
    getPrefixCls,
    iconPrefixCls,
    theme: mergedTheme,
    segmented,
    statistic,
    spin,
    calendar,
    carousel,
    cascader,
    collapse,
    typography,
    checkbox,
    descriptions,
    divider,
    drawer,
    skeleton,
    steps,
    image,
    input,
    textArea,
    layout,
    list,
    mentions,
    modal,
    progress,
    result,
    slider,
    breadcrumb,
    menu,
    pagination,
    empty,
    badge,
    radio,
    rate,
    switch: SWITCH,
    transfer,
    avatar,
    message,
    tag,
    table,
    card,
    tabs,
    timeline,
    timePicker,
    upload,
    notification,
    tree,
    colorPicker,
    datePicker,
    rangePicker,
    flex,
    wave,
    dropdown,
    warning: warningConfig,
    tour,
    floatButtonGroup,
    variant,
    inputNumber,
    treeSelect,
  };

  if (process.env.NODE_ENV !== "production") {
    const warningFn = devUseWarning("ConfigProvider");
    warningFn(
      !("autoInsertSpaceInButton" in props),
      "deprecated",
      "`autoInsertSpaceInButton` is deprecated. Please use `{ button: { autoInsertSpace: boolean }}` instead."
    );
  }

  const config: ConfigConsumerProps = {
    ...parentContext,
  };

  (Object.keys(baseConfig) as (keyof typeof baseConfig)[]).forEach((key) => {
    if (baseConfig[key] !== undefined) {
      (config as any)[key] = baseConfig[key];
    }
  });

  // Pass the props used by `useContext` directly with child component.
  // These props should merged into `config`.
  PASSED_PROPS.forEach((propName) => {
    const propValue = props[propName];
    if (propValue) {
      (config as any)[propName] = propValue;
    }
  });

  if (typeof autoInsertSpaceInButton !== "undefined") {
    // merge deprecated api
    config.button = {
      autoInsertSpace: autoInsertSpaceInButton,
      ...config.button,
    };
  }

  // https://github.com/ant-design/ant-design/issues/27617
  const memoedConfig = useMemo(
    () => config,
    config,
    (prevConfig, currentConfig) => {
      const prevKeys = Object.keys(prevConfig) as Array<keyof typeof config>;
      const currentKeys = Object.keys(currentConfig) as Array<
        keyof typeof config
      >;
      return (
        prevKeys.length !== currentKeys.length ||
        prevKeys.some((key) => prevConfig[key] !== currentConfig[key])
      );
    }
  );

  const memoIconContextValue = React.useMemo(
    () => ({ prefixCls: iconPrefixCls, csp }),
    [iconPrefixCls, csp]
  );

  let childNode = (
    <>
      <PropWarning dropdownMatchSelectWidth={dropdownMatchSelectWidth} />
      {children}
    </>
  );

  const validateMessages = React.useMemo(
    () =>
      merge(
        defaultLocale.Form?.defaultValidateMessages || {},
        memoedConfig.locale?.Form?.defaultValidateMessages || {},
        memoedConfig.form?.validateMessages || {},
        form?.validateMessages || {}
      ),
    [memoedConfig, form?.validateMessages]
  );

  if (Object.keys(validateMessages).length > 0) {
    childNode = (
      <ValidateMessagesContext.Provider value={validateMessages}>
        {childNode}
      </ValidateMessagesContext.Provider>
    );
  }

  if (locale) {
    childNode = (
      <LocaleProvider locale={locale} _ANT_MARK__={ANT_MARK}>
        {childNode}
      </LocaleProvider>
    );
  }

  if (iconPrefixCls || csp) {
    childNode = (
      <IconContext.Provider value={memoIconContextValue}>
        {childNode}
      </IconContext.Provider>
    );
  }

  if (componentSize) {
    childNode = (
      <SizeContextProvider size={componentSize}>
        {childNode}
      </SizeContextProvider>
    );
  }

  // =================================== Motion ===================================
  childNode = <MotionWrapper>{childNode}</MotionWrapper>;

  // ================================ Dynamic theme ================================
  const memoTheme = React.useMemo(() => {
    const { algorithm, token, components, cssVar, ...rest } = mergedTheme || {};
    const themeObj =
      algorithm && (!Array.isArray(algorithm) || algorithm.length > 0)
        ? createTheme(algorithm)
        : defaultTheme;

    const parsedComponents: any = {};
    Object.entries(components || {}).forEach(
      ([componentName, componentToken]) => {
        const parsedToken: typeof componentToken & {
          theme?: typeof defaultTheme;
        } = {
          ...componentToken,
        };
        if ("algorithm" in parsedToken) {
          if (parsedToken.algorithm === true) {
            parsedToken.theme = themeObj;
          } else if (
            Array.isArray(parsedToken.algorithm) ||
            typeof parsedToken.algorithm === "function"
          ) {
            parsedToken.theme = createTheme(parsedToken.algorithm);
          }
          delete parsedToken.algorithm;
        }
        parsedComponents[componentName] = parsedToken;
      }
    );

    const mergedToken = {
      ...defaultSeedToken,
      ...token,
    };

    return {
      ...rest,
      theme: themeObj,

      token: mergedToken,
      components: parsedComponents,
      override: {
        override: mergedToken,
        ...parsedComponents,
      },
      cssVar: cssVar as Exclude<ThemeConfig["cssVar"], boolean>,
    };
  }, [mergedTheme]);

  if (theme) {
    childNode = (
      <DesignTokenContext.Provider value={memoTheme}>
        {childNode}
      </DesignTokenContext.Provider>
    );
  }

  // ================================== Warning ===================================
  if (memoedConfig.warning) {
    childNode = (
      <WarningContext.Provider value={memoedConfig.warning}>
        {childNode}
      </WarningContext.Provider>
    );
  }

  // =================================== Render ===================================
  if (componentDisabled !== undefined) {
    childNode = (
      <DisabledContextProvider disabled={componentDisabled}>
        {childNode}
      </DisabledContextProvider>
    );
  }

  return (
    <ConfigContext.Provider value={memoedConfig}>
      {childNode}
    </ConfigContext.Provider>
  );
};

const ConfigProvider: React.FC<ConfigProviderProps> & {
  /** @private internal Usage. do not use in your production */
  ConfigContext: typeof ConfigContext;
  /** @deprecated Please use `ConfigProvider.useConfig().componentSize` instead */
  SizeContext: typeof SizeContext;
  config: typeof setGlobalConfig;
  useConfig: typeof useConfig;
} = (props) => {
  const context = React.useContext<ConfigConsumerProps>(ConfigContext);
  const antLocale = React.useContext<LocaleContextProps | undefined>(
    LocaleContext
  );
  return (
    <ProviderChildren
      parentContext={context}
      legacyLocale={antLocale!}
      {...props}
    />
  );
};

ConfigProvider.ConfigContext = ConfigContext;
ConfigProvider.SizeContext = SizeContext;
ConfigProvider.config = setGlobalConfig;
ConfigProvider.useConfig = useConfig;

Object.defineProperty(ConfigProvider, "SizeContext", {
  get: () => {
    warning(
      false,
      "ConfigProvider",
      "ConfigProvider.SizeContext is deprecated. Please use `ConfigProvider.useConfig().componentSize` instead."
    );
    return SizeContext;
  },
});

if (process.env.NODE_ENV !== "production") {
  ConfigProvider.displayName = "ConfigProvider";
}

export default ConfigProvider;

DisabledContext

用于管理某些功能的启用或禁用状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import * as React from "react";

const DisabledContext = React.createContext < boolean > false;

export interface DisabledContextProps {
  disabled?: boolean;
  children?: React.ReactNode;
}

export const DisabledContextProvider: React.FC<DisabledContextProps> = ({
  children,
  disabled,
}) => {
  const originDisabled = React.useContext(DisabledContext);
  return (
    <DisabledContext.Provider value={disabled ?? originDisabled}>
      {children}
    </DisabledContext.Provider>
  );
};

export default DisabledContext;

SizeContext

用于管理应用程序中组件的尺寸设置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import * as React from "react";

export type SizeType = "small" | "middle" | "large" | undefined;

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

export interface SizeContextProps {
  size?: SizeType;
  children?: React.ReactNode;
}

export const SizeContextProvider: React.FC<SizeContextProps> = ({
  children,
  size,
}) => {
  const originSize = React.useContext<SizeType>(SizeContext);
  return (
    <SizeContext.Provider value={size || originSize}>
      {children}
    </SizeContext.Provider>
  );
};

export default SizeContext;

useSize

用于从上下文或 customSize 中获取和计算组件的尺寸值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * 用于从上下文或 customSize 中获取和计算组件的尺寸值。
 */
import React from "react";

import type { SizeType } from "../SizeContext";
import SizeContext from "../SizeContext";

const useSize = <T,>(customSize?: T | ((ctxSize: SizeType) => T)): T => {
  const size = React.useContext<SizeType>(SizeContext);
  const mergedSize = React.useMemo<T>(() => {
    if (!customSize) {
      return size as T;
    }
    if (typeof customSize === "string") {
      return customSize ?? size;
    }
    if (customSize instanceof Function) {
      return customSize(size);
    }
    return size as T;
  }, [customSize, size]);
  return mergedSize;
};

export default useSize;

Compact

用于处理紧凑布局的 UI 组件。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import * as React from "react";
import classNames from "classnames";
import toArray from "rc-util/lib/Children/toArray";

import type { DirectionType } from "../config-provider";
import { ConfigContext } from "../config-provider";
import useSize from "../config-provider/hooks/useSize";
import type { SizeType } from "../config-provider/SizeContext";
import useStyle from "./style";

/**
 * compactSize - 尺寸
 * compactDirection - 方向
 * isFirstItem - 是否为首项
 * isLastItem - 是否为尾项
 */
export interface SpaceCompactItemContextType {
  compactSize?: SizeType;
  compactDirection?: "horizontal" | "vertical";
  isFirstItem?: boolean;
  isLastItem?: boolean;
}

/**
 * 紧凑项相关信息的上下文
 */
export const SpaceCompactItemContext =
  React.createContext<SpaceCompactItemContextType | null>(null);

export const useCompactItemContext = (
  prefixCls: string,
  direction: DirectionType
) => {
  const compactItemContext = React.useContext(SpaceCompactItemContext);

  // 计算类名,考虑 direction、compactDirection、isFirstItem、isLastItem 等
  const compactItemClassnames = React.useMemo<string>(() => {
    if (!compactItemContext) {
      return "";
    }
    const { compactDirection, isFirstItem, isLastItem } = compactItemContext;
    const separator = compactDirection === "vertical" ? "-vertical-" : "-";

    return classNames(`${prefixCls}-compact${separator}item`, {
      [`${prefixCls}-compact${separator}first-item`]: isFirstItem,
      [`${prefixCls}-compact${separator}last-item`]: isLastItem,
      [`${prefixCls}-compact${separator}item-rtl`]: direction === "rtl",
    });
  }, [prefixCls, direction, compactItemContext]);

  return {
    compactSize: compactItemContext?.compactSize,
    compactDirection: compactItemContext?.compactDirection,
    compactItemClassnames,
  };
};

// 不使用紧凑样式
export const NoCompactStyle: React.FC<React.PropsWithChildren<unknown>> = ({
  children,
}) => (
  <SpaceCompactItemContext.Provider value={null}>
    {children}
  </SpaceCompactItemContext.Provider>
);

/**
 * prefixCls - CSS 前缀
 * size - 尺寸
 * direction - 方向
 * block - 块级元素标识
 * rootClassName - 根类名
 */
export interface SpaceCompactProps
  extends React.HTMLAttributes<HTMLDivElement> {
  prefixCls?: string;
  size?: SizeType;
  direction?: "horizontal" | "vertical";
  block?: boolean;
  rootClassName?: string;
}

const CompactItem: React.FC<
  React.PropsWithChildren<SpaceCompactItemContextType>
> = ({ children, ...otherProps }) => (
  <SpaceCompactItemContext.Provider value={otherProps}>
    {children}
  </SpaceCompactItemContext.Provider>
);

const Compact: React.FC<SpaceCompactProps> = (props) => {
  const { getPrefixCls, direction: directionConfig } =
    React.useContext(ConfigContext);

  const {
    size,
    direction,
    block,
    prefixCls: customizePrefixCls,
    className,
    rootClassName,
    children,
    ...restProps
  } = props;

  const mergedSize = useSize((ctx) => size ?? ctx);

  const prefixCls = getPrefixCls("space-compact", customizePrefixCls);
  const [wrapCSSVar, hashId] = useStyle(prefixCls);
  const clx = classNames(
    prefixCls,
    hashId,
    {
      [`${prefixCls}-rtl`]: directionConfig === "rtl",
      [`${prefixCls}-block`]: block,
      [`${prefixCls}-vertical`]: direction === "vertical",
    },
    className,
    rootClassName
  );

  const compactItemContext = React.useContext(SpaceCompactItemContext);

  const childNodes = toArray(children);
  const nodes = React.useMemo(
    () =>
      // 映射每个子元素到 CompactItem
      childNodes.map((child, i) => {
        const key = child?.key || `${prefixCls}-item-${i}`;
        return (
          <CompactItem
            key={key}
            compactSize={mergedSize}
            compactDirection={direction}
            isFirstItem={
              i === 0 &&
              (!compactItemContext || compactItemContext?.isFirstItem)
            }
            isLastItem={
              i === childNodes.length - 1 &&
              (!compactItemContext || compactItemContext?.isLastItem)
            }
          >
            {child}
          </CompactItem>
        );
      }),
    [size, childNodes, compactItemContext]
  );

  // =========================== Render ===========================
  if (childNodes.length === 0) {
    return null;
  }

  return wrapCSSVar(
    <div className={clx} {...restProps}>
      {nodes}
    </div>
  );
};

export default Compact;

reactNode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from "react";

import type { AnyObject } from "./type";

/**
 * 检查 child 是否是 React Fragment
 */
export function isFragment(child: any): boolean {
  return child && React.isValidElement(child) && child.type === React.Fragment;
}

type RenderProps =
  | AnyObject
  | ((originProps: AnyObject) => AnyObject | undefined);

/**
 * 替换 React 元素
 */
export const replaceElement = <P>(
  element: React.ReactNode,
  replacement: React.ReactNode,
  props?: RenderProps
) => {
  if (!React.isValidElement<P>(element)) {
    return replacement;
  }
  return React.cloneElement<P>(
    element,
    typeof props === "function" ? props(element.props || {}) : props
  );
};

/**
 * 克隆 React 元素
 */
export function cloneElement<P>(element: React.ReactNode, props?: RenderProps) {
  return replaceElement<P>(element, element, props) as React.ReactElement;
}

buttonHelpers

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import React from "react";

import { cloneElement, isFragment } from "../_util/reactNode";
import type { BaseButtonProps, LegacyButtonType } from "./button";

/**
 * 是否由两个中文字符组成
 */
const rxTwoCNChar = /^[\u4E00-\u9FA5]{2}$/;
export const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);

/**
 * 将旧版按钮类型转换为新版按钮类型
 */
export function convertLegacyProps(
  type?: LegacyButtonType
): Pick<BaseButtonProps, "danger" | "type"> {
  if (type === "danger") {
    return { danger: true };
  }
  return { type };
}

/**
 * 是否为字符串
 */
export function isString(str: any): str is string {
  return typeof str === "string";
}

/**
 * 是否为无边框样式
 */
export function isUnBorderedButtonVariant(type?: ButtonVariantType) {
  return type === "text" || type === "link";
}

/**
 * 处理子元素,在两个中文字符之间插入空格
 */
function splitCNCharsBySpace(
  child: React.ReactElement | string | number,
  needInserted: boolean
) {
  if (child === null || child === undefined) {
    return;
  }

  const SPACE = needInserted ? " " : "";

  if (
    typeof child !== "string" &&
    typeof child !== "number" &&
    isString(child.type) &&
    isTwoCNChar(child.props.children)
  ) {
    return cloneElement(child, {
      children: child.props.children.split("").join(SPACE),
    });
  }

  if (isString(child)) {
    return isTwoCNChar(child) ? (
      <span>{child.split("").join(SPACE)}</span>
    ) : (
      <span>{child}</span>
    );
  }

  if (isFragment(child)) {
    return <span>{child}</span>;
  }

  return child;
}

/**
 * 处理子元素,将相邻的字符串或数字合并为一个元素,并在需要时插入空格
 */
export function spaceChildren(
  children: React.ReactNode,
  needInserted: boolean
) {
  let isPrevChildPure = false;
  const childList: React.ReactNode[] = [];

  React.Children.forEach(children, (child) => {
    const type = typeof child;
    const isCurrentChildPure = type === "string" || type === "number";
    // 前一个子元素和当前子元素都是纯字符串或数字,则将它们合并
    if (isPrevChildPure && isCurrentChildPure) {
      const lastIndex = childList.length - 1;
      const lastChild = childList[lastIndex];
      childList[lastIndex] = `${lastChild}${child}`;
    } else {
      childList.push(child);
    }

    isPrevChildPure = isCurrentChildPure;
  });

  return React.Children.map(childList, (child) =>
    splitCNCharsBySpace(
      child as React.ReactElement | string | number,
      needInserted
    )
  );
}

/**
 * 按钮类型
 */
const _ButtonTypes = ["default", "primary", "dashed", "link", "text"] as const;
export type ButtonType = (typeof _ButtonTypes)[number];

/**
 * 按钮形状
 */
const _ButtonShapes = ["default", "circle", "round"] as const;
export type ButtonShape = (typeof _ButtonShapes)[number];

/**
 * 按钮原生的 type 值
 */
const _ButtonHTMLTypes = ["submit", "button", "reset"] as const;
export type ButtonHTMLType = (typeof _ButtonHTMLTypes)[number];

/**
 * 按钮变体
 */
export const _ButtonVariantTypes = [
  "outlined",
  "dashed",
  "solid",
  "filled",
  "text",
  "link",
] as const;
export type ButtonVariantType = (typeof _ButtonVariantTypes)[number];

/**
 * 按钮颜色
 */
export const _ButtonColorTypes = ["default", "primary", "danger"] as const;
export type ButtonColorType = (typeof _ButtonColorTypes)[number];

IconWrapper

用于包装图标元素的组件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { forwardRef } from "react";
import classNames from "classnames";

export type IconWrapperProps = {
  prefixCls: string;
  className?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
};

const IconWrapper = forwardRef<HTMLSpanElement, IconWrapperProps>(
  (props, ref) => {
    const { className, style, children, prefixCls } = props;

    const iconWrapperCls = classNames(`${prefixCls}-icon`, className);

    return (
      <span ref={ref} className={iconWrapperCls} style={style}>
        {children}
      </span>
    );
  }
);

export default IconWrapper;

LoadingIcon

用于显示一个加载状态的图标,并结合动画效果的组件。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
import React, { forwardRef } from "react";
import LoadingOutlined from "@ant-design/icons/LoadingOutlined";
import classNames from "classnames";
import CSSMotion from "rc-motion";

import IconWrapper from "./IconWrapper";

type InnerLoadingIconProps = {
  prefixCls: string;
  className?: string;
  style?: React.CSSProperties;
  iconClassName?: string;
};

/**
 * 内层加载图标
 */
const InnerLoadingIcon = forwardRef<HTMLSpanElement, InnerLoadingIconProps>(
  (props, ref) => {
    const { prefixCls, className, style, iconClassName } = props;
    const mergedIconCls = classNames(`${prefixCls}-loading-icon`, className);

    return (
      <IconWrapper
        prefixCls={prefixCls}
        className={mergedIconCls}
        style={style}
        ref={ref}
      >
        <LoadingOutlined className={iconClassName} />
      </IconWrapper>
    );
  }
);

export type LoadingIconProps = {
  prefixCls: string;
  existIcon: boolean;
  loading?: boolean | object;
  className?: string;
  style?: React.CSSProperties;
};

/**
 * 动画宽度计算函数
 */
const getCollapsedWidth = (): React.CSSProperties => ({
  width: 0,
  opacity: 0,
  transform: "scale(0)",
});

const getRealWidth = (node: HTMLElement): React.CSSProperties => ({
  width: node.scrollWidth,
  opacity: 1,
  transform: "scale(1)",
});

const LoadingIcon: React.FC<LoadingIconProps> = (props) => {
  const { prefixCls, loading, existIcon, className, style } = props;
  const visible = !!loading;

  if (existIcon) {
    return (
      <InnerLoadingIcon
        prefixCls={prefixCls}
        className={className}
        style={style}
      />
    );
  }

  // 过渡动画
  return (
    <CSSMotion
      visible={visible}
      // We do not really use this motionName
      motionName={`${prefixCls}-loading-icon-motion`}
      motionLeave={visible}
      removeOnLeave
      onAppearStart={getCollapsedWidth}
      onAppearActive={getRealWidth}
      onEnterStart={getCollapsedWidth}
      onEnterActive={getRealWidth}
      onLeaveStart={getRealWidth}
      onLeaveActive={getCollapsedWidth}
    >
      {(
        { className: motionCls, style: motionStyle },
        ref: React.Ref<HTMLSpanElement>
      ) => (
        <InnerLoadingIcon
          prefixCls={prefixCls}
          className={className}
          style={{ ...style, ...motionStyle }}
          ref={ref}
          iconClassName={motionCls}
        />
      )}
    </CSSMotion>
  );
};

export default LoadingIcon;

Types

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
export type LegacyButtonType = ButtonType | "danger";

export interface BaseButtonProps {
  // 按钮类型
  type?: ButtonType;
  // 按钮颜色
  color?: ButtonColorType;
  // 按钮变体
  variant?: ButtonVariantType;
  // 按钮的图标组件
  icon?: React.ReactNode;
  // 按钮的图标组件的位置
  iconPosition?: "start" | "end";
  // 按钮形状
  shape?: ButtonShape;
  // 按钮尺寸
  size?: SizeType;
  // 按钮失效状态
  disabled?: boolean;
  // 按钮载入状态
  loading?: boolean | { delay?: number };
  // 类前缀
  prefixCls?: string;
  // 类名
  className?: string;
  // 根类名
  rootClassName?: string;
  // 幽灵属性,按钮背景透明
  ghost?: boolean;
  // 危险按钮
  danger?: boolean;
  // 按钮宽度调整为其父宽度的选项
  block?: boolean;
  children?: React.ReactNode;
  [key: `data-${string}`]: string;
  // 语义化结构 class
  classNames?: { icon: string };
  // 语义化结构 style
  styles?: { icon: React.CSSProperties };
}

type MergedHTMLAttributes = Omit<
  React.HTMLAttributes<HTMLElement> &
    React.ButtonHTMLAttributes<HTMLElement> &
    React.AnchorHTMLAttributes<HTMLElement>,
  "type" | "color"
>;

export interface ButtonProps extends BaseButtonProps, MergedHTMLAttributes {
  // 点击跳转的地址
  href?: string;
  // 设置 button 原生的 type 值
  htmlType?: ButtonHTMLType;
  // 默认提供两个汉字之间的空格
  autoInsertSpace?: boolean;
}

// 按钮载入状态
type LoadingConfigType = {
  loading: boolean;
  delay: number;
};

type ColorVariantPairType = [
  color?: ButtonColorType,
  variant?: ButtonVariantType
];

Composed

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
/**
 * 获取 loading 配置
 */
function getLoadingConfig(
  loading: BaseButtonProps["loading"]
): LoadingConfigType {
  if (typeof loading === "object" && loading) {
    let delay = loading?.delay;
    delay = !Number.isNaN(delay) && typeof delay === "number" ? delay : 0;
    return {
      loading: delay <= 0,
      delay,
    };
  }

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

/**
 * 按钮类型映射 - 颜色、变体
 */
const ButtonTypeMap: Partial<Record<ButtonType, ColorVariantPairType>> = {
  default: ["default", "outlined"],
  primary: ["primary", "solid"],
  dashed: ["default", "dashed"],
  link: ["primary", "link"],
  text: ["default", "text"],
};

const InternalCompoundedButton = React.forwardRef<
  HTMLButtonElement | HTMLAnchorElement,
  ButtonProps
>((props, ref) => {
  const {
    loading = false,
    prefixCls: customizePrefixCls,
    color,
    variant,
    type,
    danger = false,
    shape = "default",
    size: customizeSize,
    styles,
    disabled: customDisabled,
    className,
    rootClassName,
    children,
    icon,
    iconPosition = "start",
    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 = {},
    autoInsertSpace,
    ...rest
  } = props;

  // https://github.com/ant-design/ant-design/issues/47605
  // Compatible with original `type` behavior
  // 合并类型
  const mergedType = type || "default";

  // 合并颜色和变体
  const [mergedColor, mergedVariant] = useMemo<ColorVariantPairType>(() => {
    if (color && variant) {
      return [color, variant];
    }

    const colorVariantPair = ButtonTypeMap[mergedType] || [];

    if (danger) {
      return ["danger", colorVariantPair[1]];
    }

    return colorVariantPair;
  }, [type, color, variant, danger]);

  const isDanger = mergedColor === "danger";
  const mergedColorText = isDanger ? "dangerous" : mergedColor;

  const { getPrefixCls, direction, button } = useContext(ConfigContext);

  // 默认提供两个汉字之间的空格
  const mergedInsertSpace = autoInsertSpace ?? button?.autoInsertSpace ?? true;

  const prefixCls = getPrefixCls("btn", customizePrefixCls);

  const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);

  // 按钮失效状态
  const disabled = useContext(DisabledContext);
  const mergedDisabled = customDisabled ?? disabled;

  const groupSize = useContext(GroupSizeContext);

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

  const [innerLoading, setLoading] = useState<boolean>(loadingOrDelay.loading);

  const [hasTwoCNChar, setHasTwoCNChar] = useState<boolean>(false);

  const internalRef = createRef<HTMLButtonElement | HTMLAnchorElement>();

  const buttonRef = composeRef(ref, internalRef);

  const needInserted =
    Children.count(children) === 1 &&
    !icon &&
    !isUnBorderedButtonVariant(mergedVariant);

  // loading delay timer
  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]);

  useEffect(() => {
    // FIXME: for HOC usage like <FormatMessage />
    if (!buttonRef || !(buttonRef as any).current || !mergedInsertSpace) {
      return;
    }
    // 获取按钮的文本内容
    const buttonText = (buttonRef as any).current.textContent;
    if (needInserted && isTwoCNChar(buttonText)) {
      if (!hasTwoCNChar) {
        // 文本是否由两个中文字符组成
        setHasTwoCNChar(true);
      }
    } else if (hasTwoCNChar) {
      setHasTwoCNChar(false);
    }
  }, [buttonRef]);

  const handleClick = React.useCallback(
    (
      e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>
    ) => {
      // FIXME: https://github.com/ant-design/ant-design/issues/30207
      // 按钮处于加载或禁用状态,阻止点击
      if (innerLoading || mergedDisabled) {
        e.preventDefault();
        return;
      }
      props.onClick?.(e);
    },
    [props.onClick, innerLoading, mergedDisabled]
  );

  // 警告信息
  if (process.env.NODE_ENV !== "production") {
    const warning = devUseWarning("Button");

    warning(
      !(typeof icon === "string" && icon.length > 2),
      "breaking",
      `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`
    );

    warning(
      !(ghost && isUnBorderedButtonVariant(mergedVariant)),
      "usage",
      "`link` or `text` button can't be a `ghost` button."
    );
  }

  // 处理按钮样式
  const { compactSize, compactItemClassnames } = useCompactItemContext(
    prefixCls,
    direction
  );

  const sizeClassNameMap = { large: "lg", small: "sm", middle: undefined };

  const sizeFullName = useSize(
    (ctxSize) => customizeSize ?? compactSize ?? groupSize ?? ctxSize
  );

  const sizeCls = sizeFullName ? sizeClassNameMap[sizeFullName] ?? "" : "";

  const iconType = innerLoading ? "loading" : icon;

  const linkButtonRestProps = omit(rest as ButtonProps & { navigate: any }, [
    "navigate",
  ]);

  const classes = classNames(
    prefixCls,
    hashId,
    cssVarCls,
    {
      [`${prefixCls}-${shape}`]: shape !== "default" && shape,
      // line(253 - 254): Compatible with versions earlier than 5.21.0
      [`${prefixCls}-${mergedType}`]: mergedType,
      [`${prefixCls}-dangerous`]: danger,
      [`${prefixCls}-color-${mergedColorText}`]: mergedColorText,
      [`${prefixCls}-variant-${mergedVariant}`]: mergedVariant,
      [`${prefixCls}-${sizeCls}`]: sizeCls,
      [`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType,
      [`${prefixCls}-background-ghost`]:
        ghost && !isUnBorderedButtonVariant(mergedVariant),
      [`${prefixCls}-loading`]: innerLoading,
      [`${prefixCls}-two-chinese-chars`]:
        hasTwoCNChar && mergedInsertSpace && !innerLoading,
      [`${prefixCls}-block`]: block,
      [`${prefixCls}-rtl`]: direction === "rtl",
      [`${prefixCls}-icon-end`]: iconPosition === "end",
    },
    compactItemClassnames,
    className,
    rootClassName,
    button?.className
  );

  const fullStyle: React.CSSProperties = { ...button?.style, ...customStyle };

  const iconClasses = classNames(
    customClassNames?.icon,
    button?.classNames?.icon
  );
  const iconStyle: React.CSSProperties = {
    ...(styles?.icon || {}),
    ...(button?.styles?.icon || {}),
  };

  // 图标组件
  const iconNode =
    icon && !innerLoading ? (
      <IconWrapper
        prefixCls={prefixCls}
        className={iconClasses}
        style={iconStyle}
      >
        {icon}
      </IconWrapper>
    ) : (
      <LoadingIcon
        existIcon={!!icon}
        prefixCls={prefixCls}
        loading={innerLoading}
      />
    );

  const kids =
    children || children === 0
      ? spaceChildren(children, needInserted && mergedInsertSpace)
      : null;

  // 渲染 <a> 标签
  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>
    );
  }

  // 渲染 <button> 标签
  let buttonNode = (
    <button
      {...rest}
      type={htmlType}
      className={classes}
      style={fullStyle}
      onClick={handleClick}
      disabled={mergedDisabled}
      ref={buttonRef as React.Ref<HTMLButtonElement>}
    >
      {iconNode}
      {kids}
      {/* Styles: compact */}
      {!!compactItemClassnames && (
        <CompactCmp key="compact" prefixCls={prefixCls} />
      )}
    </button>
  );

  // 无边框样式按钮添加波纹效果
  if (!isUnBorderedButtonVariant(mergedVariant)) {
    buttonNode = (
      <Wave component="Button" disabled={innerLoading}>
        {buttonNode}
      </Wave>
    );
  }
  return wrapCSSVar(buttonNode);
});

type CompoundedComponent = typeof InternalCompoundedButton & {
  Group: typeof Group;
  /** @internal */
  __ANT_BUTTON: boolean;
};

const Button = InternalCompoundedButton as CompoundedComponent;

Button.Group = Group;
Button.__ANT_BUTTON = true;

if (process.env.NODE_ENV !== "production") {
  Button.displayName = "Button";
}

export default Button;

Button.Group

用作按钮组的包装器组件,方便为一组按钮提供一致的样式、尺寸及其布局方向等相关属性。

Composed

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import * as React from "react";
import classNames from "classnames";

import { devUseWarning } from "../_util/warning";
import { ConfigContext } from "../config-provider";
import type { SizeType } from "../config-provider/SizeContext";
import { useToken } from "../theme/internal";

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;
  const prefixCls = getPrefixCls("btn-group", customizePrefixCls);

  const [, , hashId] = useToken();

  let sizeCls = "";

  // 处理 size
  switch (size) {
    case "large":
      sizeCls = "lg";
      break;
    case "small":
      sizeCls = "sm";
      break;
    default:
    // Do nothing
  }

  // 警告信息
  if (process.env.NODE_ENV !== "production") {
    const warning = devUseWarning("Button.Group");

    warning(
      !size || ["large", "small", "middle"].includes(size),
      "usage",
      "Invalid prop `size`."
    );
  }

  const classes = classNames(
    prefixCls,
    {
      [`${prefixCls}-${sizeCls}`]: sizeCls,
      [`${prefixCls}-rtl`]: direction === "rtl",
    },
    className,
    hashId
  );

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

export default ButtonGroup;