Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions site/site.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@ export default {
path: '/react/components/tag',
component: () => import('tdesign-react/tag/tag.md'),
},
{
title: 'Timeline 时间轴',
name: 'timeline',
path: '/react/components/timeline',
component: () => import('tdesign-react/timeline/timeline.md'),
},
{
title: 'Tooltip 文字提示',
name: 'tooltip',
Expand Down
10 changes: 6 additions & 4 deletions src/_util/renderTNode.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React from 'react';
import { TNode } from '../common';

type RenderType<T> = T extends () => infer P ? P : T;

/**
* 渲染 TNode 类型节点
* 渲染 任意 T | () => T 类型节点
* 默认类型为TNode
* @param tnode
*/
export default function renderTNode(tnode: TNode, defaultNode?: React.ReactNode): React.ReactNode {
export default function renderTNode<T = TNode>(tnode: T, defaultNode?: T): RenderType<T> {
if (typeof tnode === 'function') {
return tnode();
}

return tnode || defaultNode;
return tnode || (typeof defaultNode === 'function' ? defaultNode() : defaultNode);
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export * from './watermark';
export * from './image-viewer';
export * from './space';
export * from './jumper';
export * from './timeline';
export * from './image';
export * from './rate';
export * from './link';
77 changes: 77 additions & 0 deletions src/timeline/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import classNames from 'classnames';
import React from 'react';
import { StyledProps } from '../common';
import useConfig from '../hooks/useConfig';
import TimelineItem from './TimelineItem';
import { TdTimelineProps } from './type';
import TimelineContext from './TimelineContext';
import forwardRefWithStatics from '../_util/forwardRefWithStatics';
import { useAlign } from './useAlign';

export interface TimelineProps extends TdTimelineProps, StyledProps {
children?: React.ReactNode;
}

const Timeline = forwardRefWithStatics(
(props: TimelineProps, ref: React.Ref<HTMLUListElement>) => {
const {
theme = 'default',
labelAlign = 'left',
children,
className,
style,
reverse = false,
layout = 'vertical',
mode = 'alternate',
} = props;
const { classPrefix } = useConfig();
const renderAlign = useAlign(labelAlign, layout);

const timelineItems = React.Children.toArray(children).filter(
(child: JSX.Element) => child.type.displayName === TimelineItem.displayName,
);
// 获取所有子节点类型
const itemsStatus = React.Children.map(timelineItems, (child: JSX.Element) => child.props?.dotColor || 'primary');
const hasLabelItem = timelineItems.some((item: React.ReactElement<any>) => !!item?.props?.label);

if (reverse) {
timelineItems.reverse();
}

const itemsCounts = React.Children.count(timelineItems);

const timelineClassName = classNames(
`${classPrefix}-timeline`,
{
[`${classPrefix}-timeline-${renderAlign}`]: true,
[`${classPrefix}-timeline-reverse`]: reverse,
[`${classPrefix}-timeline-${layout}`]: true,
[`${classPrefix}-timeline-label`]: hasLabelItem,
[`${classPrefix}-timeline-label--${mode}`]: true,
},
className,
);

return (
<TimelineContext.Provider value={{ theme, reverse, itemsStatus, layout, globalAlign: labelAlign, mode }}>
<ul className={timelineClassName} style={style} ref={ref}>
{React.Children.map(timelineItems, (ele: JSX.Element, index) =>
React.cloneElement(ele, {
index,
className: classNames([ele?.props?.className], {
[`${classPrefix}-timeline-item--last`]: index === itemsCounts - 1,
}),
}),
)}
</ul>
</TimelineContext.Provider>
);
},
{
Item: TimelineItem,
},
);

Timeline.displayName = 'Timeline';

export default Timeline;
19 changes: 19 additions & 0 deletions src/timeline/TimelineContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { TdTimelineProps } from './type';

const StepsContext = React.createContext<{
theme: TdTimelineProps['theme'];
reverse: TdTimelineProps['reverse'];
itemsStatus: string[];
layout: TdTimelineProps['layout'];
globalAlign?: TdTimelineProps['labelAlign'];
mode?: TdTimelineProps['mode'];
}>({
theme: 'default',
reverse: false,
itemsStatus: [],
layout: 'vertical',
mode: 'alternate',
});

export default StepsContext;
112 changes: 112 additions & 0 deletions src/timeline/TimelineItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useContext, useMemo } from 'react';
import classNames from 'classnames';
import { TdTimelineItemProps } from './type';
import { StyledProps } from '../common';
import useConfig from '../hooks/useConfig';
import TimelineContext from './TimelineContext';
import renderTNode from '../_util/renderTNode';
import { useAlign } from './useAlign';
import Loading from '../loading';

export interface TimelineItemProps extends TdTimelineItemProps, StyledProps {
children?: React.ReactNode;
index?: number;
}

const DefaultTheme = ['default', 'primary', 'success', 'warning', 'error'];

const TimelineItem: React.FC<TimelineItemProps> = (props) => {
const {
className,
style = {},
dot,
dotColor = 'primary',
labelAlign,
children,
index,
content,
label,
loading = false,
} = props;
const { theme, reverse, itemsStatus, layout, globalAlign, mode } = useContext(TimelineContext);
const { classPrefix } = useConfig();
const renderAlign = useAlign(globalAlign, layout);

// 计算节点模式 CSS 类名
const getPositionClassName = (index: number) => {
// 横向布局 以及 纵向布局对应为不同的样式名
const left = layout === 'horizontal' ? 'top' : 'left';
const right = layout === 'horizontal' ? 'bottom' : 'right';
// 单独设置则单独生效
if (renderAlign === 'alternate') {
return labelAlign || index % 2 === 0
? `${classPrefix}-timeline-item-${left}`
: `${classPrefix}-timeline-item-${right}`;
}
if (renderAlign === 'left' || renderAlign === 'top') {
return `${classPrefix}-timeline-item-${left}`;
}
if (renderAlign === 'right' || renderAlign === 'bottom') {
return `${classPrefix}-timeline-item-${right}`;
}
return '';
};

const dotElement = useMemo(() => {
const ele = renderTNode(dot);
return (
ele &&
React.cloneElement(ele, {
className: classNames(ele?.props?.className, `${classPrefix}-timeline-item__dot-content`),
})
);
}, [dot, classPrefix]);

// 节点类名
const itemClassName = classNames(
{
[`${classPrefix}-timeline-item`]: true,
[`${getPositionClassName(index)}`]: true,
},
className,
);

// 连线类名
const tailClassName = classNames({
[`${classPrefix}-timeline-item__tail`]: true,
[`${classPrefix}-timeline-item__tail--theme-${theme}`]: true,
[`${classPrefix}-timeline-item__tail--status-${itemsStatus[index]}`]: reverse,
});

// 圆圈类名
const dotClassName = classNames({
[`${classPrefix}-timeline-item__dot`]: true,
[`${classPrefix}-timeline-item__dot--custom`]: !!dotElement || (!dotElement && loading),
[`${classPrefix}-timeline-item__dot--${dotColor}`]: DefaultTheme.includes(dotColor),
});

const labelClassName = classNames(`${classPrefix}-timeline-item__label`, {
[`${classPrefix}-timeline-item__label--${mode}`]: true,
});

return (
<li className={itemClassName} style={style}>
{mode === 'alternate' && label && <div className={labelClassName}>{label}</div>}
<div className={`${classPrefix}-timeline-item__wrapper`}>
<div className={dotClassName} style={{ borderColor: !DefaultTheme.includes(dotColor) && dotColor }}>
{!dotElement && loading && <Loading size="12px" className={`${classPrefix}-timeline-item__dot-content`} />}
{dotElement}
</div>
<div className={tailClassName} />
</div>
<div className={`${classPrefix}-timeline-item__content`}>
{content || children}
{mode === 'same' && label && <div className={labelClassName}>{label}</div>}
</div>
</li>
);
};

TimelineItem.displayName = 'TimelineItem';

export default TimelineItem;
Loading