import { isNumber, isUndefined, nth } from 'lodash';
import { CheckOutlined, CloseOutlined } from '@react-devui/icons';
import { checkNodeExist, getClassName } from '@react-devui/utils';
import { registerComponentMate, TTANSITION_DURING_BASE } from '../../utils';
import { DCollapseTransition } from '../_transition';
import { DProgress } from '../progress';
import { useComponentConfig, usePrefixConfig } from '../root';
export interface DStepperItem {
step?: number;
title: React.ReactNode;
description?: React.ReactNode;
icon?: React.ReactNode;
status?: 'completed' | 'active' | 'wait' | 'error';
}
export interface DStepperProps<T extends DStepperItem> extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
dList: T[];
dActive?: number;
dPercent?: number;
dVertical?: boolean;
dIconSize?: number;
dLabelBottom?: boolean;
dClickable?: boolean;
onItemClick?: (step: number, item: T) => void;
}
const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DStepper' as const });
export function DStepper<T extends DStepperItem>(props: DStepperProps<T>): JSX.Element | null {
const {
dList,
dActive,
dPercent,
dVertical = false,
dIconSize = 36,
dLabelBottom = false,
dClickable = false,
onItemClick,
...restProps
} = useComponentConfig(COMPONENT_NAME, props);
const dPrefix = usePrefixConfig();
const active = dActive ?? nth(dList, 0)?.step ?? 1;
return (
<div
{...restProps}
className={getClassName(restProps.className, `${dPrefix}stepper`, {
[`${dPrefix}stepper--label-bottom`]: dLabelBottom,
[`${dPrefix}stepper--vertical`]: dVertical,
[`${dPrefix}stepper--button`]: dClickable,
})}
>
{dList.map((item, index) => {
const { step: itemStep = index + 1, title: itemTitle, description: itemDescription, icon: itemIcon, status: itemStatus } = item;
const isCompleted = itemStep < active;
const isActive = itemStep === active;
const isWait = itemStep > active;
const isProgress = isActive && isNumber(dPercent);
const titleNode = (
<div
className={`${dPrefix}stepper__step-title`}
style={{ marginTop: dLabelBottom ? undefined : `calc((${dIconSize}px - 1.1em) / 2)` }}
>
{itemTitle}
</div>
);
const separatoreNode =
dVertical && dLabelBottom
? null
: index !== dList.length - 1 && (
<div
className={`${dPrefix}stepper__step-separator`}
style={{
top: dVertical ? `${dIconSize}px` : `calc((${dIconSize}px - 2px) / 2)`,
left: dVertical ? `${dIconSize / 2}px` : dLabelBottom ? `calc(50% + ${dIconSize / 2}px)` : undefined,
width: !dVertical && dLabelBottom ? `calc(100% - ${dIconSize}px)` : undefined,
height: dVertical ? `calc(100% - ${dIconSize}px)` : undefined,
}}
></div>
);
return (
<div
key={itemStep}
className={getClassName(
`${dPrefix}stepper__step`,
isUndefined(itemStatus)
? {
'is-completed': isCompleted,
'is-active': isActive,
'is-wait': isWait,
}
: {},
{
[`is-${itemStatus}`]: itemStatus,
[`${dPrefix}stepper__step--last`]: index === dList.length - 1,
}
)}
style={{
maxWidth: !dVertical && index === dList.length - 1 ? `calc(100% / ${dList.length})` : undefined,
width: !dVertical && dLabelBottom ? `calc(100% / ${dList.length})` : undefined,
}}
role={dClickable ? 'button' : undefined}
tabIndex={dClickable ? 0 : undefined}
onKeyDown={(e) => {
if (dClickable) {
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault();
onItemClick?.(itemStep, item);
}
}
}}
onClick={() => {
if (dClickable) {
onItemClick?.(itemStep, item);
}
}}
>
<div className={`${dPrefix}stepper__step-header`}>
<div
className={getClassName(`${dPrefix}stepper__step-icon`, {
[`${dPrefix}stepper__step-icon--progress`]: isProgress,
})}
style={{
width: dIconSize,
height: dIconSize,
}}
>
{itemIcon === false ? null : checkNodeExist(itemIcon) ? (
itemIcon
) : itemStatus === 'error' ? (
<CloseOutlined />
) : isCompleted ? (
<CheckOutlined />
) : (
itemStep
)}
{isProgress && (
<DProgress
className={`${dPrefix}stepper__step-progress`}
dStatus="process"
dPercent={dPercent}
dSize={dIconSize + 8}
dLineWidth={2}
dType="circle"
dLabel={false}
></DProgress>
)}
</div>
{!dLabelBottom && titleNode}
{!dVertical && separatoreNode}
</div>
{dLabelBottom && titleNode}
{dVertical && separatoreNode}
{checkNodeExist(itemDescription) && (
<DCollapseTransition
dOriginalSize={{
height: 'auto',
}}
dCollapsedStyle={{
height: 0,
}}
dIn={!dVertical || isActive}
dDuring={TTANSITION_DURING_BASE}
dStyles={{
entering: {
transition: ['height', 'padding', 'margin'].map((attr) => `${attr} ${TTANSITION_DURING_BASE}ms ease-out`).join(', '),
},
leaving: {
transition: ['height', 'padding', 'margin'].map((attr) => `${attr} ${TTANSITION_DURING_BASE}ms ease-in`).join(', '),
},
leaved: { display: 'none' },
}}
>
{(collapseRef, collapseStyle) => (
<div
ref={collapseRef}
className={`${dPrefix}stepper__step-description`}
style={{
marginLeft: dLabelBottom ? undefined : `calc(${dIconSize}px + 8px)`,
...collapseStyle,
}}
>
{itemDescription}
</div>
)}
</DCollapseTransition>
)}
</div>
);
})}
</div>
);
}