diff --git a/assets/index.less b/assets/index.less index 4f8ebd83..3a3284bc 100644 --- a/assets/index.less +++ b/assets/index.less @@ -17,37 +17,24 @@ display: inline-block; vertical-align: top; flex: 1; - overflow: hidden; - - &-container[role='button'] { - cursor: pointer; - transition: opacity .3s; - - &:hover { - opacity: 0.7; - } - } + // overflow: hidden; &:last-child { flex: none; } - &:last-child &-tail, &:last-child &-title:after { display: none; } - &-container { - display: inline-block; - } - &-icon, - &-content { + &-section { display: inline-block; vertical-align: top; } &-icon { + flex: none; border: 1px solid @wait-icon-color; width: 26px; height: 26px; @@ -55,8 +42,9 @@ text-align: center; border-radius: 26px; font-size: 14px; - margin-right: 8px; - transition: background-color .3s, border-color .3s; + transition: + background-color 0.3s, + border-color 0.3s; > .@{stepsPrefixClass}-icon { line-height: 1; @@ -72,49 +60,20 @@ } } - &-tail { - position: absolute; - left: 0; - width: 100%; - top: 12px; - padding: 0 10px; - &:after { - content: ''; - display: inline-block; - background: @wait-tail-color; - height: 1px; - border-radius: 1px; - width: 100%; - transition: background .3s; - } - } - &-content { + &-section { margin-top: 3px; } &-title { font-size: 14px; - margin-bottom: 4px; color: #666; font-weight: bold; display: inline-block; - padding-right: 10px; position: relative; - &:after { - content: ''; - height: 1px; - width: 1000px; - background: @wait-tail-color; - display: block; - position: absolute; - top: 0.55em; - left: 100%; - } } &-subtitle { font-size: 12px; display: inline-block; color: #999; - margin-left: 8px; } &-description { font-size: 12px; @@ -138,13 +97,6 @@ .@{stepsPrefixClass}-horizontal:not(.@{stepsPrefixClass}-label-vertical) { .@{stepsPrefixClass}-item { - margin-right: 10px; - &:last-child { - margin-right: 0; - } - &-tail { - display: none; - } &-description { max-width: @stepDescriptionMaxWidth; } @@ -152,10 +104,10 @@ } .step-item-status(@status) { - @icon-color: "@{status}-icon-color"; - @title-color: "@{status}-title-color"; - @description-color: "@{status}-description-color"; - @tail-color: "@{status}-tail-color"; + @icon-color: '@{status}-icon-color'; + @title-color: '@{status}-title-color'; + @description-color: '@{status}-description-color'; + @tail-color: '@{status}-tail-color'; &-@{status} &-icon { border-color: @@icon-color; background-color: #fff; @@ -168,22 +120,102 @@ } &-@{status} &-title { color: @@title-color; - &:after { - background-color: @@tail-color; - } } &-@{status} &-description { color: @@description-color; } - &-@{status} &-tail:after { - background-color: @@tail-color; +} + +// @import 'custom-icon'; +// @import 'small'; +// @import 'vertical'; +// @import 'label-placement'; +// @import 'progress-dot'; +// @import 'nav'; +// @import 'inline'; + +// ======================= Horizontal ======================= +.verticalFlex() { + display: flex; + flex-direction: column; + align-items: center; +} + +.@{stepsPrefixClass} { + .@{stepsPrefixClass}-item { + &-section { + min-width: 0; + } + + &-header { + display: flex; + gap: 8px; + align-items: center; + } + + // Ellipsis + &-title, + &-subtitle, + &-description { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } } -@import 'custom-icon'; -@import 'small'; -@import 'vertical'; -@import 'label-placement'; -@import 'progress-dot'; -@import 'nav'; -@import 'inline'; +.@{stepsPrefixClass}-horizontal { + .@{stepsPrefixClass}-item { + flex: 1; + position: relative; + min-width: 0; + + &-rail { + height: 1px; + background: @process-tail-color; + } + } + + // Label Vertical + &.@{stepsPrefixClass}-label-vertical { + .@{stepsPrefixClass}-item { + .verticalFlex(); + padding-inline: 8px; + + &-section { + .verticalFlex(); + } + + &-rail { + position: absolute; + top: 13px; + left: calc(50% + 13px); + width: 100%; + } + } + } + + // Label Horizontal + &.@{stepsPrefixClass}-label-horizontal { + .@{stepsPrefixClass}-item { + display: flex; + + &:last-child { + flex: none; + } + + &-section { + flex: 1; + } + + &-rail { + flex: 1; + min-width: 0; + } + } + } +} + +// ======================== Vertical ======================== +.@{stepsPrefixClass}-vertical { +} diff --git a/assets/inline.less b/assets/inline.less index 1bbe6a62..519e9907 100644 --- a/assets/inline.less +++ b/assets/inline.less @@ -7,18 +7,6 @@ .@{stepsPrefixClass}-item { flex: none; - &-container { - padding: 9px 4px 0; - margin: 0 2px; - border-radius: 4px; - cursor: pointer; - &:hover { - background: rgba(0, 0, 0, 0.04); - } - &[role='button']:hover { - opacity: 1; - } - } &-icon { width: 6px; @@ -32,7 +20,7 @@ } } - &-content { + &-section { width: auto; margin-top: 7px; } @@ -47,26 +35,7 @@ display: none; } - &-tail { - margin-left: 0; - top: 11px; - &:after { - height: 1px; - } - } - &:first-child .@{stepsPrefixClass}-item-tail{ - width: 50%; - margin-left: 50%; - } - &:last-child .@{stepsPrefixClass}-item-tail{ - display: block; - width: 50%; - } - &-finish { - .@{stepsPrefixClass}-item-tail:after { - background-color: @process-tail-color; - } .@{stepsPrefixClass}-item-icon .@{stepsPrefixClass}-icon .@{stepsPrefixClass}-icon-dot { background-color: @process-tail-color; } diff --git a/assets/label-placement.less b/assets/label-placement.less index 76f07c92..6de6495b 100644 --- a/assets/label-placement.less +++ b/assets/label-placement.less @@ -3,11 +3,7 @@ .@{stepsPrefixClass}-label-vertical { .@{stepsPrefixClass}-item { overflow: visible; - &-tail { - padding: 0px 24px; - margin-left: 48px; - } - &-content { + &-section { display: block; text-align: center; margin-top: 8px; diff --git a/assets/nav.less b/assets/nav.less index d7fc9f4c..b5a257cb 100644 --- a/assets/nav.less +++ b/assets/nav.less @@ -13,11 +13,6 @@ text-align: center; overflow: visible; - &-container { - text-align: left; - padding-bottom: 8px; - outline: none; - } &-title { max-width: @stepNavContentMaxWidth; @@ -53,11 +48,5 @@ margin-left: -8px; } - &-active { - .@{stepsPrefixClass}-item-container { - padding-bottom: 5px; - border-bottom: 3px solid @primary-color; - } - } } } diff --git a/assets/progress-dot.less b/assets/progress-dot.less index f4a6a7b1..1c3dc208 100644 --- a/assets/progress-dot.less +++ b/assets/progress-dot.less @@ -2,16 +2,6 @@ .@{stepsPrefixClass}-dot { .@{stepsPrefixClass}-item { - &-tail { - width: 100%; - top: 1px; - margin: 0 0 0 @stepDescriptionMaxWidth / 2; - padding: 0; - - &:after { - height: 3px; - } - } &-icon { padding-right: 0; width: 5px; diff --git a/assets/small.less b/assets/small.less index d5b46617..95e281f3 100644 --- a/assets/small.less +++ b/assets/small.less @@ -16,7 +16,7 @@ top: -1px; } } - .@{stepsPrefixClass}-item-content { + .@{stepsPrefixClass}-item-section { margin-top: 0; } .@{stepsPrefixClass}-item-title { @@ -29,15 +29,6 @@ font-size: 12px; color: #999; } - .@{stepsPrefixClass}-item-tail { - top: 8px; - padding: 0 8px; - &:after { - height: 1px; - border-radius: 1px; - width: 100%; - } - } .@{stepsPrefixClass}-item-custom .@{stepsPrefixClass}-item-icon { width: inherit; diff --git a/assets/vertical.less b/assets/vertical.less index 6d669b8c..0c2e3ec1 100644 --- a/assets/vertical.less +++ b/assets/vertical.less @@ -11,7 +11,7 @@ margin-right: 16px; } } - &-content { + &-section { min-height: 48px; overflow: hidden; display: block; @@ -25,27 +25,9 @@ &-description { padding-bottom: 12px; } - &-tail { - position: absolute; - left: 13px; - top: 0; - height: 100%; - width: 1px; - padding: 30px 0 4px 0; - &:after { - height: 100%; - width: 1px; - } - } } &.@{stepsPrefixClass}-small { - .@{stepsPrefixClass}-item-tail { - position: absolute; - left: 9px; - top: 0; - padding: 22px 0 4px 0; - } .@{stepsPrefixClass}-item-title { line-height: 18px; } diff --git a/package.json b/package.json index 07e9f09b..d5fb883b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rc-component/steps", - "version": "0.0.0", + "version": "0.0.0-alpha.5", "description": "steps ui component for react", "keywords": [ "react", @@ -80,8 +80,8 @@ "querystring": "^0.2.0", "rc-dialog": "8.x", "rc-test": "^7.0.9", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", "typescript": "^5.0.0" }, "peerDependencies": { diff --git a/src/Rail.tsx b/src/Rail.tsx new file mode 100644 index 00000000..85503c33 --- /dev/null +++ b/src/Rail.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import cls from 'classnames'; +import type { Status, StepsProps } from './Steps'; + +export interface RailProps { + prefixCls: string; + classNames: StepsProps['classNames']; + styles: StepsProps['styles']; + status: Status; +} + +export default function Rail(props: RailProps) { + const { prefixCls, classNames, styles, status } = props; + const railCls = `${prefixCls}-rail`; + + // ============================= render ============================= + return ( +
+ ); +} diff --git a/src/Step.tsx b/src/Step.tsx index 02cc9d92..966ce6df 100644 --- a/src/Step.tsx +++ b/src/Step.tsx @@ -1,66 +1,93 @@ /* eslint react/prop-types: 0 */ import * as React from 'react'; -import classNames from 'classnames'; +import cls from 'classnames'; import KeyCode from '@rc-component/util/lib/KeyCode'; -import type { Status, Icons } from './interface'; -import type { StepIconRender, ProgressDotRender } from './Steps'; - -function isString(str: any): str is string { - return typeof str === 'string'; -} +import type { Status, StepItem, StepsProps } from './Steps'; +import Rail from './Rail'; export interface StepProps { + // style prefixCls?: string; - className?: string; - style?: React.CSSProperties; - wrapperStyle?: React.CSSProperties; - iconPrefix?: string; + classNames: StepsProps['classNames']; + styles: StepsProps['styles']; + + // data + data: StepItem; + nextStatus?: Status; active?: boolean; - disabled?: boolean; - stepIndex?: number; - stepNumber?: number; - status?: Status; - title?: React.ReactNode; - subTitle?: React.ReactNode; - description?: React.ReactNode; - tailContent?: React.ReactNode; + index: number; + last: boolean; + + // stepIndex?: number; + // stepNumber?: number; + // title?: React.ReactNode; + // subTitle?: React.ReactNode; + // description?: React.ReactNode; + + // render + iconRender?: StepsProps['iconRender']; icon?: React.ReactNode; - icons?: Icons; - onClick?: React.MouseEventHandler; - onStepClick?: (index: number) => void; - progressDot?: ProgressDotRender | boolean; - stepIcon?: StepIconRender; - render?: (stepItem: React.ReactElement) => React.ReactNode; + itemRender?: StepsProps['itemRender']; + itemWrapperRender?: StepsProps['itemWrapperRender']; + + // Event + onClick: (index: number) => void; } -const Step: React.FC = (props) => { +export default function Step(props: StepProps) { const { - className, + // style prefixCls, - style, + classNames, + styles, + + // data + data, + last, + nextStatus, active, - status, - iconPrefix, - icon, - wrapperStyle, - stepNumber, - disabled, - description, - title, - subTitle, - progressDot, - stepIcon, - tailContent, - icons, - stepIndex, - onStepClick, + index, + + // render + itemRender, + iconRender, + itemWrapperRender, + + // events onClick, - render, - ...restProps } = props; + const itemCls = `${prefixCls}-item`; + + // ========================== Data ========================== + const { + onClick: onItemClick, + title, + subTitle, + content, + description, + disabled, + icon, + status, + + className, + style, + ...restItemProps + } = data; + + const mergedContent = content ?? description; + + const renderInfo = { + item: { + ...data, + content: mergedContent, + }, + index, + active, + }; + // ========================= Click ========================== - const clickable = !!onStepClick && !disabled; + const clickable = !!(onClick || onItemClick) && !disabled; const accessibilityProps: { role?: string; @@ -68,120 +95,94 @@ const Step: React.FC = (props) => { onClick?: React.MouseEventHandler; onKeyDown?: React.KeyboardEventHandler; } = {}; + if (clickable) { accessibilityProps.role = 'button'; accessibilityProps.tabIndex = 0; accessibilityProps.onClick = (e) => { - onClick?.(e); - onStepClick(stepIndex); + onItemClick?.(e); + onClick(index); }; + accessibilityProps.onKeyDown = (e) => { const { which } = e; if (which === KeyCode.ENTER || which === KeyCode.SPACE) { - onStepClick(stepIndex); + onClick(index); } }; } // ========================= Render ========================= - const renderIconNode = () => { - let iconNode: React.ReactNode; - const iconClassName = classNames(`${prefixCls}-icon`, `${iconPrefix}icon`, { - [`${iconPrefix}icon-${icon}`]: icon && isString(icon), - [`${iconPrefix}icon-check`]: - !icon && status === 'finish' && ((icons && !icons.finish) || !icons), - [`${iconPrefix}icon-cross`]: - !icon && status === 'error' && ((icons && !icons.error) || !icons), - }); - const iconDot = ; - // `progressDot` enjoy the highest priority - if (progressDot) { - if (typeof progressDot === 'function') { - iconNode = ( - - {progressDot(iconDot, { - index: stepNumber - 1, - status, - title, - description, - })} - - ); - } else { - iconNode = {iconDot}; - } - } else if (icon && !isString(icon)) { - iconNode = {icon}; - } else if (icons && icons.finish && status === 'finish') { - iconNode = {icons.finish}; - } else if (icons && icons.error && status === 'error') { - iconNode = {icons.error}; - } else if (icon || status === 'finish' || status === 'error') { - iconNode = ; - } else { - iconNode = {stepNumber}; - } - - if (stepIcon) { - iconNode = stepIcon({ - index: stepNumber - 1, - status, - title, - description, - node: iconNode, - }); - } - - return iconNode; - }; - const mergedStatus = status || 'wait'; - const classString = classNames( - `${prefixCls}-item`, - `${prefixCls}-item-${mergedStatus}`, - className, + const classString = cls( + itemCls, + `${itemCls}-${mergedStatus}`, { - [`${prefixCls}-item-custom`]: icon, - [`${prefixCls}-item-active`]: active, - [`${prefixCls}-item-disabled`]: disabled === true, + [`${itemCls}-custom`]: icon, + [`${itemCls}-active`]: active, + [`${itemCls}-disabled`]: disabled === true, }, + className, + classNames.item, ); - const stepItemStyle: React.CSSProperties = { ...style }; - - let stepNode: React.ReactNode = ( -
-
-
{tailContent}
-
{renderIconNode()}
-
-
+ const wrapperNode = ( +
+
+ {iconRender?.(renderInfo)} +
+
+
+
{title} - {subTitle && ( -
- {subTitle} -
- )}
- {description &&
{description}
} + {subTitle && ( +
+ {subTitle} +
+ )} + + {!last && ( + + )}
+ {mergedContent && ( +
+ {mergedContent} +
+ )}
); - if (render) { - stepNode = (render(stepNode) || null) as React.ReactElement; + let stepNode: React.ReactNode = ( +
+ {itemWrapperRender ? itemWrapperRender(wrapperNode) : wrapperNode} +
+ ); + + if (itemRender) { + stepNode = (itemRender(stepNode, renderInfo) || null) as React.ReactElement; } return stepNode; -}; - -if (process.env.NODE_ENV !== 'production') { - Step.displayName = 'rc-step'; } - -export default Step; diff --git a/src/Steps.tsx b/src/Steps.tsx index 41f6bc66..756f55ef 100644 --- a/src/Steps.tsx +++ b/src/Steps.tsx @@ -1,153 +1,192 @@ /* eslint react/no-did-mount-set-state: 0, react/prop-types: 0 */ -import classNames from 'classnames'; +import cls from 'classnames'; import React from 'react'; -import type { Icons, Status } from './interface'; -import type { StepProps } from './Step'; import Step from './Step'; +export type Status = 'error' | 'process' | 'finish' | 'wait'; + +export type SemanticName = + | 'root' + | 'item' + | 'itemWrapper' + | 'itemHeader' + | 'itemTitle' + | 'itemSubtitle' + | 'itemSection' + | 'itemContent' + | 'itemIcon' + | 'itemRail'; + +export type StepItem = { + /** @deprecated Please use `content` instead. */ + description?: React.ReactNode; + content?: React.ReactNode; + disabled?: boolean; + icon?: React.ReactNode; + status?: Status; + subTitle?: React.ReactNode; + title?: React.ReactNode; +} & Pick, 'onClick' | 'className' | 'style'>; + export type StepIconRender = (info: { index: number; status: Status; title: React.ReactNode; + // @deprecated Please use `content` instead. description: React.ReactNode; + content: React.ReactNode; node: React.ReactNode; }) => React.ReactNode; -export type ProgressDotRender = ( - iconDot: React.ReactNode, - info: { - index: number; - status: Status; - title: React.ReactNode; - description: React.ReactNode; - }, -) => React.ReactNode; +export type RenderInfo = { + index: number; + active: boolean; + item: StepItem; +}; export interface StepsProps { + // style prefixCls?: string; style?: React.CSSProperties; className?: string; - children?: React.ReactNode; - direction?: 'horizontal' | 'vertical'; - type?: 'default' | 'navigation' | 'inline'; + classNames?: Partial>; + styles?: Partial>; + rootClassName?: string; + + // layout + orientation?: 'horizontal' | 'vertical'; labelPlacement?: 'horizontal' | 'vertical'; - iconPrefix?: string; + + // data status?: Status; - size?: 'default' | 'small'; current?: number; - progressDot?: ProgressDotRender | boolean; - stepIcon?: StepIconRender; initial?: number; - icons?: Icons; - items?: StepProps[]; - itemRender?: (item: StepProps, stepItem: React.ReactElement) => React.ReactNode; + items?: StepItem[]; onChange?: (current: number) => void; + + // render + iconRender?: (info: RenderInfo) => React.ReactNode; + itemRender?: (originNode: React.ReactElement, info: RenderInfo) => React.ReactNode; + itemWrapperRender?: (originNode: React.ReactElement) => React.ReactNode; } -const Steps: React.FC & { - Step: typeof Step; -} = (props) => { +export default function Steps(props: StepsProps) { const { + // style prefixCls = 'rc-steps', - style = {}, + style, className, - children, - direction = 'horizontal', - type = 'default', + classNames = {}, + styles = {}, + rootClassName, + + // layout + orientation = 'horizontal', labelPlacement = 'horizontal', - iconPrefix = 'rc', + + // data status = 'process', - size, current = 0, - progressDot = false, - stepIcon, initial = 0, - icons, onChange, + items, + + // render + iconRender, itemRender, - items = [], + itemWrapperRender, + ...restProps } = props; - const isNav = type === 'navigation'; - const isInline = type === 'inline'; - - // inline type requires fixed progressDot direction size. - const mergedProgressDot = isInline || progressDot; - const mergedDirection = isInline ? 'horizontal' : direction; - const mergedSize = isInline ? undefined : size; + // ============================= layout ============================= + const isVertical = orientation === 'vertical'; + const mergedOrientation = isVertical ? 'vertical' : 'horizontal'; + const mergeLabelPlacement = + !isVertical && labelPlacement === 'vertical' ? 'vertical' : 'horizontal'; + + // ============================= styles ============================= + const classString = cls( + prefixCls, + `${prefixCls}-${mergedOrientation}`, + `${prefixCls}-label-${mergeLabelPlacement}`, + rootClassName, + className, + classNames.root, + ); - const adjustedLabelPlacement = mergedProgressDot ? 'vertical' : labelPlacement; - const classString = classNames(prefixCls, `${prefixCls}-${mergedDirection}`, className, { - [`${prefixCls}-${mergedSize}`]: mergedSize, - [`${prefixCls}-label-${adjustedLabelPlacement}`]: mergedDirection === 'horizontal', - [`${prefixCls}-dot`]: !!mergedProgressDot, - [`${prefixCls}-navigation`]: isNav, - [`${prefixCls}-inline`]: isInline, - }); + // ============================== Data ============================== + const mergedItems = React.useMemo(() => (items || []).filter(Boolean), [items]); + const statuses = React.useMemo( + () => + mergedItems.map(({ status: itemStatus }, index) => { + const stepNumber = initial + index; + + if (!itemStatus) { + if (stepNumber === current) { + return status; + } else if (stepNumber < current) { + return 'finish'; + } + return 'wait'; + } + + return itemStatus; + }), + [mergedItems, status, current, initial], + ); + // ============================= events ============================= const onStepClick = (next: number) => { if (onChange && current !== next) { onChange(next); } }; - const renderStep = (item: StepProps, index: number) => { - const mergedItem: StepProps = { ...item }; - const stepNumber = initial + index; - // fix tail color - if (status === 'error' && index === current - 1) { - mergedItem.className = `${prefixCls}-next-error`; - } - - if (!mergedItem.status) { - if (stepNumber === current) { - mergedItem.status = status; - } else if (stepNumber < current) { - mergedItem.status = 'finish'; - } else { - mergedItem.status = 'wait'; - } - } + // ============================= render ============================= + const renderStep = (item: StepItem, index: number) => { + const stepIndex = initial + index; - if (isInline) { - mergedItem.icon = undefined; - mergedItem.subTitle = undefined; - } + const itemStatus = statuses[index]; + const nextStatus = statuses[index + 1]; - if (!mergedItem.render && itemRender) { - mergedItem.render = (stepItem) => itemRender(mergedItem, stepItem); - } + const data = { + ...item, + status: itemStatus, + }; return ( ); }; return ( -
- {items.filter(Boolean).map(renderStep)} +
+ {mergedItems.map(renderStep)}
); -}; - -Steps.Step = Step; - -if (process.env.NODE_ENV !== 'production') { - Steps.displayName = 'rc-steps'; } - -export default Steps; diff --git a/src/index.ts b/src/index.ts index 738dae39..8c5e175c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ -import Steps from './Steps'; +import Steps, { type StepsProps } from './Steps'; import Step from './Step'; export { Step }; +export type { StepsProps }; export default Steps; diff --git a/src/interface.ts b/src/interface.ts index 54bce1e7..8b137891 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,6 +1 @@ -export type Status = 'error' | 'process' | 'finish' | 'wait'; -export interface Icons { - finish: React.ReactNode; - error: React.ReactNode; -} diff --git a/tests/__snapshots__/index.test.tsx.snap b/tests/__snapshots__/index.test.tsx.snap index b44aebb3..c7ac8a0f 100644 --- a/tests/__snapshots__/index.test.tsx.snap +++ b/tests/__snapshots__/index.test.tsx.snap @@ -8,27 +8,25 @@ exports[`Steps render renders correctly 1`] = ` class="rc-steps-item rc-steps-item-process rc-steps-item-active" >
-
- - 1 - -
+ />
- 已完成 +
+ 已完成 +
+
@@ -37,27 +35,25 @@ exports[`Steps render renders correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 2 - -
+ />
- 进行中 +
+ 进行中 +
+
@@ -66,27 +62,25 @@ exports[`Steps render renders correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 3 - -
+ />
- 待运行 +
+ 待运行 +
+
@@ -95,27 +89,22 @@ exports[`Steps render renders correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 4 - -
+ />
- 待运行 +
+ 待运行 +
@@ -131,25 +120,25 @@ exports[`Steps render renders current correctly 1`] = ` class="rc-steps-item rc-steps-item-finish" >
-
- -
+ />
- 已完成 +
+ 已完成 +
+
@@ -158,25 +147,25 @@ exports[`Steps render renders current correctly 1`] = ` class="rc-steps-item rc-steps-item-finish" >
-
- -
+ />
- 进行中 +
+ 进行中 +
+
@@ -185,27 +174,25 @@ exports[`Steps render renders current correctly 1`] = ` class="rc-steps-item rc-steps-item-process rc-steps-item-active" >
-
- - 3 - -
+ />
- 待运行 +
+ 待运行 +
+
@@ -214,27 +201,22 @@ exports[`Steps render renders current correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 4 - -
+ />
- 待运行 +
+ 待运行 +
@@ -250,27 +232,25 @@ exports[`Steps render renders labelPlacement correctly 1`] = ` class="rc-steps-item rc-steps-item-process rc-steps-item-active" >
-
- - 1 - -
+ />
- 已完成 +
+ 已完成 +
+
@@ -279,27 +259,25 @@ exports[`Steps render renders labelPlacement correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 2 - -
+ />
- 进行中 +
+ 进行中 +
+
@@ -308,27 +286,25 @@ exports[`Steps render renders labelPlacement correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 3 - -
+ />
- 待运行 +
+ 待运行 +
+
@@ -337,27 +313,22 @@ exports[`Steps render renders labelPlacement correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 4 - -
+ />
- 待运行 +
+ 待运行 +
@@ -367,35 +338,31 @@ exports[`Steps render renders labelPlacement correctly 1`] = ` exports[`Steps render renders progressDot correctly 1`] = `
-
- - - -
+ />
- 已完成 +
+ 已完成 +
+
@@ -404,29 +371,25 @@ exports[`Steps render renders progressDot correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - - -
+ />
- 进行中 +
+ 进行中 +
+
@@ -435,29 +398,25 @@ exports[`Steps render renders progressDot correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - - -
+ />
- 待运行 +
+ 待运行 +
+
@@ -466,29 +425,22 @@ exports[`Steps render renders progressDot correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - - -
+ />
- 待运行 +
+ 待运行 +
@@ -498,35 +450,31 @@ exports[`Steps render renders progressDot correctly 1`] = ` exports[`Steps render renders progressDot function correctly 1`] = `
-
- - - a - - -
+ />
- 已完成 +
+ 已完成 +
+
@@ -535,29 +483,25 @@ exports[`Steps render renders progressDot function correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - - a - - -
+ />
- 进行中 +
+ 进行中 +
+
@@ -566,29 +510,25 @@ exports[`Steps render renders progressDot function correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - - a - - -
+ />
- 待运行 +
+ 待运行 +
+
@@ -597,29 +537,22 @@ exports[`Steps render renders progressDot function correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - - a - - -
+ />
- 待运行 +
+ 待运行 +
@@ -635,52 +568,52 @@ exports[`Steps render renders status correctly 1`] = ` class="rc-steps-item rc-steps-item-finish" >
-
- -
+ />
- 已完成 +
+ 已完成 +
+
-
- -
+ />
- 进行中 +
+ 进行中 +
+
@@ -689,25 +622,25 @@ exports[`Steps render renders status correctly 1`] = ` class="rc-steps-item rc-steps-item-error rc-steps-item-active" >
-
- -
+ />
- 待运行 +
+ 待运行 +
+
@@ -716,27 +649,22 @@ exports[`Steps render renders status correctly 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 4 - -
+ />
- 待运行 +
+ 待运行 +
@@ -752,30 +680,28 @@ exports[`Steps render renders step with description 1`] = ` class="rc-steps-item rc-steps-item-process rc-steps-item-active" >
- - 1 - -
-
- 已完成 +
+ 已完成 +
+
xx
@@ -786,30 +712,28 @@ exports[`Steps render renders step with description 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 2 - -
+ />
- 进行中 +
+ 进行中 +
+
xx
@@ -820,30 +744,28 @@ exports[`Steps render renders step with description 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 3 - -
+ />
- 待运行 +
+ 待运行 +
+
xx
@@ -854,30 +776,25 @@ exports[`Steps render renders step with description 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 4 - -
+ />
- 待运行 +
+ 待运行 +
xx
@@ -895,30 +812,28 @@ exports[`Steps render renders step with description and status 1`] = ` class="rc-steps-item rc-steps-item-wait rc-steps-item-active" >
-
- - 1 - -
+ />
- 已完成 +
+ 已完成 +
+
xx
@@ -929,30 +844,28 @@ exports[`Steps render renders step with description and status 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- - 2 - -
+ />
- 进行中 +
+ 进行中 +
+
xx
@@ -963,30 +876,28 @@ exports[`Steps render renders step with description and status 1`] = ` class="rc-steps-item rc-steps-item-process" >
-
- - 3 - -
+ />
- 待运行 +
+ 待运行 +
+
xx
@@ -997,28 +908,25 @@ exports[`Steps render renders step with description and status 1`] = ` class="rc-steps-item rc-steps-item-finish" >
-
- -
+ />
- 待运行 +
+ 待运行 +
xx
@@ -1028,7 +936,7 @@ exports[`Steps render renders step with description and status 1`] = `
`; -exports[`Steps render renders step with tailContent 1`] = ` +exports[`Steps render renders stepIcon function correctly 1`] = `
@@ -1036,34 +944,25 @@ exports[`Steps render renders step with tailContent 1`] = ` class="rc-steps-item rc-steps-item-process rc-steps-item-active" >
-
- text -
- - 1 - -
+ />
- 已完成 -
-
- xx +
+ 已完成 +
+
@@ -1072,36 +971,25 @@ exports[`Steps render renders step with tailContent 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
-
- content -
-
- - 2 - -
+ />
- 进行中 -
-
- xx +
+ 进行中 +
+
@@ -1110,34 +998,25 @@ exports[`Steps render renders step with tailContent 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- 3 -
- - 3 - -
+ />
- 待运行 -
-
- xx +
+ 待运行 +
+
@@ -1146,34 +1025,22 @@ exports[`Steps render renders step with tailContent 1`] = ` class="rc-steps-item rc-steps-item-wait" >
-
- text -
- - 4 - -
+ />
- 待运行 -
-
- xx +
+ 待运行 +
@@ -1181,121 +1048,112 @@ exports[`Steps render renders step with tailContent 1`] = `
`; -exports[`Steps render renders step with type inline 1`] = ` +exports[`Steps render renders vertical correctly 1`] = `
-
- - - -
+ />
- Step 1 -
-
- This is a description. +
+ 已完成 +
+
-
- - - -
+ />
- Step 2 -
-
- This is a description. +
+ 进行中 +
+
- - + 待运行 +
+
- +
+
+
+
+
+
- Step 3 -
-
- This is a description. +
+ 待运行 +
@@ -1303,499 +1161,135 @@ exports[`Steps render renders step with type inline 1`] = `
`; -exports[`Steps render renders step with type navigation 1`] = ` +exports[`Steps render renders with falsy children 1`] = `
-
- -
+ />
- Step 1
- 剩余 00:00:05 超长隐藏 + 已完成
+
- This is a description. + xx
-
- - 2 - -
+ />
- Step 2 +
+ 进行中 +
+
+ 剩余 00:00:07 +
+
- This is a description. + xx
-
- - 3 - -
+ />
- Step 3 +
+ 待运行 +
+
- This is a description. -
-
-
-
-
-`; - -exports[`Steps render renders stepIcon function correctly 1`] = ` -
-
-
-
-
- - a - -
-
-
- 已完成 -
-
-
-
-
-
-
-
- - a - -
-
-
- 进行中 -
-
-
-
-
-
-
-
- - a - -
-
-
- 待运行 + xx
-
-
-
- - a - -
-
-
- 待运行 -
-
-
-
-
-`; - -exports[`Steps render renders vertical correctly 1`] = ` -
-
-
-
-
- - 1 - -
-
-
- 已完成 -
-
-
-
-
-
-
-
- - 2 - -
-
-
- 进行中 -
-
-
-
-
-
-
-
- - 3 - -
-
-
- 待运行 -
-
-
-
-
-
-
-
- - 4 - -
-
-
- 待运行 -
-
-
-
-
-`; - -exports[`Steps render renders with falsy children 1`] = ` -
-
-
- - 1 - -
-
-
- 已完成 -
-
- xx -
-
-
-
-
-
-
- - 2 - -
-
- 进行中
- 剩余 00:00:07 + 待运行
- xx -
-
-
-
-
-
-
-
- - 3 - -
-
-
- 待运行 -
-
- xx -
-
-
-
-
-
-
-
- -
-
-
- 待运行 -
-
xx
@@ -1805,138 +1299,6 @@ exports[`Steps render renders with falsy children 1`] = `
`; -exports[`Steps render should render svg finishIcon and errorIcon correctly 1`] = ` -
-
-
-
-
- - - - - -
-
-
- Finished -
-
- This is a description -
-
-
-
-
-
-
-
- - - - - - -
-
-
- In Process -
-
- This is a description -
-
-
-
-
-
-
-
- - 3 - -
-
-
- Waiting -
-
- This is a description -
-
-
-
-
-`; - exports[`Steps should render customIcon correctly 1`] = `
-
- - - -
+ />
- 步骤1 +
+ 步骤1 +
+
@@ -1976,25 +1334,25 @@ exports[`Steps should render customIcon correctly 1`] = ` class="rc-steps-item rc-steps-item-process rc-steps-item-custom rc-steps-item-active" >
-
- -
+ />
- 步骤2 +
+ 步骤2 +
+
@@ -2003,25 +1361,22 @@ exports[`Steps should render customIcon correctly 1`] = ` class="rc-steps-item rc-steps-item-wait rc-steps-item-custom" >
-
- -
+ />
- 步骤3 +
+ 步骤3 +
diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 413c859f..f0a2d034 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { render, fireEvent, createEvent } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import Steps from '../src'; describe('Steps', () => { describe('render', () => { - let description = 'xx'; + const description = 'xx'; + const setSteps = (props) => ( { ); expect(container.firstChild).toMatchSnapshot(); }); - - it('renders step with tailContent', () => { - const { container } = render( - content
, - }, - { - title: '待运行', - description, - tailContent: 3, - }, - { - title: '待运行', - description, - tailContent: 'text', - }, - ]} - />, - ); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('renders step with type navigation', () => { - description = 'This is a description.'; - const { container } = render( - {}} - items={[ - { - title: 'Step 1', - subTitle: '剩余 00:00:05 超长隐藏', - description, - }, - { - title: 'Step 2', - description, - }, - { - title: 'Step 3', - description, - disabled: true, - }, - ]} - />, - ); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('renders step with type inline', () => { - description = 'This is a description.'; - const { container } = render( - {}} - items={[ - { - title: 'Step 1', - description, - }, - { - title: 'Step 2', - description, - }, - { - title: 'Step 3', - description, - disabled: true, - }, - ]} - itemRender={(item, stepItem) => React.cloneElement(stepItem, { title: item.description })} - />, - ); - expect(container.firstChild).toMatchSnapshot(); - }); - - function getFinishIcon() { - const path = - 'M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.' + - '5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139' + - '.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5' + - '-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 ' + - '55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33' + - '.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99' + - '.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.' + - '7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 10' + - '1.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 ' + - '20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z'; - return ( - - - - ); - } - - function getErrorIcon() { - const path1 = - 'M512 0C229.2 0 0 229.2 0 512s229.2 512 512 512 512-229' + - '.2 512-512S794.8 0 512 0zm311.1 823.1c-40.4 40.4-87.5 72.2-139.9 9' + - '4.3C629 940.4 571.4 952 512 952s-117-11.6-171.2-34.5c-52.4-22.2-99' + - '.4-53.9-139.9-94.3-40.4-40.4-72.2-87.5-94.3-139.9C83.6 629 72 571.' + - '4 72 512s11.6-117 34.5-171.2c22.2-52.4 53.9-99.4 94.3-139.9 40.4-4' + - '0.4 87.5-72.2 139.9-94.3C395 83.6 452.6 72 512 72s117 11.6 171.2 3' + - '4.5c52.4 22.2 99.4 53.9 139.9 94.3 40.4 40.4 72.2 87.5 94.3 139.9C' + - '940.4 395 952 452.6 952 512s-11.6 117-34.5 171.2c-22.2 52.4-53.9 9' + - '9.5-94.4 139.9z'; - const path2 = - 'M640.3 765.5c-19.9 0-36-16.1-36-36 0-50.9-41.4-92.3-92' + - '.3-92.3s-92.3 41.4-92.3 92.3c0 19.9-16.1 36-36 36s-36-16.1-36-36c0' + - '-90.6 73.7-164.3 164.3-164.3s164.3 73.7 164.3 164.3c0 19.9-16.1 36' + - '-36 36zM194.2 382.4a60 60 0 1 0 120 0 60 60 0 1 0-120 0zM709.5 382' + - '.4a60 60 0 1 0 120 0 60 60 0 1 0-120 0z'; - return ( - - - - - ); - } - it('should render svg finishIcon and errorIcon correctly', () => { - const icons = { - finish: getFinishIcon(), - error: getErrorIcon(), - }; - const { container } = render( - , - ); - expect(container.firstChild).toMatchSnapshot(); - }); }); it('should render customIcon correctly', () => { @@ -380,7 +210,7 @@ describe('Steps', () => { ]} />, ); - const items = container.querySelectorAll('.rc-steps-item-container'); + const items = container.querySelectorAll('.rc-steps-item-wrapper'); fireEvent.click(items[1]); expect(onChange).toBeCalledWith(1); }); @@ -403,7 +233,7 @@ describe('Steps', () => { , ); - const step = container.querySelectorAll('.rc-steps-item-container')[1]; + const step = container.querySelectorAll('.rc-steps-item-wrapper')[1]; fireEvent.click(step); rerender(); expect(container.querySelectorAll('.rc-steps-item')[1].classList).toContain( @@ -435,7 +265,7 @@ describe('Steps', () => { />, ); - const btn = container.querySelectorAll('.rc-steps-item-container')[0]; + const btn = container.querySelectorAll('.rc-steps-item-wrapper')[0]; fireEvent.click(btn); expect(onClick).toHaveBeenCalled(); }); @@ -446,7 +276,7 @@ describe('Steps', () => { , ); - const items = container.querySelectorAll('.rc-steps-item-container'); + const items = container.querySelectorAll('.rc-steps-item-wrapper'); fireEvent.click(items[2]); expect(onChange).not.toBeCalled(); }); @@ -475,4 +305,43 @@ describe('Steps', () => { expect(onChange).toHaveBeenCalledWith(1); }); + + it('itemRender', () => { + const { container } = render( + { + return
{oriNode}
; + }} + />, + ); + + expect(container.querySelector('.bamboo')).toBeTruthy(); + expect(container.querySelectorAll('.bamboo')).toHaveLength(1); + expect(container.querySelector('.rc-steps-item')).toBeTruthy(); + }); + + it('itemWrapperRender', () => { + const { container } = render( + { + return
{oriNode}
; + }} + />, + ); + + expect(container.querySelector('.bamboo')).toBeTruthy(); + expect(container.querySelectorAll('.bamboo')).toHaveLength(1); + expect(container.querySelector('.rc-steps-item')).toBeTruthy(); + expect(container.querySelector('.rc-steps-item > .bamboo')).toBeTruthy(); + }); }); diff --git a/tests/semantic.test.tsx b/tests/semantic.test.tsx new file mode 100644 index 00000000..d84b7355 --- /dev/null +++ b/tests/semantic.test.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import Steps, { type StepsProps } from '../src'; +import type { SemanticName } from '../src/Steps'; + +describe('Steps.Semantic', () => { + const renderSteps = (props: Partial) => ( + ({ + title: `Step ${index + 1}`, + subTitle: `SubTitle ${index + 1}`, + description: `Description ${index + 1}`, + }))} + {...props} + /> + ); + + it('semantic structure', () => { + const classNames: Record = { + root: 'custom-root', + item: 'custom-item', + itemWrapper: 'custom-item-wrapper', + itemIcon: 'custom-item-icon', + itemSection: 'custom-item-section', + itemHeader: 'custom-item-header', + itemTitle: 'custom-item-title', + itemSubtitle: 'custom-item-subtitle', + itemContent: 'custom-item-content', + itemRail: 'custom-item-rail', + }; + + const classNamesTargets: Record = { + root: 'rc-steps', + item: 'rc-steps-item', + itemWrapper: 'rc-steps-item-wrapper', + itemIcon: 'rc-steps-item-icon', + itemSection: 'rc-steps-item-section', + itemHeader: 'rc-steps-item-header', + itemTitle: 'rc-steps-item-title', + itemSubtitle: 'rc-steps-item-subtitle', + itemContent: 'rc-steps-item-content', + itemRail: 'rc-steps-item-rail', + }; + + const styles: Record> = { + root: { color: 'red' }, + item: { color: 'blue' }, + itemWrapper: { color: 'green' }, + itemIcon: { color: 'yellow' }, + itemSection: { color: 'purple' }, + itemHeader: { color: 'orange' }, + itemTitle: { color: 'pink' }, + itemSubtitle: { color: 'cyan' }, + itemContent: { color: 'magenta' }, + itemRail: { color: 'lime' }, + }; + + const { container } = render( + renderSteps({ + classNames, + styles, + }), + ); + + Object.keys(classNames).forEach((key) => { + const className = classNames[key as SemanticName]; + const oriClassName = classNamesTargets[key as SemanticName]; + const style = styles[key as SemanticName]; + + const element = container.querySelector(`.${className}`); + expect(element).toBeTruthy(); + expect(element).toHaveClass(oriClassName); + expect(element).toHaveStyle(style); + }); + }); +});