mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-11 11:32:52 +08:00
384 lines
10 KiB
TypeScript
384 lines
10 KiB
TypeScript
import * as React from 'react';
|
|
import CheckOutlined from '@ant-design/icons/CheckOutlined';
|
|
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
|
import RcSteps from '@rc-component/steps';
|
|
import type { StepsProps as RcStepsProps } from '@rc-component/steps/lib/Steps';
|
|
import cls from 'classnames';
|
|
|
|
import useMergeSemantic from '../_util/hooks/useMergeSemantic';
|
|
import { devUseWarning } from '../_util/warning';
|
|
import Wave from '../_util/wave';
|
|
import { TARGET_CLS } from '../_util/wave/interface';
|
|
import { useComponentConfig } from '../config-provider/context';
|
|
import useSize from '../config-provider/hooks/useSize';
|
|
import useBreakpoint from '../grid/hooks/useBreakpoint';
|
|
import Tooltip from '../tooltip';
|
|
import { BlockContext } from './context';
|
|
import PanelArrow from './PanelArrow';
|
|
import ProgressIcon from './ProgressIcon';
|
|
import useStyle from './style';
|
|
import type { GetProp } from '../_util/type';
|
|
|
|
type RcIconRenderTypeInfo = Parameters<NonNullable<RcStepsProps['iconRender']>>[1];
|
|
|
|
export type IconRenderType = (
|
|
oriNode: React.ReactNode,
|
|
info: Pick<RcIconRenderTypeInfo, 'index' | 'active' | 'item' | 'components'>,
|
|
) => React.ReactNode;
|
|
|
|
interface StepItem {
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
classNames?: GetProp<RcStepsProps, 'items'>[number]['classNames'];
|
|
styles?: GetProp<RcStepsProps, 'items'>[number]['styles'];
|
|
|
|
/** @deprecated Please use `content` instead */
|
|
description?: React.ReactNode;
|
|
content?: React.ReactNode;
|
|
icon?: React.ReactNode;
|
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
status?: 'wait' | 'process' | 'finish' | 'error';
|
|
disabled?: boolean;
|
|
title?: React.ReactNode;
|
|
subTitle?: React.ReactNode;
|
|
}
|
|
|
|
export type ProgressDotRender = (
|
|
iconDot: React.ReactNode,
|
|
info: {
|
|
index: number;
|
|
status: NonNullable<RcStepsProps['status']>;
|
|
title: React.ReactNode;
|
|
/** @deprecated Please use `content` instead. */
|
|
description: React.ReactNode;
|
|
content: React.ReactNode;
|
|
},
|
|
) => React.ReactNode;
|
|
|
|
export interface StepsProps {
|
|
// Style
|
|
prefixCls?: string;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
rootClassName?: string;
|
|
classNames?: RcStepsProps['classNames'];
|
|
styles?: RcStepsProps['styles'];
|
|
variant?: 'filled' | 'outlined';
|
|
size?: 'default' | 'small';
|
|
|
|
// Layout
|
|
type?: 'default' | 'navigation' | 'inline' | 'panel' | 'dot';
|
|
/** @deprecated Please use `orientation` instead. */
|
|
direction?: 'horizontal' | 'vertical';
|
|
orientation?: 'horizontal' | 'vertical';
|
|
/** @deprecated Please use `titlePlacement` instead. */
|
|
labelPlacement?: 'horizontal' | 'vertical';
|
|
titlePlacement?: 'horizontal' | 'vertical';
|
|
/** @deprecated Please use `type` and `iconRender` instead. */
|
|
progressDot?: boolean | ProgressDotRender;
|
|
responsive?: boolean;
|
|
ellipsis?: boolean;
|
|
/**
|
|
* Set offset cell, only work when `type` is `inline`.
|
|
*/
|
|
offset?: number;
|
|
|
|
// Data
|
|
current?: number;
|
|
initial?: number;
|
|
items?: StepItem[];
|
|
percent?: number;
|
|
status?: 'wait' | 'process' | 'finish' | 'error';
|
|
|
|
// Render
|
|
iconRender?: IconRenderType;
|
|
|
|
// Events
|
|
onChange?: (current: number) => void;
|
|
}
|
|
|
|
const waveEffectClassNames: StepsProps['classNames'] = {
|
|
itemIcon: TARGET_CLS,
|
|
};
|
|
|
|
const Steps = (props: StepsProps) => {
|
|
const {
|
|
// Style
|
|
size,
|
|
className,
|
|
rootClassName,
|
|
style,
|
|
variant = 'filled',
|
|
type,
|
|
classNames,
|
|
styles,
|
|
|
|
// Layout
|
|
direction,
|
|
orientation,
|
|
responsive = true,
|
|
progressDot,
|
|
labelPlacement,
|
|
titlePlacement,
|
|
ellipsis,
|
|
offset = 0,
|
|
|
|
// Data
|
|
items,
|
|
percent,
|
|
current = 0,
|
|
onChange,
|
|
|
|
// Render
|
|
iconRender,
|
|
|
|
// MISC
|
|
...restProps
|
|
} = props;
|
|
|
|
const blockContent = React.useContext(BlockContext);
|
|
|
|
const contextContent = useComponentConfig('steps');
|
|
|
|
const {
|
|
getPrefixCls,
|
|
direction: rtlDirection,
|
|
className: contextClassName,
|
|
style: contextStyle,
|
|
} = contextContent;
|
|
|
|
let contextClassNames: StepsProps['classNames'];
|
|
let contextStyles: StepsProps['styles'];
|
|
|
|
if (!blockContent) {
|
|
({ classNames: contextClassNames, styles: contextStyles } = contextContent);
|
|
}
|
|
|
|
const prefixCls = getPrefixCls('steps', props.prefixCls);
|
|
const itemIconCls = `${prefixCls}-item-icon`;
|
|
|
|
const [hashId, cssVarCls] = useStyle(prefixCls);
|
|
|
|
// ============================= Size =============================
|
|
const mergedSize = useSize(size);
|
|
|
|
// ============================= Item =============================
|
|
const mergedItems = React.useMemo(() => (items || []).filter(Boolean), [items]);
|
|
|
|
// ============================ Styles ============================
|
|
const [mergedClassNames, mergedStyles] = useMergeSemantic(
|
|
[waveEffectClassNames, contextClassNames, classNames],
|
|
[contextStyles, styles],
|
|
);
|
|
|
|
// ============================ Layout ============================
|
|
const { xs } = useBreakpoint(responsive);
|
|
|
|
// Type
|
|
const mergedType = React.useMemo(() => {
|
|
if (type && type !== 'default') {
|
|
return type;
|
|
}
|
|
|
|
if (progressDot) {
|
|
return 'dot';
|
|
}
|
|
|
|
return type;
|
|
}, []);
|
|
|
|
const isInline = mergedType === 'inline';
|
|
const isDot = mergedType === 'dot' || mergedType === 'inline';
|
|
|
|
// Progress Dot Render function
|
|
const legacyProgressDotRender = React.useMemo(() => {
|
|
return mergedType === 'dot' && typeof progressDot === 'function' ? progressDot : undefined;
|
|
}, [progressDot]);
|
|
|
|
const mergedOrientation = React.useMemo<StepsProps['orientation']>(() => {
|
|
const nextOrientation = orientation || direction;
|
|
|
|
if (mergedType === 'panel') {
|
|
return 'horizontal';
|
|
}
|
|
|
|
return (responsive && xs) || nextOrientation === 'vertical' ? 'vertical' : 'horizontal';
|
|
}, [xs, direction]);
|
|
|
|
const mergedTitlePlacement = React.useMemo<StepsProps['titlePlacement']>(() => {
|
|
if (isDot || mergedOrientation === 'vertical') {
|
|
return mergedOrientation === 'vertical' ? 'horizontal' : 'vertical';
|
|
}
|
|
if (type === 'navigation') {
|
|
return 'horizontal';
|
|
}
|
|
|
|
return titlePlacement || labelPlacement || 'horizontal';
|
|
}, []);
|
|
|
|
// ========================== Percentage ==========================
|
|
const mergedPercent = isInline ? undefined : percent;
|
|
|
|
// ============================= Icon =============================
|
|
const internalIconRender: RcStepsProps['iconRender'] = (_, info) => {
|
|
const {
|
|
item,
|
|
index,
|
|
active,
|
|
components: { Icon: StepIcon },
|
|
} = info;
|
|
|
|
const { status, icon } = item;
|
|
|
|
let iconContent: React.ReactNode = null;
|
|
|
|
if (isDot || icon) {
|
|
iconContent = icon;
|
|
} else {
|
|
switch (status) {
|
|
case 'finish':
|
|
iconContent = <CheckOutlined className={`${itemIconCls}-finish`} />;
|
|
break;
|
|
case 'error':
|
|
iconContent = <CloseOutlined className={`${itemIconCls}-error`} />;
|
|
break;
|
|
default: {
|
|
let numNode = <span className={`${itemIconCls}-number`}>{info.index + 1}</span>;
|
|
|
|
if (status === 'process' && mergedPercent !== undefined) {
|
|
numNode = (
|
|
<ProgressIcon prefixCls={prefixCls} percent={mergedPercent}>
|
|
{numNode}
|
|
</ProgressIcon>
|
|
);
|
|
}
|
|
|
|
iconContent = numNode;
|
|
}
|
|
}
|
|
}
|
|
|
|
let iconNode: React.ReactNode = <StepIcon>{iconContent}</StepIcon>;
|
|
|
|
// Custom Render Props
|
|
if (iconRender) {
|
|
iconNode = iconRender(iconNode, {
|
|
index,
|
|
active,
|
|
item,
|
|
components: { Icon: StepIcon },
|
|
});
|
|
} else if (typeof legacyProgressDotRender === 'function') {
|
|
iconNode = legacyProgressDotRender(iconNode, {
|
|
index,
|
|
...(item as Required<typeof item>),
|
|
});
|
|
}
|
|
|
|
return iconNode;
|
|
};
|
|
|
|
// ============================ Custom ============================
|
|
const itemRender: RcStepsProps['itemRender'] = (itemNode, itemInfo) => {
|
|
let content = itemNode;
|
|
|
|
if (isInline && itemInfo.item.content) {
|
|
content = (
|
|
<Tooltip destroyOnHidden title={itemInfo.item.content}>
|
|
{itemNode}
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Wave
|
|
component="Steps"
|
|
disabled={itemInfo.item.disabled || !onChange}
|
|
colorSource={variant === 'filled' ? 'color' : null}
|
|
>
|
|
{content}
|
|
</Wave>
|
|
);
|
|
};
|
|
|
|
const itemWrapperRender: RcStepsProps['itemWrapperRender'] =
|
|
mergedType === 'panel'
|
|
? (itemNode) => {
|
|
return (
|
|
<>
|
|
{itemNode}
|
|
<PanelArrow prefixCls={prefixCls} />
|
|
</>
|
|
);
|
|
}
|
|
: undefined;
|
|
|
|
// ============================ Styles ============================
|
|
const mergedStyle: React.CSSProperties = {
|
|
'--steps-items-offset': `${offset}`,
|
|
...contextStyle,
|
|
...style,
|
|
};
|
|
|
|
const stepsClassName = cls(
|
|
contextClassName,
|
|
`${prefixCls}-${variant}`,
|
|
{
|
|
[`${prefixCls}-${mergedType}`]: mergedType !== 'dot' ? mergedType : false,
|
|
[`${prefixCls}-rtl`]: rtlDirection === 'rtl',
|
|
[`${prefixCls}-dot`]: isDot,
|
|
[`${prefixCls}-ellipsis`]: ellipsis,
|
|
[`${prefixCls}-with-progress`]: mergedPercent !== undefined,
|
|
[`${prefixCls}-${mergedSize}`]: mergedSize,
|
|
},
|
|
className,
|
|
rootClassName,
|
|
hashId,
|
|
cssVarCls,
|
|
);
|
|
|
|
// =========================== Warning ============================
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
const warning = devUseWarning('Steps');
|
|
|
|
warning.deprecated(!labelPlacement, 'labelPlacement', 'titlePlacement');
|
|
warning.deprecated(!progressDot, 'progressDot', 'type="dot"');
|
|
warning.deprecated(!direction, 'direction', 'orientation');
|
|
warning.deprecated(
|
|
mergedItems.every((item) => !item.description),
|
|
'items.description',
|
|
'items.content',
|
|
);
|
|
}
|
|
|
|
// ============================ Render ============================
|
|
return (
|
|
<RcSteps
|
|
{...restProps}
|
|
// Style
|
|
prefixCls={prefixCls}
|
|
className={stepsClassName}
|
|
style={mergedStyle}
|
|
classNames={mergedClassNames}
|
|
styles={mergedStyles}
|
|
// Layout
|
|
orientation={mergedOrientation}
|
|
titlePlacement={mergedTitlePlacement}
|
|
// Data
|
|
current={current}
|
|
items={mergedItems}
|
|
onChange={onChange}
|
|
// Render
|
|
iconRender={internalIconRender}
|
|
itemRender={itemRender}
|
|
itemWrapperRender={itemWrapperRender}
|
|
/>
|
|
);
|
|
};
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
Steps.displayName = 'Steps';
|
|
}
|
|
|
|
export default Steps;
|