Skip to content

Commit 88df9aa

Browse files
Lyan-ugithub-actions[bot]anlyyao
authored
feat(steps): 组件对齐 mobile-vue (#532)
* feat(steps): 对齐 mobile-vue * fix(steps): 修复参数异常 * test(steps): add unit test * test(steps): update snapshots N * chore: update common * chore: update _common * test: update snapshots * chore: update snapshot --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: anlyyao <anly_yaw@163.com>
1 parent 2228652 commit 88df9aa

30 files changed

+6688
-477
lines changed

site/mobile/mobile.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export default {
235235
{
236236
title: 'Steps 步骤条',
237237
name: 'steps',
238-
component: () => import('tdesign-mobile-react/steps/_example/index.jsx'),
238+
component: () => import('tdesign-mobile-react/steps/_example/index.tsx'),
239239
},
240240
{
241241
title: 'TabBar 标签栏',

src/steps/StepItem.tsx

Lines changed: 142 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import React, { FC, useContext, useMemo } from 'react';
2-
import classnames from 'classnames';
3-
import { Icon } from 'tdesign-icons-react';
1+
import React, { FC, useContext, useEffect, useMemo, useRef, MouseEvent } from 'react';
2+
import classNames from 'classnames';
3+
import { CheckIcon, CloseIcon } from 'tdesign-icons-react';
44
import withNativeProps, { NativeProps } from '../_util/withNativeProps';
5+
import parseTNode from '../_util/parseTNode';
6+
import useDefaultProps from '../hooks/useDefaultProps';
7+
import { usePrefixClass } from '../hooks/useClass';
58
import { TdStepItemProps } from './type';
6-
import useConfig from '../_util/useConfig';
79
import { stepItemDefaultProps } from './defaultProps';
8-
import StepsContext from './StepsContext';
10+
import { StepsContext } from './StepsContext';
911

1012
export enum StepItemStatusEnum {
1113
DEFAULT = 'default',
@@ -19,106 +21,164 @@ export enum StepThemeEnum {
1921
DOT = 'dot',
2022
}
2123

22-
export enum StepLayoutEnum {
23-
VERTICAL = 'vertical',
24-
HORIZONTAL = 'horizontal',
25-
}
26-
27-
export interface StepItemProps extends TdStepItemProps, NativeProps {
28-
index: number;
29-
}
24+
export interface StepItemProps extends TdStepItemProps, NativeProps {}
3025

3126
const StepItem: FC<StepItemProps> = (props) => {
32-
const { title, content, icon, status, index, children } = props;
33-
34-
const { value, readonly, theme, layout, onChange } = useContext(StepsContext);
27+
const { title, content, icon, status, children, titleRight, extra } = useDefaultProps(props, stepItemDefaultProps);
28+
29+
const stepItemClass = usePrefixClass('step-item');
30+
const stepItemRef = useRef<HTMLDivElement>(null);
31+
32+
const {
33+
childrenNodes,
34+
current,
35+
relation,
36+
removeRelation,
37+
onClickItem,
38+
currentStatus: stepsStatus,
39+
layout,
40+
readonly,
41+
theme,
42+
sequence,
43+
} = useContext(StepsContext);
44+
45+
useEffect(() => {
46+
relation(stepItemRef.current);
47+
const stepItemCur = stepItemRef.current;
48+
return () => {
49+
removeRelation(stepItemCur);
50+
};
51+
// eslint-disable-next-line react-hooks/exhaustive-deps
52+
}, []);
53+
54+
const index = useMemo(() => childrenNodes.indexOf(stepItemRef.current), [childrenNodes]);
55+
const isLastChild = useMemo(
56+
() => index === (sequence === 'positive' ? childrenNodes.length - 1 : 0),
57+
[index, sequence, childrenNodes],
58+
);
3559

36-
const { classPrefix } = useConfig();
60+
const dot = useMemo(() => theme === StepThemeEnum.DOT, [theme]);
61+
const currentStatus = useMemo(() => {
62+
if (status !== 'default') return status;
63+
if (index === current) return stepsStatus;
64+
if (index < +current) return 'finish';
65+
return status;
66+
}, [index, current, stepsStatus, status]);
67+
68+
const rootClassName = useMemo(
69+
() =>
70+
classNames(stepItemClass, `${stepItemClass}--${layout}`, {
71+
[`${stepItemClass}--default`]: readonly,
72+
[`${stepItemClass}--${currentStatus}`]: currentStatus,
73+
}),
74+
[stepItemClass, layout, readonly, currentStatus],
75+
);
3776

38-
const name = `${classPrefix}-step`;
77+
const iconWrapperClassName = useMemo(
78+
() => classNames(`${stepItemClass}__anchor`, `${stepItemClass}__anchor--${layout}`),
79+
[stepItemClass, layout],
80+
);
81+
const dotClassName = useMemo(
82+
() => classNames(`${stepItemClass}__dot`, `${stepItemClass}__dot--${currentStatus}`),
83+
[stepItemClass, currentStatus],
84+
);
85+
const iconClassName = useMemo(
86+
() =>
87+
classNames({
88+
[`${stepItemClass}__icon`]: icon,
89+
[`${stepItemClass}__icon--${currentStatus}`]: icon,
90+
[`${stepItemClass}__circle`]: !icon,
91+
[`${stepItemClass}__circle--${currentStatus}`]: !icon,
92+
}),
93+
[stepItemClass, currentStatus, icon],
94+
);
95+
const contentClassName = useMemo(
96+
() =>
97+
classNames(`${stepItemClass}__content`, `${stepItemClass}__content--${layout}`, {
98+
[`${stepItemClass}__content--last`]: isLastChild,
99+
}),
100+
[stepItemClass, layout, isLastChild],
101+
);
39102

40-
const dot = useMemo(() => theme === StepThemeEnum.DOT && layout === StepLayoutEnum.VERTICAL, [theme, layout]);
103+
const titleClassName = useMemo(
104+
() =>
105+
classNames(
106+
`${stepItemClass}__title`,
107+
`${stepItemClass}__title--${currentStatus}`,
108+
`${stepItemClass}__title--${layout}`,
109+
),
110+
[stepItemClass, currentStatus, layout],
111+
);
112+
const descriptionClassName = useMemo(
113+
() =>
114+
classNames(
115+
`${stepItemClass}__description`,
116+
`${stepItemClass}__description--${currentStatus}`,
117+
`${stepItemClass}__description--${layout}`,
118+
),
119+
[stepItemClass, currentStatus, layout],
120+
);
121+
const extraClassName = useMemo(
122+
() =>
123+
classNames(
124+
`${stepItemClass}__extra`,
125+
`${stepItemClass}__extra--${currentStatus}`,
126+
`${stepItemClass}__extra--${layout}`,
127+
),
128+
[stepItemClass, currentStatus, layout],
129+
);
130+
const separatorClassName = useMemo(
131+
() =>
132+
classNames(
133+
`${stepItemClass}__line`,
134+
`${stepItemClass}__line--${currentStatus}`,
135+
`${stepItemClass}__line--${sequence}`,
136+
`${stepItemClass}__line--${layout}`,
137+
`${stepItemClass}__line--${theme}`,
138+
),
139+
[stepItemClass, currentStatus, layout, sequence, theme],
140+
);
41141

42-
const onStepClick = (e) => {
43-
if (readonly || dot) return;
44-
const currentValue = index;
45-
onChange(currentValue, value, { e });
142+
const onStepClick = (e: MouseEvent<HTMLDivElement>) => {
143+
if (readonly) return;
144+
onClickItem(index, current, { e });
46145
};
47146

48-
const innerClassName = useMemo(() => {
49-
if (typeof icon === 'boolean') {
50-
return `${name}__inner`;
147+
const renderIconContent = () => {
148+
if (icon) {
149+
return parseTNode(icon);
51150
}
52-
return classnames(`${name}__inner`, `${name}__inner__icon`);
53-
}, [name, icon]);
54151

55-
const iconContent = useMemo(() => {
56-
if (dot) {
57-
return '';
152+
if (currentStatus === StepItemStatusEnum.ERROR) {
153+
return <CloseIcon />;
58154
}
59155

60-
if (status === StepItemStatusEnum.ERROR) {
61-
return <Icon name="close" />;
156+
if (currentStatus === StepItemStatusEnum.FINISH) {
157+
return <CheckIcon />;
62158
}
63159

64-
if (index < value && readonly) {
65-
return <Icon name="check" />;
66-
}
67-
68-
if (typeof icon === 'boolean') {
69-
return index + 1;
70-
}
71-
72-
if (typeof icon === 'string') {
73-
return <Icon name={icon} size="24" />;
74-
}
75-
76-
return icon;
77-
}, [status, index, value, readonly, icon, dot]);
78-
79-
const currentStatus = useMemo(() => {
80-
if (status !== StepItemStatusEnum.DEFAULT) {
81-
return status;
82-
}
83-
if (+value === index) {
84-
return StepItemStatusEnum.PROCESS;
85-
}
86-
if (+value > index) {
87-
return StepItemStatusEnum.FINISH;
88-
}
89-
return '';
90-
}, [value, index, status]);
160+
return index + 1;
161+
};
91162

92163
return withNativeProps(
93164
props,
94-
<div
95-
className={classnames(`${name}`, {
96-
[`${name}--default`]: !readonly,
97-
[`${name}--${currentStatus}`]: currentStatus,
98-
})}
99-
>
100-
<div className={innerClassName}>
101-
<div className={`${name}-icon`}>
102-
<div
103-
className={classnames(`${name}-icon__number`, {
104-
[`${name}-icon__dot`]: dot,
105-
})}
106-
onClick={onStepClick}
107-
>
108-
{iconContent}
109-
</div>
110-
</div>
111-
<div className={`${name}-content`}>
112-
<div className={`${name}-title`}>{title}</div>
113-
<div className={`${name}-description`}>{content || children}</div>
114-
<div className={`${name}-extra`} />
165+
<div ref={stepItemRef} className={rootClassName} onClick={onStepClick}>
166+
<div className={iconWrapperClassName}>
167+
{dot ? <div className={dotClassName}></div> : <div className={iconClassName}>{renderIconContent()}</div>}
168+
</div>
169+
<div className={contentClassName}>
170+
<div className={titleClassName}>
171+
{parseTNode(title)}
172+
{layout === 'vertical' && parseTNode(titleRight)}
115173
</div>
174+
<div className={descriptionClassName}>{parseTNode(content) || parseTNode(children)}</div>
175+
<div className={extraClassName}>{parseTNode(extra)}</div>
116176
</div>
177+
{!isLastChild && <div className={separatorClassName}></div>}
117178
</div>,
118179
);
119180
};
120181

121182
StepItem.displayName = 'StepItem';
122-
StepItem.defaultProps = stepItemDefaultProps;
123183

124184
export default StepItem;

src/steps/Steps.tsx

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import React, { FC, useMemo } from 'react';
2-
import classnames from 'classnames';
1+
import React, { FC, useCallback, useMemo, useState } from 'react';
2+
import classNames from 'classnames';
33
import withNativeProps, { NativeProps } from '../_util/withNativeProps';
44
import useDefault from '../_util/useDefault';
5+
import parseTNode from '../_util/parseTNode';
6+
import { usePrefixClass } from '../hooks/useClass';
7+
import useDefaultProps from '../hooks/useDefaultProps';
58
import { TdStepsProps } from './type';
6-
import useConfig from '../_util/useConfig';
79
import { stepsDefaultProps } from './defaultProps';
8-
import StepsContext from './StepsContext';
9-
import StepItem from './StepItem';
10+
import { StepsProvider } from './StepsContext';
1011

1112
export interface StepsProps extends TdStepsProps, NativeProps {}
1213

@@ -16,51 +17,66 @@ const Steps: FC<StepsProps> = (props) => {
1617
layout,
1718
readonly,
1819
theme,
19-
separator,
2020
current,
2121
defaultCurrent,
22+
sequence,
23+
currentStatus,
2224
onChange: onCurrentChange,
23-
options,
24-
} = props;
25+
} = useDefaultProps(props, stepsDefaultProps);
2526

26-
const { classPrefix } = useConfig();
27-
28-
const name = `${classPrefix}-steps`;
27+
const stepsClass = usePrefixClass('steps');
2928

3029
const [value, onChange] = useDefault(current, defaultCurrent, onCurrentChange);
3130

32-
const stepItemList = useMemo(() => {
33-
if (options) {
34-
return options.map((item, index: number) => <StepItem key={index} {...item} index={index} />);
35-
}
36-
return React.Children.map(children, (child: JSX.Element, index: number) =>
37-
React.cloneElement(child, {
38-
index,
31+
const [childrenNodes, setChildrenNodes] = useState<HTMLElement[]>([]);
32+
33+
const stepsClassName = useMemo(
34+
() =>
35+
classNames(stepsClass, `${stepsClass}--${layout}`, `${stepsClass}--${sequence}`, {
36+
[`${stepsClass}--readonly`]: readonly,
3937
}),
40-
);
41-
}, [children, options]);
38+
[stepsClass, layout, sequence, readonly],
39+
);
40+
41+
const relation = useCallback((ele: HTMLElement) => {
42+
ele && setChildrenNodes((prev) => [...prev, ele]);
43+
}, []);
44+
45+
const removeRelation = useCallback((ele: HTMLElement) => {
46+
setChildrenNodes((prev) => prev.filter((item) => item !== ele));
47+
}, []);
48+
49+
const onClickItem = useCallback(
50+
(cur, prev, context) => {
51+
onChange(cur, prev, context);
52+
},
53+
[onChange],
54+
);
55+
56+
const memoProviderValues = useMemo(
57+
() => ({
58+
childrenNodes,
59+
current: value,
60+
relation,
61+
removeRelation,
62+
onClickItem,
63+
currentStatus,
64+
layout,
65+
readonly,
66+
theme,
67+
sequence,
68+
}),
69+
[childrenNodes, value, relation, removeRelation, onClickItem, currentStatus, layout, readonly, theme, sequence],
70+
);
4271

4372
return withNativeProps(
4473
props,
45-
<StepsContext.Provider value={{ value, readonly, theme, layout, onChange }}>
46-
<div
47-
className={classnames(
48-
name,
49-
`${name}--${layout}`,
50-
`${name}--${theme}-anchor`,
51-
`${name}--${separator}-separator`,
52-
{
53-
[`${name}--readonly`]: readonly,
54-
},
55-
)}
56-
>
57-
{stepItemList}
58-
</div>
59-
</StepsContext.Provider>,
74+
<StepsProvider value={memoProviderValues}>
75+
<div className={stepsClassName}>{parseTNode(children)}</div>
76+
</StepsProvider>,
6077
);
6178
};
6279

6380
Steps.displayName = 'Steps';
64-
Steps.defaultProps = stepsDefaultProps;
6581

6682
export default Steps;

0 commit comments

Comments
 (0)