Skip to content

Commit 08b85f9

Browse files
rtiofrosso
andauthored
Add new StepperPanel component (#10808)
Co-authored-by: Francesco <[email protected]>
1 parent bdf695a commit 08b85f9

File tree

10 files changed

+478
-121
lines changed

10 files changed

+478
-121
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: dev
3+
4+
Add the new StepperPanel component.

client/components/stepper/index.tsx

Lines changed: 4 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,11 @@
11
/**
22
* External dependencies
33
*/
4-
import React, { createContext, useContext, useState } from 'react';
54

65
/**
76
* Internal dependencies
87
*/
9-
10-
interface UseContextValueParams {
11-
steps: Record< string, React.ReactElement >;
12-
initialStep?: string;
13-
onStepChange?: ( step: string ) => void;
14-
onComplete?: () => void;
15-
onExit?: () => void;
16-
}
17-
18-
const useContextValue = ( {
19-
steps,
20-
initialStep,
21-
onStepChange,
22-
onComplete,
23-
onExit,
24-
}: UseContextValueParams ) => {
25-
const keys = Object.keys( steps );
26-
const [ currentStep, setCurrentStep ] = useState(
27-
initialStep ?? keys[ 0 ]
28-
);
29-
30-
const progress = ( keys.indexOf( currentStep ) + 1 ) / keys.length;
31-
32-
const nextStep = () => {
33-
const index = keys.indexOf( currentStep );
34-
const next = keys[ index + 1 ];
35-
36-
if ( next ) {
37-
setCurrentStep( next );
38-
onStepChange?.( next );
39-
} else {
40-
onComplete?.();
41-
}
42-
};
43-
44-
const prevStep = () => {
45-
const index = keys.indexOf( currentStep );
46-
const prev = keys[ index - 1 ];
47-
48-
if ( prev ) {
49-
setCurrentStep( prev );
50-
onStepChange?.( prev );
51-
} else {
52-
onExit?.();
53-
}
54-
};
55-
56-
const exit = () => onExit?.();
57-
58-
return {
59-
currentStep,
60-
progress,
61-
nextStep,
62-
prevStep,
63-
exit,
64-
};
65-
};
66-
67-
type ContextValue = ReturnType< typeof useContextValue >;
68-
69-
const StepperContext = createContext< ContextValue | null >( null );
70-
71-
interface StepperProps {
72-
children: React.ReactElement< { name: string } >[];
73-
initialStep?: string;
74-
onStepChange?: ( step: string ) => void;
75-
onComplete?: () => void;
76-
onExit?: () => void;
77-
}
78-
79-
const childrenToSteps = ( children: StepperProps[ 'children' ] ) => {
80-
return children.reduce(
81-
( acc: Record< string, React.ReactElement >, child, index ) => {
82-
if ( React.isValidElement( child ) ) {
83-
acc[ child.props.name ?? index ] = child;
84-
}
85-
return acc;
86-
},
87-
{}
88-
);
89-
};
90-
91-
export const Stepper: React.FC< React.PropsWithChildren< StepperProps > > = ( {
92-
children,
93-
...rest
94-
} ) => {
95-
const steps = childrenToSteps( children );
96-
const value = useContextValue( {
97-
steps,
98-
...rest,
99-
} );
100-
const CurrentStep = steps[ value.currentStep ];
101-
102-
return (
103-
<StepperContext.Provider value={ value }>
104-
{ CurrentStep }
105-
</StepperContext.Provider>
106-
);
107-
};
108-
109-
export const useStepperContext = (): ContextValue => {
110-
const context = useContext( StepperContext );
111-
if ( ! context ) {
112-
throw new Error( 'useStepperContext() must be used within <Stepper>' );
113-
}
114-
return context;
115-
};
8+
export { Stepper, useStepperContext } from './stepper';
9+
export { StepperPanel } from './stepper-panel';
10+
export { StepperContext } from './stepper-context';
11+
export * from './stepper-types';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { createContext } from 'react';
5+
6+
/**
7+
* Internal dependencies
8+
*/
9+
import { StepperContextValue } from './stepper-types';
10+
11+
export const StepperContext = createContext< StepperContextValue | null >(
12+
null
13+
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import React from 'react';
5+
import { check } from '@wordpress/icons';
6+
7+
/**
8+
* Internal dependencies
9+
*/
10+
import { Icon } from 'wcpay/components/wp-components-wrapped';
11+
import './style.scss';
12+
import clsx from 'clsx';
13+
14+
interface StepperIndicatorProps {
15+
steps: string[];
16+
currentStep: number;
17+
}
18+
19+
export const StepperPanel: React.FC< StepperIndicatorProps > = ( {
20+
steps,
21+
currentStep,
22+
} ) => (
23+
<div className="stepper-panel">
24+
{ steps.map( ( label, idx ) => {
25+
const isComplete = idx < currentStep;
26+
const isActive = idx === currentStep;
27+
return (
28+
<div
29+
key={ label }
30+
className={ clsx( 'stepper-step', {
31+
active: isActive,
32+
complete: isComplete,
33+
} ) }
34+
>
35+
<div className="stepper-circle">
36+
{ isComplete ? (
37+
<Icon icon={ check } size={ 36 } />
38+
) : (
39+
idx + 1
40+
) }
41+
</div>
42+
<div className="stepper-label">{ label }</div>
43+
{ idx < steps.length - 1 && (
44+
<div className="stepper-line" />
45+
) }
46+
</div>
47+
);
48+
} ) }
49+
</div>
50+
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Types and interfaces for the Stepper component and context
2+
3+
/**
4+
* External dependencies
5+
*/
6+
import React from 'react';
7+
8+
/**
9+
* Internal dependencies
10+
*/
11+
12+
export type StepperContextValue = {
13+
currentStep: string;
14+
progress: number;
15+
nextStep: () => void;
16+
prevStep: () => void;
17+
exit: () => void;
18+
};
19+
20+
export interface UseContextValueParams {
21+
steps: Record< string, React.ReactElement >;
22+
initialStep?: string;
23+
onStepChange?: ( step: string ) => void;
24+
onComplete?: () => void;
25+
onExit?: () => void;
26+
}
27+
28+
export interface StepperProps {
29+
children: React.ReactElement< { name: string } >[];
30+
initialStep?: string;
31+
onStepChange?: ( step: string ) => void;
32+
onComplete?: () => void;
33+
onExit?: () => void;
34+
}

client/components/stepper/stepper.tsx

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import React, { useContext, useState } from 'react';
5+
6+
/**
7+
* Internal dependencies
8+
*/
9+
import { StepperContext } from './stepper-context';
10+
import {
11+
StepperContextValue,
12+
UseContextValueParams,
13+
StepperProps,
14+
} from './stepper-types';
15+
16+
const useContextValue = ( {
17+
steps,
18+
initialStep,
19+
onStepChange,
20+
onComplete,
21+
onExit,
22+
}: UseContextValueParams ): StepperContextValue => {
23+
const keys = Object.keys( steps );
24+
const [ currentStep, setCurrentStep ] = useState(
25+
initialStep ?? keys[ 0 ]
26+
);
27+
28+
const progress = ( keys.indexOf( currentStep ) + 1 ) / keys.length;
29+
30+
const nextStep = () => {
31+
const index = keys.indexOf( currentStep );
32+
const next = keys[ index + 1 ];
33+
34+
if ( next ) {
35+
setCurrentStep( next );
36+
onStepChange?.( next );
37+
} else {
38+
onComplete?.();
39+
}
40+
};
41+
42+
const prevStep = () => {
43+
const index = keys.indexOf( currentStep );
44+
const prev = keys[ index - 1 ];
45+
46+
if ( prev ) {
47+
setCurrentStep( prev );
48+
onStepChange?.( prev );
49+
} else {
50+
onExit?.();
51+
}
52+
};
53+
54+
const exit = () => onExit?.();
55+
56+
return {
57+
currentStep,
58+
progress,
59+
nextStep,
60+
prevStep,
61+
exit,
62+
};
63+
};
64+
65+
function childrenToSteps( children: StepperProps[ 'children' ] ) {
66+
return children.reduce(
67+
( acc: Record< string, React.ReactElement >, child, index ) => {
68+
if ( React.isValidElement( child ) ) {
69+
acc[ child.props.name ?? index ] = child;
70+
}
71+
return acc;
72+
},
73+
{}
74+
);
75+
}
76+
77+
export const Stepper: React.FC< React.PropsWithChildren< StepperProps > > = ( {
78+
children,
79+
...rest
80+
} ) => {
81+
const steps = childrenToSteps( children );
82+
const value = useContextValue( {
83+
steps,
84+
...rest,
85+
} );
86+
const CurrentStep = steps[ value.currentStep ];
87+
88+
return (
89+
<StepperContext.Provider value={ value }>
90+
{ CurrentStep }
91+
</StepperContext.Provider>
92+
);
93+
};
94+
95+
export const useStepperContext = (): StepperContextValue => {
96+
const context = useContext( StepperContext );
97+
if ( ! context ) {
98+
throw new Error( 'useStepperContext() must be used within <Stepper>' );
99+
}
100+
return context;
101+
};

0 commit comments

Comments
 (0)