@snack-uikit/stepper 0.2.19-preview-85c5f47b.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/LICENSE +201 -0
  3. package/README.md +55 -0
  4. package/dist/StepperContext/StepperContext.d.ts +4 -0
  5. package/dist/StepperContext/StepperContext.js +15 -0
  6. package/dist/StepperContext/index.d.ts +1 -0
  7. package/dist/StepperContext/index.js +1 -0
  8. package/dist/components/Stepper/Stepper.d.ts +29 -0
  9. package/dist/components/Stepper/Stepper.js +103 -0
  10. package/dist/components/Stepper/index.d.ts +1 -0
  11. package/dist/components/Stepper/index.js +1 -0
  12. package/dist/components/Stepper/styles.module.css +4 -0
  13. package/dist/components/index.d.ts +1 -0
  14. package/dist/components/index.js +1 -0
  15. package/dist/constants.d.ts +11 -0
  16. package/dist/constants.js +13 -0
  17. package/dist/helperComponents/Icon/Icon.d.ts +7 -0
  18. package/dist/helperComponents/Icon/Icon.js +24 -0
  19. package/dist/helperComponents/Icon/index.d.ts +1 -0
  20. package/dist/helperComponents/Icon/index.js +1 -0
  21. package/dist/helperComponents/Icon/styles.module.css +38 -0
  22. package/dist/helperComponents/Step/Step.d.ts +8 -0
  23. package/dist/helperComponents/Step/Step.js +26 -0
  24. package/dist/helperComponents/Step/index.d.ts +1 -0
  25. package/dist/helperComponents/Step/index.js +1 -0
  26. package/dist/helperComponents/Step/styles.module.css +65 -0
  27. package/dist/helperComponents/index.d.ts +2 -0
  28. package/dist/helperComponents/index.js +2 -0
  29. package/dist/hooks.d.ts +1 -0
  30. package/dist/hooks.js +3 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +2 -0
  33. package/dist/types.d.ts +18 -0
  34. package/dist/types.js +1 -0
  35. package/dist/utils.d.ts +1 -0
  36. package/dist/utils.js +1 -0
  37. package/package.json +43 -0
  38. package/src/StepperContext/StepperContext.ts +20 -0
  39. package/src/StepperContext/index.ts +1 -0
  40. package/src/components/Stepper/Stepper.tsx +167 -0
  41. package/src/components/Stepper/index.ts +1 -0
  42. package/src/components/Stepper/styles.module.scss +4 -0
  43. package/src/components/index.ts +1 -0
  44. package/src/constants.ts +12 -0
  45. package/src/helperComponents/Icon/Icon.tsx +38 -0
  46. package/src/helperComponents/Icon/index.ts +1 -0
  47. package/src/helperComponents/Icon/styles.module.scss +45 -0
  48. package/src/helperComponents/Step/Step.tsx +54 -0
  49. package/src/helperComponents/Step/index.ts +1 -0
  50. package/src/helperComponents/Step/styles.module.scss +76 -0
  51. package/src/helperComponents/index.ts +2 -0
  52. package/src/hooks.ts +5 -0
  53. package/src/index.ts +2 -0
  54. package/src/types.ts +21 -0
  55. package/src/utils.ts +2 -0
@@ -0,0 +1,103 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ import cn from 'classnames';
14
+ import { useCallback, useEffect, useMemo, useState } from 'react';
15
+ import { extractSupportProps } from '@snack-uikit/utils';
16
+ import { StepState, StepValidationResult } from '../../constants';
17
+ import { Step } from '../../helperComponents';
18
+ import { StepperContext } from '../../StepperContext';
19
+ import styles from './styles.module.css';
20
+ export function Stepper(_a) {
21
+ var { children, steps, className, onChangeCurrentStep, onCompleteChange, defaultCurrentStepIndex = 0 } = _a, props = __rest(_a, ["children", "steps", "className", "onChangeCurrentStep", "onCompleteChange", "defaultCurrentStepIndex"]);
22
+ const isCompletedByDefault = defaultCurrentStepIndex === steps.length - 1;
23
+ const [currentStepState, setCurrentStepState] = useState(isCompletedByDefault ? StepState.Completed : StepState.Current);
24
+ const [currentStepIndex, setCurrentStepIndexValue] = useState(defaultCurrentStepIndex);
25
+ const [isCompleted, setIsCompleted] = useState(isCompletedByDefault);
26
+ useEffect(() => {
27
+ onCompleteChange === null || onCompleteChange === void 0 ? void 0 : onCompleteChange(isCompleted);
28
+ }, [isCompleted, onCompleteChange]);
29
+ const setCurrentStepIndex = useCallback((newValue) => {
30
+ setCurrentStepIndexValue(prevValue => {
31
+ if (prevValue !== newValue) {
32
+ onChangeCurrentStep === null || onChangeCurrentStep === void 0 ? void 0 : onChangeCurrentStep(newValue, prevValue);
33
+ }
34
+ return newValue;
35
+ });
36
+ }, [onChangeCurrentStep]);
37
+ const goNext = useCallback(() => {
38
+ var _a, _b;
39
+ if (currentStepIndex >= steps.length || isCompleted) {
40
+ return;
41
+ }
42
+ const currentStep = steps[currentStepIndex];
43
+ const stepperValidPromise = (_b = (_a = currentStep.validation) === null || _a === void 0 ? void 0 : _a.call(currentStep)) !== null && _b !== void 0 ? _b : Promise.resolve(StepState.Completed);
44
+ setCurrentStepState(StepState.Loading);
45
+ stepperValidPromise
46
+ .then(validationResult => {
47
+ switch (validationResult) {
48
+ case StepValidationResult.Completed:
49
+ if (currentStepIndex === steps.length - 1) {
50
+ // завершился последний шаг
51
+ setCurrentStepState(StepState.Completed);
52
+ setIsCompleted(true);
53
+ }
54
+ else {
55
+ setCurrentStepIndex(currentStepIndex + 1);
56
+ setCurrentStepState(StepState.Current);
57
+ }
58
+ return;
59
+ case StepValidationResult.Rejected:
60
+ setCurrentStepState(StepState.Rejected);
61
+ return;
62
+ default:
63
+ setCurrentStepState(StepState.Current);
64
+ }
65
+ })
66
+ .catch(() => setCurrentStepState(StepState.Rejected));
67
+ }, [currentStepIndex, isCompleted, setCurrentStepIndex, steps]);
68
+ const goPrev = useCallback((index = currentStepIndex - 1) => {
69
+ if (currentStepIndex === 0 || index < 0 || index > currentStepIndex) {
70
+ return;
71
+ }
72
+ if (index === currentStepIndex && !isCompleted) {
73
+ return;
74
+ }
75
+ if (isCompleted) {
76
+ setIsCompleted(false);
77
+ }
78
+ setCurrentStepIndex(index);
79
+ setCurrentStepState(StepState.Current);
80
+ }, [currentStepIndex, isCompleted, setCurrentStepIndex]);
81
+ const stepsView = useMemo(() => steps.map((step, index) => {
82
+ const number = index + 1;
83
+ if (index < currentStepIndex) {
84
+ return Object.assign(Object.assign({}, step), { number, state: StepState.Completed, onClick: () => goPrev(index) });
85
+ }
86
+ if (index === currentStepIndex) {
87
+ return Object.assign(Object.assign({}, step), { number, state: currentStepState, onClick: isCompleted ? () => goPrev(index) : undefined });
88
+ }
89
+ if (index - 1 === currentStepIndex) {
90
+ return Object.assign(Object.assign({}, step), { number, state: StepState.Waiting, onClick: () => goNext() });
91
+ }
92
+ return Object.assign(Object.assign({}, step), { number, state: StepState.Waiting });
93
+ }), [steps, currentStepIndex, goPrev, currentStepState, isCompleted, goNext]);
94
+ const resetValidation = useCallback(() => {
95
+ if (currentStepState === StepState.Rejected) {
96
+ setCurrentStepState(StepState.Current);
97
+ }
98
+ }, [currentStepState]);
99
+ const stepper = (_jsx("div", Object.assign({ className: cn(styles.stepper, className) }, extractSupportProps(props), { children: stepsView.map((step, index, all) => (_jsx(Step, { step: step, isLast: all.length - 1 === index, "data-test-id": props['data-test-id'] }, step.title + index))) })));
100
+ const stepperApi = { goNext, goPrev, currentStepIndex, isCompleted, resetValidation, stepCount: steps.length };
101
+ return _jsx(StepperContext.Provider, Object.assign({ value: stepperApi }, { children: children(Object.assign({ stepper }, stepperApi)) }));
102
+ }
103
+ Stepper.validationResults = StepValidationResult;
@@ -0,0 +1 @@
1
+ export * from './Stepper';
@@ -0,0 +1 @@
1
+ export * from './Stepper';
@@ -0,0 +1,4 @@
1
+ .stepper{
2
+ display:flex;
3
+ flex-direction:row;
4
+ }
@@ -0,0 +1 @@
1
+ export * from './Stepper';
@@ -0,0 +1 @@
1
+ export * from './Stepper';
@@ -0,0 +1,11 @@
1
+ export declare enum StepState {
2
+ Completed = "completed",
3
+ Current = "current",
4
+ Loading = "loading",
5
+ Waiting = "waiting",
6
+ Rejected = "rejected"
7
+ }
8
+ export declare enum StepValidationResult {
9
+ Completed = "completed",
10
+ Rejected = "rejected"
11
+ }
@@ -0,0 +1,13 @@
1
+ export var StepState;
2
+ (function (StepState) {
3
+ StepState["Completed"] = "completed";
4
+ StepState["Current"] = "current";
5
+ StepState["Loading"] = "loading";
6
+ StepState["Waiting"] = "waiting";
7
+ StepState["Rejected"] = "rejected";
8
+ })(StepState || (StepState = {}));
9
+ export var StepValidationResult;
10
+ (function (StepValidationResult) {
11
+ StepValidationResult["Completed"] = "completed";
12
+ StepValidationResult["Rejected"] = "rejected";
13
+ })(StepValidationResult || (StepValidationResult = {}));
@@ -0,0 +1,7 @@
1
+ import { StepState } from '../../constants';
2
+ export type IconProps = {
3
+ state: StepState;
4
+ number: number;
5
+ className?: string;
6
+ };
7
+ export declare function Icon({ state, number, className }: IconProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import cn from 'classnames';
3
+ import { useMemo } from 'react';
4
+ import { CheckSVG, CrossSVG } from '@snack-uikit/icons';
5
+ import { Sun } from '@snack-uikit/loaders';
6
+ import { Typography } from '@snack-uikit/typography';
7
+ import { StepState } from '../../constants';
8
+ import styles from './styles.module.css';
9
+ function getContent(state, number) {
10
+ switch (state) {
11
+ case StepState.Completed:
12
+ return _jsx(CheckSVG, {});
13
+ case StepState.Rejected:
14
+ return _jsx(CrossSVG, {});
15
+ case StepState.Loading:
16
+ return _jsx(Sun, { size: Sun.sizes.S });
17
+ default:
18
+ return number;
19
+ }
20
+ }
21
+ export function Icon({ state, number, className }) {
22
+ const content = useMemo(() => getContent(state, number), [number, state]);
23
+ return (_jsx("div", Object.assign({ "data-state": state, className: cn(styles.icon, className) }, { children: typeof content === 'number' ? _jsx(Typography.SansLabelL, { children: content }) : content })));
24
+ }
@@ -0,0 +1 @@
1
+ export * from './Icon';
@@ -0,0 +1 @@
1
+ export * from './Icon';
@@ -0,0 +1,38 @@
1
+ .icon{
2
+ width:var(--size-stepper-step-status, 32px);
3
+ height:var(--size-stepper-step-status, 32px);
4
+ border-radius:var(--radius-stepper-step-status, 16px);
5
+ border-width:var(--border-width-stepper-step-status, 2px);
6
+ overflow:hidden;
7
+ display:inline-flex;
8
+ align-items:center;
9
+ justify-content:center;
10
+ }
11
+ .icon svg{
12
+ width:var(--dimension-3m, 24px) !important;
13
+ height:var(--dimension-3m, 24px) !important;
14
+ }
15
+ .icon[data-state=completed]{
16
+ color:var(--sys-primary-accent-default, #794ed3);
17
+ background-color:var(--sys-primary-decor-default, #ebdefd);
18
+ }
19
+ .icon[data-state=current]{
20
+ color:var(--sys-primary-on-accent, #fdfbff);
21
+ background-color:var(--sys-primary-accent-default, #794ed3);
22
+ }
23
+ .icon[data-state=waiting]{
24
+ box-sizing:border-box;
25
+ color:var(--sys-neutral-text-support, #565656);
26
+ border-color:var(--sys-neutral-decor-default, #dedede);
27
+ border-style:solid;
28
+ }
29
+ .icon[data-state=loading]{
30
+ box-sizing:border-box;
31
+ color:var(--sys-neutral-accent-default, #757575);
32
+ border-color:var(--sys-neutral-decor-default, #dedede);
33
+ border-style:solid;
34
+ }
35
+ .icon[data-state=rejected]{
36
+ color:var(--sys-red-on-accent, #fffbfa);
37
+ background-color:var(--sys-red-accent-default, #d93f2f);
38
+ }
@@ -0,0 +1,8 @@
1
+ import { WithSupportProps } from '@snack-uikit/utils';
2
+ import { StepViewData } from '../../types';
3
+ export type StepProps = WithSupportProps<{
4
+ step: StepViewData;
5
+ className?: string;
6
+ isLast: boolean;
7
+ }>;
8
+ export declare function Step({ step, className, isLast, 'data-test-id': testId, ...props }: StepProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,26 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import cn from 'classnames';
14
+ import { TruncateString } from '@snack-uikit/truncate-string';
15
+ import { Typography } from '@snack-uikit/typography';
16
+ import { extractSupportProps } from '@snack-uikit/utils';
17
+ import { StepState } from '../../constants';
18
+ import { getTestIdBuilder } from '../../utils';
19
+ import { Icon } from '../Icon';
20
+ import styles from './styles.module.css';
21
+ const getTailTestId = getTestIdBuilder('_element-tail');
22
+ const getStepTestId = getTestIdBuilder('_element-step');
23
+ export function Step(_a) {
24
+ var { step, className, isLast, 'data-test-id': testId } = _a, props = __rest(_a, ["step", "className", "isLast", 'data-test-id']);
25
+ return (_jsxs("div", Object.assign({ className: cn(styles.step, className), "data-state": step.state }, extractSupportProps(props), { children: [_jsxs("button", Object.assign({ className: styles.content, onClick: step.onClick, disabled: !step.onClick, "data-test-id": getStepTestId(testId), "data-state": step.state }, { children: [_jsx(Icon, Object.assign({}, step, { className: styles.icon })), _jsx(Typography, Object.assign({ className: styles.title, tag: Typography.tags.div, family: Typography.families.Sans, role: Typography.roles.Body, size: Typography.sizes.M }, { children: _jsx(TruncateString, { text: step.title }) }))] })), !isLast && (_jsx("div", Object.assign({ className: styles.tail, "data-completed": step.state === StepState.Completed || undefined, "data-test-id": getTailTestId(testId) }, { children: _jsx("div", { className: styles.tailLine }) })))] })));
26
+ }
@@ -0,0 +1 @@
1
+ export * from './Step';
@@ -0,0 +1 @@
1
+ export * from './Step';
@@ -0,0 +1,65 @@
1
+ .tailLine{
2
+ height:var(--size-stepper-step-track-line, 2px);
3
+ flex-grow:1;
4
+ background-color:var(--sys-neutral-decor-default, #dedede);
5
+ }
6
+
7
+ .tail{
8
+ height:var(--size-stepper-step-track-container, 32px);
9
+ padding-left:var(--space-stepper-step-track, 8px);
10
+ padding-right:var(--space-stepper-step-track, 8px);
11
+ display:flex;
12
+ flex-grow:1;
13
+ flex-shrink:1;
14
+ align-items:center;
15
+ min-width:var(--dimension-2m, 16px);
16
+ }
17
+ .tail[data-completed] .tailLine{
18
+ background-color:var(--sys-primary-accent-default, #794ed3);
19
+ }
20
+
21
+ .title{
22
+ overflow:hidden;
23
+ }
24
+
25
+ .step{
26
+ display:flex;
27
+ flex:1;
28
+ align-items:center;
29
+ justify-content:center;
30
+ min-width:0;
31
+ }
32
+ .step:last-child{
33
+ max-width:-moz-max-content;
34
+ max-width:max-content;
35
+ }
36
+
37
+ .content{
38
+ gap:var(--space-stepper-step-content-gap, 8px);
39
+ overflow:hidden;
40
+ display:flex;
41
+ flex-shrink:1;
42
+ align-items:center;
43
+ margin:0;
44
+ padding:0;
45
+ color:var(--sys-neutral-text-main, #333333);
46
+ background-color:transparent;
47
+ border:none;
48
+ }
49
+ .content:focus-visible{
50
+ outline-width:var(--border-state-focus-s-border-width, 2px);
51
+ outline-style:var(--border-state-focus-s-border-style, solid);
52
+ outline-color:var(--border-state-focus-s-border-color, );
53
+ outline-color:var(--sys-available-complementary, #131313);
54
+ outline-offset:var(--spacing-state-focus-offset, 2px);
55
+ }
56
+ .content:not([disabled]){
57
+ cursor:pointer;
58
+ }
59
+ .content:not([disabled]):focus-visible, .content:not([disabled]):hover{
60
+ color:var(--sys-neutral-text-light, #898989);
61
+ }
62
+
63
+ .icon{
64
+ flex-shrink:0;
65
+ }
@@ -0,0 +1,2 @@
1
+ export * from './Step';
2
+ export * from './Icon';
@@ -0,0 +1,2 @@
1
+ export * from './Step';
2
+ export * from './Icon';
@@ -0,0 +1 @@
1
+ export declare const useStepperApi: () => import("./types").StepperApi;
package/dist/hooks.js ADDED
@@ -0,0 +1,3 @@
1
+ import { useContext } from 'react';
2
+ import { StepperContext } from './StepperContext';
3
+ export const useStepperApi = () => useContext(StepperContext);
@@ -0,0 +1,2 @@
1
+ export * from './components';
2
+ export * from './hooks';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './components';
2
+ export * from './hooks';
@@ -0,0 +1,18 @@
1
+ import { StepState, StepValidationResult } from './constants';
2
+ export type StepData = {
3
+ title: string;
4
+ validation?(): Promise<StepValidationResult>;
5
+ };
6
+ export type StepViewData = StepData & {
7
+ onClick?(): void;
8
+ state: StepState;
9
+ number: number;
10
+ };
11
+ export type StepperApi = {
12
+ goNext(): void;
13
+ goPrev(): void;
14
+ resetValidation(): void;
15
+ isCompleted: boolean;
16
+ currentStepIndex: number;
17
+ stepCount: number;
18
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const getTestIdBuilder: (postfix?: string) => (testId?: string) => string | undefined;
package/dist/utils.js ADDED
@@ -0,0 +1 @@
1
+ export const getTestIdBuilder = (postfix) => (testId) => postfix && testId ? `${testId}${postfix}` : undefined;
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@snack-uikit/stepper",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "title": "Stepper",
7
+ "version": "0.2.19-preview-85c5f47b.0",
8
+ "sideEffects": [
9
+ "*.css",
10
+ "*.woff",
11
+ "*.woff2"
12
+ ],
13
+ "description": "",
14
+ "main": "./dist/index.js",
15
+ "module": "./dist/index.js",
16
+ "homepage": "https://github.com/cloud-ru-tech/snack-uikit/tree/master/packages/stepper",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/cloud-ru-tech/snack-uikit.git",
20
+ "directory": "packages/stepper"
21
+ },
22
+ "author": "Сергей Хлупин <svhlupin@cloud.ru>",
23
+ "contributors": [
24
+ "Сергей Хлупин <svhlupin@cloud.ru>"
25
+ ],
26
+ "files": [
27
+ "dist",
28
+ "src",
29
+ "./CHANGELOG.md",
30
+ "./LICENSE"
31
+ ],
32
+ "license": "Apache-2.0",
33
+ "scripts": {},
34
+ "dependencies": {
35
+ "@snack-uikit/icons": "0.18.2-preview-85c5f47b.0",
36
+ "@snack-uikit/loaders": "0.3.5-preview-85c5f47b.0",
37
+ "@snack-uikit/truncate-string": "0.2.17-preview-85c5f47b.0",
38
+ "@snack-uikit/typography": "0.4.5-preview-85c5f47b.0",
39
+ "@snack-uikit/utils": "2.0.2-preview-85c5f47b.0",
40
+ "classnames": "2.3.2"
41
+ },
42
+ "gitHead": "6a7eef41d780d945f64f791448eaa72b9e0ae72d"
43
+ }
@@ -0,0 +1,20 @@
1
+ import { createContext } from 'react';
2
+
3
+ import { StepperApi } from '../types';
4
+
5
+ export type StepperContextValue = StepperApi;
6
+
7
+ export const StepperContext = createContext<StepperApi>({
8
+ stepCount: 0,
9
+ isCompleted: false,
10
+ currentStepIndex: 0,
11
+ goNext() {
12
+ /* stub */
13
+ },
14
+ goPrev() {
15
+ /* stub */
16
+ },
17
+ resetValidation() {
18
+ /* stub */
19
+ },
20
+ });
@@ -0,0 +1 @@
1
+ export * from './StepperContext';
@@ -0,0 +1,167 @@
1
+ import cn from 'classnames';
2
+ import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
3
+
4
+ import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
5
+
6
+ import { StepState, StepValidationResult } from '../../constants';
7
+ import { Step } from '../../helperComponents';
8
+ import { StepperContext } from '../../StepperContext';
9
+ import { StepData, StepperApi, StepViewData } from '../../types';
10
+ import styles from './styles.module.scss';
11
+
12
+ export type StepperState = StepperApi & {
13
+ stepper: ReactElement;
14
+ };
15
+
16
+ export type StepperProps = WithSupportProps<{
17
+ /** Массив шагов */
18
+ steps: StepData[];
19
+ /** Индекс текущего шага по-дефолту */
20
+ defaultCurrentStepIndex: number;
21
+ /** CSS-класс */
22
+ className?: string;
23
+ /**
24
+ * Render function. Принимает аргументы: `stepper` - JSX-элемент степпера,
25
+ * `goNext` - перейти на след. шаг, `goPrev` - перейти на пред. шаг, `resetValidation` - сбросить состояние валидации для текущего шага, `isCompleted` - окончен ли процесс, `currentStepIndex` - индекс текущего шага, `stepCount` - кол-во шагов.
26
+ * @type ({stepper, ...api}) => ReactElement
27
+ */
28
+ children: (params: StepperState) => ReactElement;
29
+ /** Колбек смены текущего степа */
30
+ onChangeCurrentStep: (newValue: number, prevValue: number) => void;
31
+ /** Колбек изменения завершенности */
32
+ onCompleteChange: (isCompleted: boolean) => void;
33
+ }>;
34
+
35
+ export function Stepper({
36
+ children,
37
+ steps,
38
+ className,
39
+ onChangeCurrentStep,
40
+ onCompleteChange,
41
+ defaultCurrentStepIndex = 0,
42
+ ...props
43
+ }: StepperProps) {
44
+ const isCompletedByDefault = defaultCurrentStepIndex === steps.length - 1;
45
+ const [currentStepState, setCurrentStepState] = useState(
46
+ isCompletedByDefault ? StepState.Completed : StepState.Current,
47
+ );
48
+ const [currentStepIndex, setCurrentStepIndexValue] = useState(defaultCurrentStepIndex);
49
+ const [isCompleted, setIsCompleted] = useState(isCompletedByDefault);
50
+
51
+ useEffect(() => {
52
+ onCompleteChange?.(isCompleted);
53
+ }, [isCompleted, onCompleteChange]);
54
+
55
+ const setCurrentStepIndex = useCallback(
56
+ (newValue: number) => {
57
+ setCurrentStepIndexValue(prevValue => {
58
+ if (prevValue !== newValue) {
59
+ onChangeCurrentStep?.(newValue, prevValue);
60
+ }
61
+ return newValue;
62
+ });
63
+ },
64
+ [onChangeCurrentStep],
65
+ );
66
+
67
+ const goNext = useCallback(() => {
68
+ if (currentStepIndex >= steps.length || isCompleted) {
69
+ return;
70
+ }
71
+
72
+ const currentStep = steps[currentStepIndex];
73
+
74
+ const stepperValidPromise = currentStep.validation?.() ?? Promise.resolve(StepState.Completed);
75
+
76
+ setCurrentStepState(StepState.Loading);
77
+
78
+ stepperValidPromise
79
+ .then(validationResult => {
80
+ switch (validationResult) {
81
+ case StepValidationResult.Completed:
82
+ if (currentStepIndex === steps.length - 1) {
83
+ // завершился последний шаг
84
+ setCurrentStepState(StepState.Completed);
85
+ setIsCompleted(true);
86
+ } else {
87
+ setCurrentStepIndex(currentStepIndex + 1);
88
+ setCurrentStepState(StepState.Current);
89
+ }
90
+ return;
91
+ case StepValidationResult.Rejected:
92
+ setCurrentStepState(StepState.Rejected);
93
+ return;
94
+ default:
95
+ setCurrentStepState(StepState.Current);
96
+ }
97
+ })
98
+ .catch(() => setCurrentStepState(StepState.Rejected));
99
+ }, [currentStepIndex, isCompleted, setCurrentStepIndex, steps]);
100
+
101
+ const goPrev = useCallback(
102
+ (index: number = currentStepIndex - 1) => {
103
+ if (currentStepIndex === 0 || index < 0 || index > currentStepIndex) {
104
+ return;
105
+ }
106
+
107
+ if (index === currentStepIndex && !isCompleted) {
108
+ return;
109
+ }
110
+
111
+ if (isCompleted) {
112
+ setIsCompleted(false);
113
+ }
114
+
115
+ setCurrentStepIndex(index);
116
+ setCurrentStepState(StepState.Current);
117
+ },
118
+ [currentStepIndex, isCompleted, setCurrentStepIndex],
119
+ );
120
+
121
+ const stepsView: StepViewData[] = useMemo(
122
+ () =>
123
+ steps.map((step, index) => {
124
+ const number = index + 1;
125
+
126
+ if (index < currentStepIndex) {
127
+ return { ...step, number, state: StepState.Completed, onClick: () => goPrev(index) };
128
+ }
129
+
130
+ if (index === currentStepIndex) {
131
+ return { ...step, number, state: currentStepState, onClick: isCompleted ? () => goPrev(index) : undefined };
132
+ }
133
+
134
+ if (index - 1 === currentStepIndex) {
135
+ return { ...step, number, state: StepState.Waiting, onClick: () => goNext() };
136
+ }
137
+
138
+ return { ...step, number, state: StepState.Waiting };
139
+ }),
140
+ [steps, currentStepIndex, goPrev, currentStepState, isCompleted, goNext],
141
+ );
142
+
143
+ const resetValidation = useCallback(() => {
144
+ if (currentStepState === StepState.Rejected) {
145
+ setCurrentStepState(StepState.Current);
146
+ }
147
+ }, [currentStepState]);
148
+
149
+ const stepper = (
150
+ <div className={cn(styles.stepper, className)} {...extractSupportProps(props)}>
151
+ {stepsView.map((step, index, all) => (
152
+ <Step
153
+ key={step.title + index}
154
+ step={step}
155
+ isLast={all.length - 1 === index}
156
+ data-test-id={props['data-test-id']}
157
+ />
158
+ ))}
159
+ </div>
160
+ );
161
+
162
+ const stepperApi = { goNext, goPrev, currentStepIndex, isCompleted, resetValidation, stepCount: steps.length };
163
+
164
+ return <StepperContext.Provider value={stepperApi}>{children({ stepper, ...stepperApi })}</StepperContext.Provider>;
165
+ }
166
+
167
+ Stepper.validationResults = StepValidationResult;
@@ -0,0 +1 @@
1
+ export * from './Stepper';
@@ -0,0 +1,4 @@
1
+ .stepper {
2
+ display: flex;
3
+ flex-direction: row;
4
+ }
@@ -0,0 +1 @@
1
+ export * from './Stepper';
@@ -0,0 +1,12 @@
1
+ export enum StepState {
2
+ Completed = 'completed',
3
+ Current = 'current',
4
+ Loading = 'loading',
5
+ Waiting = 'waiting',
6
+ Rejected = 'rejected',
7
+ }
8
+
9
+ export enum StepValidationResult {
10
+ Completed = 'completed',
11
+ Rejected = 'rejected',
12
+ }