Files
sign/packages/ui/primitives/stepper.tsx

110 lines
2.6 KiB
TypeScript
Raw Normal View History

2023-12-03 12:50:56 -05:00
import React, { createContext, useContext, useState } from 'react';
2023-12-02 22:30:10 -05:00
import type { FC } from 'react';
2023-12-07 15:06:49 +11:00
type StepContextValue = {
2023-12-03 12:50:56 -05:00
isCompleting: boolean;
2023-12-03 01:15:59 -05:00
stepIndex: number;
currentStep: number;
totalSteps: number;
isFirst: boolean;
isLast: boolean;
nextStep: () => void;
previousStep: () => void;
2023-12-02 22:30:10 -05:00
};
2023-12-07 15:06:49 +11:00
const StepContext = createContext<StepContextValue | null>(null);
2023-12-02 22:30:10 -05:00
type StepperProps = {
children: React.ReactNode;
2023-12-03 12:50:56 -05:00
onComplete?: () => void | Promise<void>;
2023-12-02 22:30:10 -05:00
onStepChanged?: (currentStep: number) => void;
2023-12-03 01:15:59 -05:00
currentStep?: number; // external control prop
setCurrentStep?: (step: number) => void; // external control function
2023-12-02 22:30:10 -05:00
};
export const Stepper: FC<StepperProps> = ({
children,
onComplete,
onStepChanged,
currentStep: propCurrentStep,
setCurrentStep: propSetCurrentStep,
}) => {
const [stateCurrentStep, stateSetCurrentStep] = useState(1);
2023-12-03 12:50:56 -05:00
const [isCompleting, setIsCompleting] = useState(false);
2023-12-02 22:30:10 -05:00
// Determine if props are provided, otherwise use state
const isControlled = propCurrentStep !== undefined && propSetCurrentStep !== undefined;
const currentStep = isControlled ? propCurrentStep : stateCurrentStep;
const setCurrentStep = isControlled ? propSetCurrentStep : stateSetCurrentStep;
const totalSteps = React.Children.count(children);
2023-12-03 12:50:56 -05:00
const handleComplete = async () => {
2023-12-07 15:06:49 +11:00
try {
if (!onComplete) {
return;
}
setIsCompleting(true);
await onComplete();
setIsCompleting(false);
} catch (error) {
setIsCompleting(false);
throw error;
2023-12-03 12:50:56 -05:00
}
};
const handleStepChange = (nextStep: number) => {
setCurrentStep(nextStep);
2023-12-07 15:06:49 +11:00
onStepChanged?.(nextStep);
2023-12-03 12:50:56 -05:00
};
2023-12-02 22:30:10 -05:00
const nextStep = () => {
if (currentStep < totalSteps) {
2023-12-03 12:50:56 -05:00
void handleStepChange(currentStep + 1);
2023-12-02 22:30:10 -05:00
} else {
2023-12-03 12:50:56 -05:00
void handleComplete();
2023-12-02 22:30:10 -05:00
}
};
const previousStep = () => {
if (currentStep > 1) {
2023-12-03 12:50:56 -05:00
void handleStepChange(currentStep - 1);
2023-12-02 22:30:10 -05:00
}
};
2023-12-03 01:15:59 -05:00
// Empty stepper
2023-12-03 11:21:51 -05:00
if (totalSteps === 0) {
return null;
}
2023-12-03 01:15:59 -05:00
const currentChild = React.Children.toArray(children)[currentStep - 1];
2023-12-07 15:06:49 +11:00
const stepContextValue: StepContextValue = {
2023-12-03 12:50:56 -05:00
isCompleting,
2023-12-02 23:56:07 -05:00
stepIndex: currentStep - 1,
2023-12-02 22:30:10 -05:00
currentStep,
totalSteps,
isFirst: currentStep === 1,
isLast: currentStep === totalSteps,
nextStep,
previousStep,
2023-12-03 01:15:59 -05:00
};
2023-12-02 23:56:07 -05:00
2023-12-03 01:15:59 -05:00
return <StepContext.Provider value={stepContextValue}>{currentChild}</StepContext.Provider>;
};
2023-12-02 22:30:10 -05:00
2023-12-03 01:15:59 -05:00
/** Hook for children to use the step context */
2023-12-07 15:06:49 +11:00
export const useStep = (): StepContextValue => {
2023-12-03 01:15:59 -05:00
const context = useContext(StepContext);
2023-12-07 15:06:49 +11:00
2023-12-03 11:21:51 -05:00
if (!context) {
throw new Error('useStep must be used within a Stepper');
}
2023-12-07 15:06:49 +11:00
2023-12-03 01:15:59 -05:00
return context;
2023-12-02 22:30:10 -05:00
};