Repository: react-component/steps
Branch: master
Commit: 674846852bbb
Files: 71
Total size: 101.5 KB
Directory structure:
gitextract_zrdxiemy/
├── .dumirc.ts
├── .editorconfig
├── .eslintrc.js
├── .fatherrc.ts
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .prettierrc
├── HISTORY.md
├── LICENSE.md
├── README.md
├── assets/
│ ├── custom-icon.less
│ ├── iconfont.less
│ ├── index.less
│ ├── inline.less
│ ├── label-placement.less
│ ├── nav.less
│ ├── progress-dot.less
│ ├── small.less
│ ├── variables.less
│ └── vertical.less
├── bunfig.toml
├── docs/
│ ├── demo/
│ │ ├── alternativeLabel.md
│ │ ├── composable.md
│ │ ├── custom-svg-icon.md
│ │ ├── customIcon.md
│ │ ├── dynamic.md
│ │ ├── errorStep.md
│ │ ├── inline.md
│ │ ├── nav-base.md
│ │ ├── nextStep.md
│ │ ├── progressDot.md
│ │ ├── simple.md
│ │ ├── smallSize.md
│ │ ├── stepIcon.md
│ │ ├── vertical.md
│ │ └── verticalSmall.md
│ ├── examples/
│ │ ├── alternativeLabel.jsx
│ │ ├── composable.jsx
│ │ ├── custom-svg-icon.jsx
│ │ ├── customIcon.jsx
│ │ ├── dynamic.jsx
│ │ ├── errorStep.jsx
│ │ ├── inline.jsx
│ │ ├── nav-base.jsx
│ │ ├── nextStep.css
│ │ ├── nextStep.jsx
│ │ ├── progressDot.jsx
│ │ ├── simple.jsx
│ │ ├── smallSize.jsx
│ │ ├── stepIcon.jsx
│ │ ├── vertical.jsx
│ │ └── verticalSmall.jsx
│ └── index.md
├── index.js
├── jest.config.js
├── package.json
├── script/
│ └── update-content.js
├── src/
│ ├── Context.ts
│ ├── Rail.tsx
│ ├── Step.tsx
│ ├── StepIcon.tsx
│ ├── Steps.tsx
│ ├── UnstableContext.ts
│ ├── index.ts
│ └── interface.ts
├── tests/
│ ├── __snapshots__/
│ │ └── index.test.tsx.snap
│ ├── index.test.tsx
│ ├── semantic.test.tsx
│ └── setup.js
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .dumirc.ts
================================================
import { defineConfig } from 'dumi';
import path from 'path';
export default defineConfig({
alias: {
'@rc-component/steps$': path.resolve('src'),
'@rc-component/steps/es': path.resolve('src'),
},
mfsu: false,
favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'],
themeConfig: {
name: 'Steps',
logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4',
},
});
================================================
FILE: .editorconfig
================================================
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*.{js,css}]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
================================================
FILE: .eslintrc.js
================================================
const base = require('@umijs/fabric/dist/eslint');
module.exports = {
...base,
rules: {
...base.rules,
'arrow-parens': 0,
'react/sort-comp': 0,
},
};
================================================
FILE: .fatherrc.ts
================================================
import { defineConfig } from 'father';
export default defineConfig({
plugins: ['@rc-component/father-plugin'],
});
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
time: "21:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: "@types/react-dom"
versions:
- 17.0.0
- 17.0.1
- 17.0.2
- dependency-name: "@types/react"
versions:
- 17.0.0
- 17.0.1
- 17.0.2
- 17.0.3
- dependency-name: less
versions:
- 4.1.0
================================================
FILE: .github/workflows/main.yml
================================================
name: ✅ test
on: [push, pull_request]
jobs:
test:
uses: react-component/rc-test/.github/workflows/test.yml@main
secrets: inherit
================================================
FILE: .gitignore
================================================
.iml
*.log
.idea/
.ipr
.iws
*~
~*
*.diff
*.patch
*.bak
.DS_Store
Thumbs.db
.project
.*proj
.svn/
*.swp
*.swo
*.pyc
*.pyo
.build
node_modules
.cache
dist
assets/**/*.css
build
lib
es
coverage
package-lock.json
pnpm-lock.yaml
yarn.lock
.doc
.umi
.npmrc
# dumi
.dumi/tmp
.dumi/tmp-test
.dumi/tmp-production
.env.local
bun.lockb
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"printWidth": 100
}
================================================
FILE: HISTORY.md
================================================
# History
----
## 3.6.0
- Remove babel-runtime and prop-types
- Fix icon missing #85
## 3.5.0
- Support `navigation` type & `disabled` prop.
## 3.4.0
- Support `onChange` event.
## 3.3.0
- Add `icons` prop for change preset icon.
## 3.2.0
- Add `initial` prop.
## 3.1.0
- Add `tailContent`.
## 3.0.0
- Rewrite from bottom.
## 2.5.1
* Support react@15.5
## 2.5.0
* Refactor for last tail style.
## 2.4.0
* Refactor for extra width of tail. https://github.com/ant-design/ant-design/issues/5083
## 2.3.0
* Add new step style of prop `progressDot`.
## 2.2.0
* `icon` can be React.Node now.
## 2.1.0
* Add `labelPlacement`, support vertial title and description
## 2.0.0
* Refactor for better layout
## 1.5
* add `status` property of `Steps`
## 1.4
* update react to 0.14
## 1.3
* add `current` property of `Steps`
## 1.2.3
* fix publish
## 1.2.2
* remove vertical `maxDescriptionWidth`
## 1.2.1
* fix vertical `maxDescriptionWidth`
## 1.2.0
* add vertical steps
## 1.1.4
* fix layout algorithm
## 1.1.3
* support `iconPrefix` property, default is `rc`
## 1.1.2
* fix bugs
## 1.1.1
* support `maxDescriptionWidth` property, default is 120
## 1.1.0
* support `prefixCls` property, default is `rc-steps`
================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)
Copyright (c) 2014-present yiminghe
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# @rc-component/steps
---
React steps component.
[![NPM version][npm-image]][npm-url]
[![build status][travis-image]][travis-url]
[![Test coverage][codecov-image]][codecov-url]
[![npm download][download-image]][download-url]
[![bundle size][bundlephobia-image]][bundlephobia-url]
[npm-image]: http://img.shields.io/npm/v/@rc-component/steps.svg?style=flat-square
[npm-url]: http://npmjs.org/package/@rc-component/steps
[travis-image]: https://img.shields.io/travis/react-component/steps.svg?style=flat-square
[travis-url]: https://travis-ci.org/react-component/steps
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/steps/master.svg?style=flat-square
[codecov-url]: https://codecov.io/gh/react-component/steps/branch/master
[download-image]: https://img.shields.io/npm/dm/@rc-component/steps.svg?style=flat-square
[download-url]: https://npmjs.org/package/@rc-component/steps
[bundlephobia-url]: https://bundlephobia.com/result?p=@rc-component/steps
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@rc-component/steps
## Usage
```bash
npm install @rc-component/steps
```
```jsx | pure
```
## Example
https://steps.vercel.app/
## API
| name |
type |
default |
description |
| type |
string |
default |
diretypetion of Steps, could be `default` `navigation` `inline` |
| direction |
string |
horizontal |
direction of Steps, enum: `horizontal` or `vertical` |
| current |
number |
0 |
index of current step |
| initial |
number |
0 |
index initial |
| size |
string |
|
size of Steps, could be `small` |
| titlePlacement |
string |
|
placement of step title, could be `vertical` |
| status |
string |
wait |
status of current Steps, could be `error` `process` `finish` `wait` |
| icons |
{ finish: ReactNode, error: ReactNode } |
|
specify the default finish icon and error icon |
| itemRender |
(item: StepProps, stepItem: React.ReactNode) => React.ReactNode |
|
custom step item renderer |
| onChange |
(current: number) => void |
|
Trigger when Step changed |
### Steps.Step
| name |
type |
default |
description |
| title |
ReactNode |
|
title of step item |
| subTitle |
ReactNode |
|
subTitle of step item |
| description |
ReactNode |
|
description of step item |
| icon |
ReactNode |
|
set icon of step item |
| status |
string |
|
status of current Steps, could be `error` `process` `finish` `wait` |
| tailContent |
ReactNode |
|
content above tail |
| disabled |
bool |
false |
disabled step when onChange exist |
| render |
(stepItem: React.ReactNode) => React.ReactNode |
|
custom step item renderer |
## Development
```bash
npm install
npm start
```
## License
@rc-component/steps is released under the MIT license.
================================================
FILE: assets/custom-icon.less
================================================
@import 'variables';
.@{stepsPrefixClass}-item-custom {
.@{stepsPrefixClass}-item-icon {
background: none;
border: 0;
width: auto;
height: auto;
> .@{stepsPrefixClass}-icon {
font-size: 20px;
top: 1px;
width: 20px;
height: 20px;
}
}
&.@{stepsPrefixClass}-item-process {
.@{stepsPrefixClass}-item-icon > .@{stepsPrefixClass}-icon {
color: @process-icon-color;
}
}
}
================================================
FILE: assets/iconfont.less
================================================
@icon-url : "//at.alicdn.com/t/font_1434092639_4910953";
.ie-rotate(@rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation);
}
.rotate(@degrees) {
-webkit-transform: rotate(@degrees);
-ms-transform: rotate(@degrees); // IE9 only
-o-transform: rotate(@degrees);
transform: rotate(@degrees);
}
.animation(@animation) {
-webkit-animation: @animation;
-o-animation: @animation;
animation: @animation;
}
// font-face
// @icon-url: 字体源文件的地址
@font-face {
font-family: 'anticon';
src: url('@{icon-url}.eot'); /* IE9*/
src: url('@{icon-url}.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('@{icon-url}.woff') format('woff'), /* chrome、firefox */
url('@{icon-url}.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
url('@{icon-url}.svg#iconfont') format('svg'); /* iOS 4.1- */
}
.rcicon {
position: relative;
display: inline-block;
font-style: normal;
vertical-align: baseline;
text-align: center;
text-transform: none;
text-rendering: auto;
// 更好地渲染字体
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0px;
-moz-osx-font-smoothing: grayscale;
&:before {
display: block;
font-family: "anticon" !important;
}
}
// 方向性图标
.rcicon-step-backward:before {content:"\e662";}
.rcicon-step-forward {.ie-rotate(2);}
.rcicon-step-forward:before {content:"\e662";.rotate(180deg);}
.rcicon-fast-backward:before {content:"\e62a";}
.rcicon-fast-forward {.ie-rotate(2);}
.rcicon-fast-forward:before {content:"\e62a";.rotate(180deg);}
.rcicon-shrink:before {content:"\e65f";}
.rcicon-arrow-salt:before {content:"\e608";}
.rcicon-caret-down:before {content:"\e60f";}
.rcicon-caret-left {.ie-rotate(1);}
.rcicon-caret-left:before {content:"\e60f";.rotate(90deg);}
.rcicon-caret-up {.ie-rotate(2);}
.rcicon-caret-up:before {content:"\e60f";.rotate(180deg);}
.rcicon-caret-right {.ie-rotate(3);}
.rcicon-caret-right:before {content:"\e60f";.rotate(270deg);}
.rcicon-caret-circle-right:before {content:"\e60d";}
.rcicon-caret-circle-left {.ie-rotate(2);}
.rcicon-caret-circle-left:before {content:"\e60d";.rotate(180deg);}
.rcicon-caret-circle-o-right:before {content:"\e60e";}
.rcicon-caret-circle-o-left {.ie-rotate(2);}
.rcicon-caret-circle-o-left:before {content:"\e60e";.rotate(180deg);}
.rcicon-circle-right:before {content:"\e602";}
.rcicon-circle-left {.ie-rotate(2);}
.rcicon-circle-left:before {content:"\e602";.rotate(180deg);}
.rcicon-circle-o-right:before {content:"\e603";}
.rcicon-circle-o-left {.ie-rotate(2);}
.rcicon-circle-o-left:before {content:"\e603";.rotate(180deg);}
.rcicon-double-right:before {content:"\e604";}
.rcicon-double-left {.ie-rotate(2);}
.rcicon-double-left:before {content:"\e604";.rotate(180deg);}
.rcicon-verticle-right:before {content:"\e605";}
.rcicon-verticle-left {.ie-rotate(2);}
.rcicon-verticle-left:before {content:"\e605";.rotate(180deg);}
.rcicon-forward:before {content:"\e630";}
.rcicon-backward {.ie-rotate(2);}
.rcicon-backward:before {content:"\e630";.rotate(180deg);}
.rcicon-rollback:before {content:"\e65a";}
.rcicon-retweet:before {content:"\e659";}
.rcicon-right:before {content:"\e611";}
.rcicon-down {.ie-rotate(1);}
.rcicon-down:before {content:"\e611";.rotate(90deg);}
.rcicon-left {.ie-rotate(2);}
.rcicon-left:before {content:"\e611";.rotate(180deg);}
.rcicon-up {.ie-rotate(3);}
.rcicon-up:before {content:"\e611";.rotate(270deg);}
// 提示性图标
.rcicon-question:before {content:"\e655";}
.rcicon-question-circle:before {content:"\e656";}
.rcicon-question-circle-o:before {content:"\e657";}
.rcicon-plus:before {content:"\e651";}
.rcicon-plus-circle:before {content:"\e652";}
.rcicon-plus-circle-o:before {content:"\e653";}
.rcicon-pause:before {content:"\e64c";}
.rcicon-pause-circle:before {content:"\e64d";}
.rcicon-pause-circle-o:before {content:"\e64e";}
.rcicon-minus:before {content:"\e646";}
.rcicon-minus-circle:before {content:"\e647";}
.rcicon-minus-circle-o:before {content:"\e648";}
.rcicon-info-circle:before {content:"\e637";}
.rcicon-info-circle-o:before {content:"\e638";}
.rcicon-info:before {content:"\e63a";}
.rcicon-exclamation:before {content:"\e627";}
.rcicon-exclamation-circle:before {content:"\e628";}
.rcicon-exclamation-circle-o:before {content:"\e629";}
.rcicon-cross:before {content:"\e61e";}
.rcicon-cross-circle:before {content:"\e61f";}
.rcicon-cross-circle-o:before {content:"\e620";}
.rcicon-check:before {content:"\e613";}
.rcicon-check-circle:before {content:"\e614";}
.rcicon-check-circle-o:before {content:"\e615";}
.rcicon-clock-circle:before {content:"\e616";}
.rcicon-clock-circle-o:before {content:"\e617";}
// 网站通用图标
.rcicon-lock:before {content:"\e641";}
.rcicon-android:before {content:"\e601";}
.rcicon-apple:before {content:"\e606";}
.rcicon-area-chart:before {content:"\e607";}
.rcicon-bar-chart:before {content:"\e609";}
.rcicon-bars:before {content:"\e60a";}
.rcicon-book:before {content:"\e60b";}
.rcicon-calendar:before {content:"\e60c";}
.rcicon-cloud:before {content:"\e618";}
.rcicon-cloud-download:before {content:"\e619";}
.rcicon-code:before {content:"\e61a";}
.rcicon-copy:before {content:"\e61c";}
.rcicon-credit-card:before {content:"\e61d";}
.rcicon-delete:before {content:"\e621";}
.rcicon-desktop:before {content:"\e622";}
.rcicon-download-line:before {content:"\e623";}
.rcicon-edit:before {content:"\e624";}
.rcicon-ellipsis:before {content:"\e625";}
.rcicon-environment:before {content:"\e626";}
.rcicon-file:before {content:"\e62c";}
.rcicon-file-text:before {content:"\e62d";}
.rcicon-folder:before {content:"\e62e";}
.rcicon-folder-open:before {content:"\e62f";}
.rcicon-github:before {content:"\e631";}
.rcicon-hdd:before {content:"\e632";}
.rcicon-frown:before {content:"\e633";}
.rcicon-meh:before {content:"\e634";}
.rcicon-inbox:before {content:"\e635";}
.rcicon-laptop:before {content:"\e63d";}
.rcicon-large:before {content:"\e63e";}
.rcicon-line-chart:before {content:"\e63f";}
.rcicon-link:before {content:"\e640";}
.rcicon-logout:before {content:"\e642";}
.rcicon-mail:before {content:"\e643";}
.rcicon-menu-fold:before {content:"\e644";}
.rcicon-menu-unfold:before {content:"\e645";}
.rcicon-mobile:before {content:"\e649";}
.rcicon-notification:before {content:"\e64a";}
.rcicon-paper-clip:before {content:"\e64b";}
.rcicon-picture:before {content:"\e64f";}
.rcicon-pie-chart:before {content:"\e650";}
.rcicon-poweroff:before {content:"\e654";}
.rcicon-reload:before {content:"\e658";}
.rcicon-search:before {content:"\e65b";}
.rcicon-setting:before {content:"\e65c";}
.rcicon-share-alt:before {content:"\e65d";}
.rcicon-shopping-cart:before {content:"\e65e";}
.rcicon-smile:before {content:"\e661";}
.rcicon-tablet:before {content:"\e664";}
.rcicon-tag:before {content:"\e665";}
.rcicon-tags:before {content:"\e666";}
.rcicon-to-top:before {content:"\e667";}
.rcicon-unlock:before {content:"\e668";}
.rcicon-upload:before {content:"\e669";}
.rcicon-user:before {content:"\e66a";}
.rcicon-video-camera:before {content:"\e66b";}
.rcicon-windows:before {content:"\e66c";}
.rcicon-loading:before {
display: inline-block;
.animation(loadingCircle 1.0s infinite linear);
content:"\e610";
}
:root {
.rcicon-step-forward,
.rcicon-fast-forward,
.rcicon-left,
.rcicon-up,
.rcicon-down,
.rcicon-caret-left,
.rcicon-caret-up,
.rcicon-caret-right,
.rcicon-caret-circle-left,
.rcicon-caret-circle-o-left,
.rcicon-circle-left,
.rcicon-circle-o-left,
.rcicon-double-left,
.rcicon-verticle-left,
.rcicon-backward {
filter: none;
}
}
================================================
FILE: assets/index.less
================================================
@import 'variables';
.@{stepsPrefixClass} {
font-size: 0;
width: 100%;
line-height: 1.5;
display: flex;
&,
* {
box-sizing: border-box;
}
}
.@{stepsPrefixClass}-item {
position: relative;
display: inline-block;
vertical-align: top;
flex: 1;
// overflow: hidden;
&:last-child {
flex: none;
}
&:last-child &-title:after {
display: none;
}
&-icon,
&-section {
display: inline-block;
vertical-align: top;
}
&-icon {
flex: none;
border: 1px solid @wait-icon-color;
width: 26px;
height: 26px;
line-height: 26px;
text-align: center;
border-radius: 26px;
font-size: 14px;
transition:
background-color 0.3s,
border-color 0.3s;
> .@{stepsPrefixClass}-icon {
line-height: 1;
top: -1px;
color: @primary-color;
position: relative;
&.rcicon {
font-size: 12px;
position: relative;
top: -2px;
}
}
}
&-section {
margin-top: 3px;
}
&-title {
font-size: 14px;
color: #666;
font-weight: bold;
display: inline-block;
position: relative;
}
&-subtitle {
font-size: 12px;
display: inline-block;
color: #999;
}
&-description {
font-size: 12px;
color: #999;
}
.step-item-status(wait);
.step-item-status(process);
&-process &-icon {
background: @process-icon-color;
> .@{stepsPrefixClass}-icon {
color: #fff;
}
}
.step-item-status(finish);
.step-item-status(error);
&.@{stepsPrefixClass}-next-error .@{stepsPrefixClass}-item-title:after {
background: @error-icon-color;
}
}
.@{stepsPrefixClass}-horizontal:not(.@{stepsPrefixClass}-label-vertical) {
.@{stepsPrefixClass}-item {
&-description {
max-width: @stepDescriptionMaxWidth;
}
}
}
.step-item-status(@status) {
@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;
> .@{stepsPrefixClass}-icon {
color: @@icon-color;
.@{stepsPrefixClass}-icon-dot {
background: @@icon-color;
}
}
}
&-@{status} &-title {
color: @@title-color;
}
&-@{status} &-description {
color: @@description-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;
}
}
}
.@{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 {
}
================================================
FILE: assets/inline.less
================================================
@import 'variables';
.@{stepsPrefixClass}-inline {
width: auto;
display: inline-flex;
.@{stepsPrefixClass}-item {
flex: none;
&-icon {
width: 6px;
height: 6px;
margin-left: calc(50% - 3px);
> .@{stepsPrefixClass}-icon {
top: 0;
}
.@{stepsPrefixClass}-icon-dot {
border-radius: 3px;
}
}
&-section {
width: auto;
margin-top: 7px;
}
&-title {
color: rgba(0, 0, 0, 0.25);
font-size: 12px;
line-height: 20px;
font-weight: normal;
margin-bottom: 2px;
}
&-description {
display: none;
}
&-finish {
.@{stepsPrefixClass}-item-icon .@{stepsPrefixClass}-icon .@{stepsPrefixClass}-icon-dot {
background-color: @process-tail-color;
}
}
&-wait {
.@{stepsPrefixClass}-item-icon .@{stepsPrefixClass}-icon .@{stepsPrefixClass}-icon-dot {
background-color: #fff;
border: 1px solid @process-tail-color;
}
}
}
}
================================================
FILE: assets/label-placement.less
================================================
@import 'variables';
.@{stepsPrefixClass}-label-vertical {
.@{stepsPrefixClass}-item {
overflow: visible;
&-section {
display: block;
text-align: center;
margin-top: 8px;
width: @stepDescriptionMaxWidth;
}
&-icon {
display: inline-block;
margin-left: 36px;
}
&-title {
padding-right: 0;
&:after {
display: none;
}
}
&-description {
text-align: left;
}
}
}
================================================
FILE: assets/nav.less
================================================
@import 'variables';
.@{stepsPrefixClass}-navigation {
padding-top: 8px;
&.@{stepsPrefixClass}-horizontal {
.@{stepsPrefixClass}-item-description {
max-width: @stepNavContentMaxWidth;
}
}
.@{stepsPrefixClass}-item {
box-sizing: border-box;
text-align: center;
overflow: visible;
&-title {
max-width: @stepNavContentMaxWidth;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:after {
display: none;
}
}
&:last-child {
flex: 1;
&:after {
display: none;
}
}
&:after {
content: '';
display: inline-block;
width: 16px;
height: 16px;
border: 1px solid #ccc;
border-bottom: none;
border-left: none;
transform: rotate(45deg);
position: absolute;
top: 50%;
left: 100%;
margin-top: -12px;
margin-left: -8px;
}
}
}
================================================
FILE: assets/progress-dot.less
================================================
@import 'variables';
.@{stepsPrefixClass}-dot {
.@{stepsPrefixClass}-item {
&-icon {
padding-right: 0;
width: 5px;
height: 5px;
line-height: 5px;
border: 0;
margin-left: 48px;
.@{stepsPrefixClass}-icon-dot {
float: left;
width: 100%;
height: 100%;
border-radius: 2.5px;
}
}
&-process &-icon {
top: -1px;
width: 7px;
height: 7px;
line-height: 7px;
.@{stepsPrefixClass}-icon-dot {
border-radius: 3.5px;
}
}
}
}
================================================
FILE: assets/small.less
================================================
@import 'variables';
.@{stepsPrefixClass}-small {
.@{stepsPrefixClass}-item-icon {
width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
border-radius: 18px;
font-size: 12px;
margin-right: 10px;
> .@{stepsPrefixClass}-icon {
font-size: 12px;
font-size: ~"9px \9"; // ie8-9
transform: scale(.75);
top: -1px;
}
}
.@{stepsPrefixClass}-item-section {
margin-top: 0;
}
.@{stepsPrefixClass}-item-title {
font-size: 12px;
margin-bottom: 4px;
color: #666;
font-weight: bold;
}
.@{stepsPrefixClass}-item-description {
font-size: 12px;
color: #999;
}
.@{stepsPrefixClass}-item-custom .@{stepsPrefixClass}-item-icon {
width: inherit;
height: inherit;
line-height: inherit;
border-radius: 0;
border: 0;
background: none;
> .@{stepsPrefixClass}-icon {
font-size: 20px;
top: -2.5px;
transform: none;
}
}
}
================================================
FILE: assets/variables.less
================================================
@stepsPrefixClass: ~"rc-steps";
@stepDescriptionMaxWidth: 100px;
@stepNavContentMaxWidth: 140px;
@primary-color: #108ee9;
@process-icon-color: @primary-color;
@process-title-color: rgba(0,0,0,.65);
@process-description-color: @process-title-color;
@process-tail-color: #e9e9e9;
@wait-icon-color: #ccc;
@wait-title-color: rgba(0,0,0,.43);
@wait-description-color: @wait-title-color;
@wait-tail-color: @process-tail-color;
@finish-icon-color: @process-icon-color;
@finish-title-color: @wait-title-color;
@finish-description-color: @finish-title-color;
@finish-tail-color: @process-icon-color;
@error-icon-color: #f50;
@error-title-color: @error-icon-color;
@error-description-color: @error-icon-color;
@error-tail-color: @process-tail-color;
================================================
FILE: assets/vertical.less
================================================
@import 'variables';
.@{stepsPrefixClass}-vertical {
display: block;
.@{stepsPrefixClass}-item {
display: block;
overflow: visible;
&-icon {
float: left;
&-inner {
margin-right: 16px;
}
}
&-section {
min-height: 48px;
overflow: hidden;
display: block;
}
&-title {
line-height: 26px;
&:after {
display: none;
}
}
&-description {
padding-bottom: 12px;
}
}
&.@{stepsPrefixClass}-small {
.@{stepsPrefixClass}-item-title {
line-height: 18px;
}
}
}
================================================
FILE: bunfig.toml
================================================
[install]
peer = false
================================================
FILE: docs/demo/alternativeLabel.md
================================================
---
title: alternativeLabel
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/composable.md
================================================
---
title: composable
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/custom-svg-icon.md
================================================
---
title: custom-svg-icon
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/customIcon.md
================================================
---
title: customIcon
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/dynamic.md
================================================
---
title: dynamic
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/errorStep.md
================================================
---
title: errorStep
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/inline.md
================================================
---
title: inline
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/nav-base.md
================================================
---
title: nav-base
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/nextStep.md
================================================
---
title: nextStep
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/progressDot.md
================================================
---
title: progressDot
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/simple.md
================================================
---
title: simple
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/smallSize.md
================================================
---
title: smallSize
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/stepIcon.md
================================================
---
title: stepIcon
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/vertical.md
================================================
---
title: vertical
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/demo/verticalSmall.md
================================================
---
title: verticalSmall
nav:
title: Demo
path: /demo
---
================================================
FILE: docs/examples/alternativeLabel.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
const description =
'这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊';
export default () => (
);
================================================
FILE: docs/examples/composable.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
const description =
'这里是多信息的描述啊描述啊描述啊描述啊哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶';
export default () => (
);
================================================
FILE: docs/examples/custom-svg-icon.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
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 (
);
}
const icons = {
finish: getFinishIcon(),
error: getErrorIcon(),
};
const description = 'This is a description';
export default () => (
);
================================================
FILE: docs/examples/customIcon.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
// eslint-disable-next-line react/prop-types
const Icon = ({ type }) => ;
export default () => (
},
{ title: '步骤2', icon: 'apple' },
{ title: '步骤1', icon: 'github' },
]}
/>
);
================================================
FILE: docs/examples/dynamic.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React, { useState } from 'react';
import Steps from '@rc-component/steps';
export default () => {
const [items, setItems] = useState([
{
title: '已完成',
description:
'这里是多信息的描述啊描述啊描述啊描述啊哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶',
},
{
title: '进行中',
description:
'这里是多信息的描述啊描述啊描述啊描述啊哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶',
},
{
title: '待运行',
description:
'这里是多信息的描述啊描述啊描述啊描述啊哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶',
},
{
title: '待运行',
description:
'这里是多信息的描述啊描述啊描述啊描述啊哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶',
},
]);
const addStep = () => {
const newSteps = [...items];
newSteps.push({
title: '待运行',
description: '新的节点',
});
setItems(newSteps);
};
return (
);
};
================================================
FILE: docs/examples/errorStep.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
const description =
'这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊';
export default () => (
);
================================================
FILE: docs/examples/inline.jsx
================================================
import '../../assets/index.less';
import React, { useState } from 'react';
import Steps from '@rc-component/steps';
export default () => {
const [current, setCurrent] = useState(0);
return (
<>
React.cloneElement(stepItem, { title: item.description })}
/>
>
);
};
================================================
FILE: docs/examples/nav-base.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React, { useState } from 'react';
import Steps from '@rc-component/steps';
export default () => {
const [current, setCurrent] = useState(0);
const onChange = (current) => {
// eslint-disable-next-line no-console
console.log('onChange:', current);
setCurrent(current);
};
const containerStyle = {
border: '1px solid rgb(235, 237, 240)',
marginBottom: 24,
};
const description = 'This is a description.';
return (
);
};
================================================
FILE: docs/examples/nextStep.css
================================================
.my-step-form {
width: 100%;
}
.my-step-form > div {
margin-bottom: 20px;
}
.my-step-container {
width: 100%;
}
================================================
FILE: docs/examples/nextStep.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import './nextStep.css';
import React from 'react';
import Steps from '@rc-component/steps';
function generateRandomSteps() {
const n = Math.floor(Math.random() * 3) + 3;
const arr = [];
for (let i = 0; i < n; i++) {
arr.push({
title: `步骤${i + 1}`,
});
}
return arr;
}
const steps = generateRandomSteps();
class MyForm extends React.Component {
state = {
currentStep: Math.floor(Math.random() * steps.length),
};
nextStep = () => {
const { currentStep } = this.state;
let s = currentStep + 1;
if (s === steps.length) {
s = 0;
}
this.setState({
currentStep: s,
});
};
render() {
const { currentStep: cs } = this.state;
this.stepsRefs = [];
return (
);
}
}
export default MyForm;
================================================
FILE: docs/examples/progressDot.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
const description =
'这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊';
export default () => (
);
================================================
FILE: docs/examples/simple.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
const description =
'这里是多信息的描述啊描述啊描述啊描述啊哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶哦耶';
const ControlSteps = () => {
const [current, setCurrent] = React.useState(0);
return (
{
// eslint-disable-next-line no-console
console.log('Change:', val);
setCurrent(val);
}}
items={[
{
title: '已完成',
},
{
title: '进行中',
},
{
title: '待运行',
description: 'Hello World!',
},
{
title: '待运行',
},
]}
/>
);
};
export default () => (
);
================================================
FILE: docs/examples/smallSize.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
// eslint-disable-next-line react/prop-types
const Icon = ({ type }) => ;
export default () => (
,
},
{
title: '步骤3',
icon: 'apple',
},
{
title: '待运行',
icon: 'github',
},
]}
/>
);
================================================
FILE: docs/examples/stepIcon.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
function stepIcon({ status, node }) {
const isProcessing = status === 'process';
return isProcessing ? {node}
: node;
}
export default () => {
const [current, setCurrent] = React.useState(0);
return (
<>
>
);
};
================================================
FILE: docs/examples/vertical.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
const description =
'这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊';
export default () => (
);
================================================
FILE: docs/examples/verticalSmall.jsx
================================================
import '../../assets/index.less';
import '../../assets/iconfont.less';
import React from 'react';
import Steps from '@rc-component/steps';
const description =
'这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊这里是多信息的描述啊';
export default () => (
);
================================================
FILE: docs/index.md
================================================
---
hero:
title: "@rc-component/steps"
description: React Steps Component
---
================================================
FILE: index.js
================================================
'use strict';
module.exports = require('./src/');
================================================
FILE: jest.config.js
================================================
module.exports = {
setupFiles: ['./tests/setup.js'],
};
================================================
FILE: package.json
================================================
{
"name": "@rc-component/steps",
"version": "1.2.2",
"description": "steps ui component for react",
"keywords": [
"react",
"react-component",
"react-steps"
],
"homepage": "http://github.com/react-component/steps",
"bugs": {
"url": "http://github.com/react-component/steps/issues"
},
"repository": {
"type": "git",
"url": " git+ssh://git@github.com/react-component/steps.git"
},
"license": "MIT",
"maintainers": [
{
"name": "afc163",
"email": "afc163@gmail.com"
}
],
"main": "./lib/index",
"module": "./es/index",
"types": "./lib/index.d.ts",
"files": [
"assets/*.css",
"es",
"lib"
],
"scripts": {
"compile": "father build && lessc assets/index.less assets/index.css",
"coverage": "rc-test --coverage",
"docs:build": "dumi build",
"docs:deploy": "gh-pages -d .doc",
"gh-pages": "npm run docs:build && npm run docs:deploy",
"lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md",
"prepare": "husky install && dumi setup",
"prepublishOnly": "npm run compile && rc-np",
"prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"postpublish": "npm run gh-pages",
"start": "dumi dev",
"test": "rc-test",
"now-build": "npm run docs:build"
},
"lint-staged": {
"**/*.{js,jsx,tsx,ts,md,json}": [
"prettier --write"
]
},
"dependencies": {
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"devDependencies": {
"@rc-component/father-plugin": "^2.0.2",
"@rc-component/np": "^1.0.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.6",
"@types/jest": "^29.4.0",
"@types/node": "^24.5.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@umijs/fabric": "^4.0.1",
"dumi": "^2.0.0",
"eslint": "^8.55.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-unicorn": "^50.0.1",
"father": "^4",
"gh-pages": "^6.1.0",
"glob": "^10.0.0",
"husky": "^8.0.1",
"less": "^4.1.3",
"lint-staged": "^15.2.0",
"prettier": "^3.1.0",
"rc-test": "^7.0.9",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"typescript": "^5.0.0"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"engines": {
"node": ">=8.x"
}
}
================================================
FILE: script/update-content.js
================================================
/*
用于 dumi 改造使用,
可用于将 examples 的文件批量修改为 demo 引入形式,
其他项目根据具体情况使用。
*/
const fs = require('fs');
const glob = require('glob');
const paths = glob.sync('./docs/examples/*.jsx');
paths.forEach(path => {
const name = path.split('/').pop().split('.')[0];
fs.writeFile(
`./docs/demo/${name}.md`,
`---
title: ${name}
nav:
title: Demo
path: /demo
---
`,
'utf8',
function(error) {
if(error){
console.log(error);
return false;
}
console.log(`${name} 更新成功~`);
}
)
});
================================================
FILE: src/Context.ts
================================================
import * as React from 'react';
import type { ComponentType, StepsProps } from './Steps';
export interface StepsContextProps {
prefixCls: string;
classNames: NonNullable;
styles: NonNullable;
ItemComponent: ComponentType;
}
export const StepsContext = React.createContext(null!);
================================================
FILE: src/Rail.tsx
================================================
import * as React from 'react';
import { clsx } from 'clsx';
import type { Status } from './Steps';
export interface RailProps {
prefixCls: string;
className: string;
style: React.CSSProperties;
status: Status;
}
export default function Rail(props: RailProps) {
const { prefixCls, className, style, status } = props;
const railCls = `${prefixCls}-rail`;
// ============================= render =============================
return ;
}
================================================
FILE: src/Step.tsx
================================================
/* eslint react/prop-types: 0 */
import * as React from 'react';
import { clsx } from 'clsx';
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';
import { StepsContext } from './Context';
function hasContent(value: T) {
return value !== undefined && value !== null;
}
export interface StepProps {
// style
prefixCls?: string;
classNames: StepsProps['classNames'];
styles: StepsProps['styles'];
// data
data: StepItem;
nextStatus?: Status;
active?: boolean;
index: number;
last: boolean;
// render
iconRender?: StepsProps['iconRender'];
icon?: React.ReactNode;
itemRender?: StepsProps['itemRender'];
itemWrapperRender?: StepsProps['itemWrapperRender'];
// Event
onClick: (index: number) => void;
}
export default function Step(props: StepProps) {
const {
// style
prefixCls,
classNames,
styles,
// data
data,
last,
nextStatus,
active,
index,
// render
itemRender,
iconRender,
itemWrapperRender,
// events
onClick,
} = props;
const itemCls = `${prefixCls}-item`;
// ======================== Contexts ========================
const { railFollowPrevStatus } = React.useContext(UnstableContext);
const { ItemComponent } = React.useContext(StepsContext);
// ========================== Data ==========================
const {
onClick: onItemClick,
title,
subTitle,
content,
description,
disabled,
icon,
status,
className,
style,
classNames: itemClassNames = {},
styles: itemStyles = {},
...restItemProps
} = data;
const mergedContent = content ?? description;
const renderInfo = {
item: {
...data,
content: mergedContent,
},
index,
active,
};
// ========================= Click ==========================
const clickable = !!(onClick || onItemClick) && !disabled;
const accessibilityProps: {
role?: string;
tabIndex?: number;
onClick?: React.MouseEventHandler;
onKeyDown?: React.KeyboardEventHandler;
} = {};
if (clickable) {
accessibilityProps.role = 'button';
accessibilityProps.tabIndex = 0;
accessibilityProps.onClick = (e) => {
onItemClick?.(e);
onClick(index);
};
accessibilityProps.onKeyDown = (e) => {
const { which } = e;
if (which === KeyCode.ENTER || which === KeyCode.SPACE) {
onClick(index);
}
};
}
// ========================= Render =========================
const mergedStatus = status || 'wait';
const hasTitle = hasContent(title);
const hasSubTitle = hasContent(subTitle);
const classString = clsx(
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 = ;
if (iconRender) {
iconNode = iconRender(iconNode, {
...renderInfo,
components: {
Icon: StepIcon,
},
}) as React.ReactElement;
}
const wrapperNode = (
{/* Icon */}
{iconNode}
{hasTitle && (
{title}
)}
{hasSubTitle && (
{subTitle}
)}
{!last && (
)}
{hasContent(mergedContent) && (
{mergedContent}
)}
);
let stepNode: React.ReactNode = (
{itemWrapperRender ? itemWrapperRender(wrapperNode) : wrapperNode}
);
if (itemRender) {
stepNode = (itemRender(stepNode, renderInfo) || null) as React.ReactElement;
}
return stepNode;
}
================================================
FILE: src/StepIcon.tsx
================================================
import * as React from 'react';
import { clsx } from 'clsx';
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({});
export type StepIconProps = React.HTMLAttributes;
const StepIcon = React.forwardRef((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 (
{children}
);
});
export default StepIcon;
================================================
FILE: src/Steps.tsx
================================================
/* eslint react/no-did-mount-set-state: 0, react/prop-types: 0 */
import { clsx } from 'clsx';
import React from 'react';
import Step from './Step';
import { StepsContext, type StepsContextProps } from './Context';
import type StepIcon from './StepIcon';
export type Status = 'error' | 'process' | 'finish' | 'wait';
const EmptyObject = {};
export type SemanticName =
| 'root'
| 'item'
| 'itemWrapper'
| 'itemHeader'
| 'itemTitle'
| 'itemSubtitle'
| 'itemSection'
| 'itemContent'
| 'itemIcon'
| 'itemRail';
export type ItemSemanticName =
| 'root'
| 'wrapper'
| 'header'
| 'title'
| 'subtitle'
| 'section'
| 'content'
| 'icon'
| 'rail';
export type ComponentType = React.ComponentType | string;
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;
classNames?: Partial>;
styles?: Partial>;
} & 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 RenderInfo = {
index: number;
active: boolean;
item: StepItem;
};
export interface StepsProps {
// style
prefixCls?: string;
style?: React.CSSProperties;
className?: string;
classNames?: Partial>;
styles?: Partial>;
rootClassName?: string;
// layout
orientation?: 'horizontal' | 'vertical';
titlePlacement?: 'horizontal' | 'vertical';
// a11y
/** Internal usage of antd. Do not deps on this. */
components?: {
root?: ComponentType;
item?: ComponentType;
};
// data
status?: Status;
current?: number;
initial?: number;
items?: StepItem[];
onChange?: (current: number) => void;
// render
iconRender?: (
originNode: React.ReactElement,
info: RenderInfo & {
components: {
Icon: typeof StepIcon;
};
},
) => React.ReactNode;
itemRender?: (originNode: React.ReactElement, info: RenderInfo) => React.ReactNode;
itemWrapperRender?: (originNode: React.ReactElement) => React.ReactNode;
}
export default function Steps(props: StepsProps) {
const {
// style
prefixCls = 'rc-steps',
style,
className,
classNames = EmptyObject as NonNullable,
styles = EmptyObject as NonNullable,
rootClassName,
// layout
orientation,
titlePlacement,
components,
// data
status = 'process',
current = 0,
initial = 0,
onChange,
items,
// render
iconRender,
itemRender,
itemWrapperRender,
...restProps
} = props;
// ============================= layout =============================
const isVertical = orientation === 'vertical';
const mergedOrientation = isVertical ? 'vertical' : 'horizontal';
const mergeTitlePlacement =
!isVertical && titlePlacement === 'vertical' ? 'vertical' : 'horizontal';
// ============================= styles =============================
const classString = clsx(
prefixCls,
`${prefixCls}-${mergedOrientation}`,
`${prefixCls}-title-${mergeTitlePlacement}`,
rootClassName,
className,
classNames.root,
);
// ============================== 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);
}
};
// =========================== components ===========================
const { root: RootComponent = 'div', item: ItemComponent = 'div' } = components || {};
// ============================ contexts ============================
const stepIconContext = React.useMemo(
() => ({
prefixCls,
classNames,
styles,
ItemComponent,
}),
[prefixCls, classNames, styles, ItemComponent],
);
// ============================= render =============================
const renderStep = (item: StepItem, index: number) => {
const stepIndex = initial + index;
const itemStatus = statuses[index];
const nextStatus = statuses[index + 1];
const data = {
...item,
status: itemStatus,
};
return (
);
};
return (
{mergedItems.map(renderStep)}
);
}
================================================
FILE: src/UnstableContext.ts
================================================
import * as React from 'react';
export interface UnstableContextProps {
/**
* Used for Timeline component `reverse` prop.
* Safe to remove if refactor.
*/
railFollowPrevStatus?: boolean;
}
export const UnstableContext = React.createContext({});
================================================
FILE: src/index.ts
================================================
import Steps, { type StepsProps } from './Steps';
import Step from './Step';
export { Step };
export type { StepsProps };
export { UnstableContext } from './UnstableContext';
export default Steps;
================================================
FILE: src/interface.ts
================================================
================================================
FILE: tests/__snapshots__/index.test.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Steps components 1`] = `
-
`;
exports[`Steps render renders correctly 1`] = `
`;
exports[`Steps render renders current correctly 1`] = `
`;
exports[`Steps render renders progressDot correctly 1`] = `
`;
exports[`Steps render renders progressDot function correctly 1`] = `
`;
exports[`Steps render renders status correctly 1`] = `
`;
exports[`Steps render renders step with description 1`] = `
`;
exports[`Steps render renders step with description and status 1`] = `
`;
exports[`Steps render renders stepIcon function correctly 1`] = `
`;
exports[`Steps render renders titlePlacement correctly 1`] = `
`;
exports[`Steps render renders vertical correctly 1`] = `
`;
exports[`Steps render renders with falsy children 1`] = `
`;
exports[`Steps should render customIcon correctly 1`] = `
`;
================================================
FILE: tests/index.test.tsx
================================================
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Steps from '../src';
describe('Steps', () => {
describe('render', () => {
const description = 'xx';
const setSteps = (props) => (
);
it('renders correctly', () => {
const { container } = render(setSteps({}));
expect(container.firstChild).toMatchSnapshot();
});
it('renders without items', () => {
expect(() => {
render(setSteps({ items: undefined }));
}).not.toThrow();
});
it('renders current correctly', () => {
const { container } = render(setSteps({ current: 2 }));
expect(container.firstChild).toMatchSnapshot();
});
it('renders status correctly', () => {
const { container } = render(setSteps({ current: 2, status: 'error' }));
expect(container.firstChild).toMatchSnapshot();
});
it('renders vertical correctly', () => {
const { container } = render(setSteps({ direction: 'vertical' }));
expect(container.firstChild).toMatchSnapshot();
});
it('renders titlePlacement correctly', () => {
const { container } = render(setSteps({ titlePlacement: 'vertical' }));
expect(container.firstChild).toMatchSnapshot();
});
it('renders progressDot correctly', () => {
const { container } = render(setSteps({ progressDot: true }));
expect(container.firstChild).toMatchSnapshot();
});
it('renders progressDot function correctly', () => {
const { container } = render(setSteps({ progressDot: () => a }));
expect(container.firstChild).toMatchSnapshot();
});
it('renders stepIcon function correctly', () => {
const { container } = render(setSteps({ stepIcon: () => a }));
expect(container.firstChild).toMatchSnapshot();
});
it('renders step with description', () => {
const { container } = render(
,
);
expect(container.firstChild).toMatchSnapshot();
});
it('renders step with description and status', () => {
const { container } = render(
,
);
expect(container.firstChild).toMatchSnapshot();
});
it('renders with falsy children', () => {
const { container } = render(
,
);
expect(container.firstChild).toMatchSnapshot();
});
});
it('should render customIcon correctly', () => {
const Icon = ({ type }) => ;
const { container } = render(
,
},
{
title: '步骤2',
icon: 'apple',
},
{
title: '步骤3',
icon: 'github',
},
]}
/>,
);
expect(container.firstChild).toMatchSnapshot();
});
it('onChange', () => {
const onChange = jest.fn();
const { container } = render(
,
);
const items = container.querySelectorAll('.rc-steps-item');
fireEvent.click(items[1]);
expect(onChange).toHaveBeenCalledTimes(1);
});
it('items out of render function', () => {
const items = [
{
title: '已完成',
},
{
title: '进行中',
},
];
let current = 0;
const onChange = (val) => {
current = val;
};
const { container, rerender } = render(
,
);
const step = container.querySelectorAll('.rc-steps-item')[1];
fireEvent.click(step);
rerender();
expect(container.querySelectorAll('.rc-steps-item')[1].classList).toContain(
'rc-steps-item-process',
);
});
it('onClick', () => {
const onClick = jest.fn();
const onChange = jest.fn();
const { container } = render(
,
);
const btn = container.querySelectorAll('.rc-steps-item')[0];
fireEvent.click(btn);
expect(onClick).toHaveBeenCalled();
});
it('disabled', () => {
const onChange = jest.fn();
const { container } = render(
,
);
const items = container.querySelectorAll('.rc-steps-item');
fireEvent.click(items[2]);
expect(onChange).not.toBeCalled();
});
it('key board support', () => {
const onChange = jest.fn();
const { container } = render(
,
);
const button = container.querySelectorAll('[role="button"]')[1];
fireEvent.keyDown(button, { key: 'Enter', keyCode: 13, which: 13 });
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();
});
it('iconRender', () => {
const { container } = render(
{
return little;
}}
/>,
);
const iconEle = container.querySelector('.rc-steps-item-icon')!;
expect(iconEle).toHaveClass('bamboo');
expect(iconEle.textContent).toBe('little');
});
it('components', () => {
const { container } = render(
,
);
expect(container.firstChild).toMatchSnapshot();
});
});
================================================
FILE: tests/semantic.test.tsx
================================================
import React from 'react';
import { render } from '@testing-library/react';
import Steps, { type StepsProps } from '../src';
import type { ItemSemanticName, SemanticName } from '../src/Steps';
describe('Steps.Semantic', () => {
const renderSteps = (props: Partial) => (
({
title: `Step ${index + 1}`,
subTitle: `SubTitle ${index + 1}`,
content: `Content ${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);
});
});
it('item semantic structure', () => {
const classNames: Record = {
root: 'custom-root',
wrapper: 'custom-wrapper',
header: 'custom-header',
title: 'custom-title',
subtitle: 'custom-subtitle',
section: 'custom-section',
content: 'custom-content',
icon: 'custom-icon',
rail: 'custom-rail',
};
const classNamesTargets: Record = {
root: 'rc-steps-item',
wrapper: 'rc-steps-item-wrapper',
header: 'rc-steps-item-header',
title: 'rc-steps-item-title',
subtitle: 'rc-steps-item-subtitle',
section: 'rc-steps-item-section',
content: 'rc-steps-item-content',
icon: 'rc-steps-item-icon',
rail: 'rc-steps-item-rail',
};
const styles: Record> = {
root: { color: 'red' },
wrapper: { color: 'green' },
header: { color: 'orange' },
title: { color: 'pink' },
subtitle: { color: 'cyan' },
section: { color: 'purple' },
content: { color: 'magenta' },
icon: { color: 'yellow' },
rail: { color: 'lime' },
};
const { container } = render(
renderSteps({
items: Array.from({ length: 2 }, (_, index) => ({
title: `Title ${index + 1}`,
subTitle: `SubTitle ${index + 1}`,
content: `Content ${index + 1}`,
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);
});
});
});
================================================
FILE: tests/setup.js
================================================
global.requestAnimationFrame = global.requestAnimationFrame || function raf(cb) {
return setTimeout(cb, 0);
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"moduleResolution": "node",
"baseUrl": "./",
"jsx": "preserve",
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true,
"paths": {
"@/*": ["src/*"],
"@@/*": ["dumi/tmp/*"],
"@rc-component/steps": ["src/index.ts"]
}
}
}