Skip to content
Merged
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rc-component/steps",
"version": "1.1.0",
"version": "1.2.0-alpha.5",
"description": "steps ui component for react",
"keywords": [
"react",
Expand Down
10 changes: 10 additions & 0 deletions src/Context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as React from 'react';
import type { StepsProps } from './Steps';

export interface StepsContextProps {
prefixCls: string;
classNames: NonNullable<StepsProps['classNames']>;
styles: NonNullable<StepsProps['styles']>;
}

export const StepsContext = React.createContext<StepsContextProps>(null!);
15 changes: 5 additions & 10 deletions src/Rail.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import * as React from 'react';
import cls from 'classnames';
import type { Status, StepsProps } from './Steps';
import type { Status } from './Steps';

export interface RailProps {
prefixCls: string;
classNames: StepsProps['classNames'];
styles: StepsProps['styles'];
className: string;
style: React.CSSProperties;
status: Status;
}

export default function Rail(props: RailProps) {
const { prefixCls, classNames, styles, status } = props;
const { prefixCls, className, style, status } = props;
const railCls = `${prefixCls}-rail`;

// ============================= render =============================
return (
<div
className={cls(railCls, `${railCls}-${status}`, classNames.itemRail)}
style={styles.itemRail}
/>
);
return <div className={cls(railCls, `${railCls}-${status}`, className)} style={style} />;
}
122 changes: 96 additions & 26 deletions src/Step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import cls from 'classnames';
import KeyCode from '@rc-component/util/lib/KeyCode';
import type { Status, StepItem, StepsProps } from './Steps';
import Rail from './Rail';
import { UnstableContext } from './UnstableContext';
import StepIcon, { StepIconSemanticContext } from './StepIcon';

function hasContent<T>(value: T) {
return value !== undefined && value !== null;
}

export interface StepProps {
// style
Expand All @@ -18,12 +24,6 @@ export interface StepProps {
index: number;
last: boolean;

// stepIndex?: number;
// stepNumber?: number;
// title?: React.ReactNode;
// subTitle?: React.ReactNode;
// description?: React.ReactNode;

// render
iconRender?: StepsProps['iconRender'];
icon?: React.ReactNode;
Expand Down Expand Up @@ -59,6 +59,9 @@ export default function Step(props: StepProps) {

const itemCls = `${prefixCls}-item`;

// ==================== Internal Context ====================
const { railFollowPrevStatus } = React.useContext(UnstableContext);

// ========================== Data ==========================
const {
onClick: onItemClick,
Expand All @@ -72,6 +75,9 @@ export default function Step(props: StepProps) {

className,
style,
classNames: itemClassNames = {},
styles: itemStyles = {},

...restItemProps
} = data;

Expand All @@ -92,8 +98,8 @@ export default function Step(props: StepProps) {
const accessibilityProps: {
role?: string;
tabIndex?: number;
onClick?: React.MouseEventHandler<HTMLDivElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
onClick?: React.MouseEventHandler<HTMLLIElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLLIElement>;
} = {};

if (clickable) {
Expand All @@ -115,46 +121,109 @@ export default function Step(props: StepProps) {
// ========================= Render =========================
const mergedStatus = status || 'wait';

const hasTitle = hasContent(title);
const hasSubTitle = hasContent(subTitle);

const classString = cls(
itemCls,
`${itemCls}-${mergedStatus}`,
{
[`${itemCls}-custom`]: icon,
[`${itemCls}-active`]: active,
[`${itemCls}-disabled`]: disabled === true,
[`${itemCls}-empty-header`]: !hasTitle && !hasSubTitle,
},
className,
classNames.item,
itemClassNames.root,
);

let iconNode = <StepIcon />;
if (iconRender) {
iconNode = iconRender(iconNode, {
...renderInfo,
components: {
Icon: StepIcon,
},
}) as React.ReactElement;
}

const wrapperNode = (
<div className={cls(`${itemCls}-wrapper`, classNames.itemWrapper)} style={styles.itemWrapper}>
<div className={cls(`${itemCls}-icon`, classNames.itemIcon)} style={styles.itemIcon}>
{iconRender?.(renderInfo)}
</div>
<div className={cls(`${itemCls}-section`, classNames.itemSection)} style={styles.itemSection}>
<div className={cls(`${itemCls}-header`, classNames.itemHeader)} style={styles.itemHeader}>
<div className={cls(`${itemCls}-title`, classNames.itemTitle)} style={styles.itemTitle}>
{title}
</div>
{subTitle && (
<div
className={cls(`${itemCls}-wrapper`, classNames.itemWrapper, itemClassNames.wrapper)}
style={{
...styles.itemWrapper,
...itemStyles.wrapper,
}}
>
{/* Icon */}
<StepIconSemanticContext.Provider
value={{
className: itemClassNames.icon,
style: itemStyles.icon,
}}
>
{iconNode}
</StepIconSemanticContext.Provider>

<div
className={cls(`${itemCls}-section`, classNames.itemSection, itemClassNames.section)}
style={{
...styles.itemSection,
...itemStyles.section,
}}
>
<div
className={cls(`${itemCls}-header`, classNames.itemHeader, itemClassNames.header)}
style={{
...styles.itemHeader,
...itemStyles.header,
}}
>
{hasTitle && (
<div
className={cls(`${itemCls}-title`, classNames.itemTitle, itemClassNames.title)}
style={{
...styles.itemTitle,
...itemStyles.title,
}}
>
{title}
</div>
)}
{hasSubTitle && (
<div
title={typeof subTitle === 'string' ? subTitle : undefined}
className={cls(`${itemCls}-subtitle`, classNames.itemSubtitle)}
style={styles.itemSubtitle}
className={cls(
`${itemCls}-subtitle`,
classNames.itemSubtitle,
itemClassNames.subtitle,
)}
style={{
...styles.itemSubtitle,
...itemStyles.subtitle,
}}
>
{subTitle}
</div>
)}

{!last && (
<Rail prefixCls={itemCls} classNames={classNames} styles={styles} status={nextStatus} />
<Rail
prefixCls={itemCls}
className={cls(classNames.itemRail, itemClassNames.rail)}
style={{
...styles.itemRail,
...itemStyles.rail,
}}
status={railFollowPrevStatus ? status : nextStatus}
/>
)}
</div>
{mergedContent && (
{hasContent(mergedContent) && (
<div
className={cls(`${itemCls}-content`, classNames.itemContent)}
style={styles.itemContent}
className={cls(`${itemCls}-content`, classNames.itemContent, itemClassNames.content)}
style={{ ...styles.itemContent, ...itemStyles.content }}
>
{mergedContent}
</div>
Expand All @@ -164,17 +233,18 @@ export default function Step(props: StepProps) {
);

let stepNode: React.ReactNode = (
<div
<li
{...restItemProps}
{...accessibilityProps}
className={classString}
style={{
...styles.item,
...itemStyles.root,
...style,
}}
>
{itemWrapperRender ? itemWrapperRender(wrapperNode) : wrapperNode}
</div>
</li>
);

if (itemRender) {
Expand Down
39 changes: 39 additions & 0 deletions src/StepIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import cls from 'classnames';
import { StepsContext } from './Context';
import pickAttrs from '@rc-component/util/lib/pickAttrs';

export interface StepIconSemanticContextProps {
className?: string;
style?: React.CSSProperties;
}

export const StepIconSemanticContext = React.createContext<StepIconSemanticContextProps>({});

export type StepIconProps = React.HTMLAttributes<HTMLDivElement>;

const StepIcon = React.forwardRef<HTMLDivElement, StepIconProps>((props, ref) => {
const { className, style, children, ...restProps } = props;

const { prefixCls, classNames, styles } = React.useContext(StepsContext);
const { className: itemClassName, style: itemStyle } = React.useContext(StepIconSemanticContext);

const itemCls = `${prefixCls}-item`;

return (
<div
{...pickAttrs(restProps, false)}
ref={ref}
className={cls(`${itemCls}-icon`, classNames.itemIcon, itemClassName, className)}
style={{
...styles.itemIcon,
...itemStyle,
...style,
}}
>
{children}
</div>
);
});

export default StepIcon;
Loading