Files
SlimeVR-Server/gui/src/components/onboarding/StepperSlider.tsx

197 lines
4.7 KiB
TypeScript

import classNames from 'classnames';
import {
FC,
MouseEventHandler,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import { useElemSize } from '@/hooks/layout';
import { CheckIcon } from '@/components/commons/icon/CheckIcon';
import { Typography } from '@/components/commons/Typography';
import { useDebouncedEffect } from '@/hooks/timeout';
type StepComponentType = FC<{
nextStep: () => void;
prevStep: () => void;
resetSteps: () => void;
variant: 'alone' | 'onboarding';
active: boolean;
}>;
export type Step = {
type: 'numbered' | 'fullsize';
component: StepComponentType;
};
export function StepContainer({
children,
width,
active,
type,
step,
variant,
}: {
type: 'numbered' | 'fullsize';
variant: 'alone' | 'onboarding';
children: ReactNode;
width: number;
active: boolean;
step: number;
}) {
return (
<div
className={classNames(
'step-container transition-transform duration-500 relative w-full xs:p-8 mobile:p-2 rounded-lg flex gap-4 h-full',
!active && 'opacity-40 pointer-events-none',
variant === 'onboarding' && 'bg-background-70',
variant === 'alone' && 'bg-background-60'
)}
style={{
minWidth: width,
width,
}}
>
{type === 'numbered' && (
<div className="xs:flex xs:flex-col mobile:absolute mobile:-top-3 mobile:-right-4">
<div className="bg-accent-background-40 rounded-full h-8 w-8 flex flex-col items-center justify-center">
<Typography variant="section-title" bold>
{step + 1}
</Typography>
</div>
</div>
)}
{children}
</div>
);
}
export function StepDot({
active,
done,
onClick,
}: {
active?: boolean;
done?: boolean;
onClick?: MouseEventHandler<HTMLDivElement>;
}) {
return (
<div
className={classNames(
'flex h-4 w-4 rounded-full justify-center items-center fill-background-10 transition-all',
active || done ? 'bg-accent-background-20 ' : 'bg-background-60'
)}
onClick={onClick}
>
{active && <div className="flex h-2 w-2 rounded-full bg-background-10" />}
{done && <CheckIcon />}
</div>
);
}
export function StepperSlider({
variant,
steps,
back,
forward,
}: {
variant: 'alone' | 'onboarding';
steps: Step[];
/**
* Ran when step is 0 and `prevStep` is executed
*/
back?: () => void;
/**
* Ran when step is `steps.length - 1` and nextStep is executed
*/
forward?: () => void;
}) {
const ref = useRef<HTMLDivElement | null>(null);
const { width } = useElemSize(ref);
const [shouldAnimate, setShouldAnimate] = useState(true);
const [step, setStep] = useState(0);
useEffect(() => {
setStep((x) => Math.min(x, steps.length - 1));
}, [steps.length]);
const nextStep = () => {
if (step + 1 === steps.length) {
forward?.();
return;
}
setStep(step + 1);
};
const prevStep = () => {
if (step - 1 < 0) {
back?.();
return;
}
setStep(step - 1);
};
const resetSteps = () => {
setStep(0);
};
useEffect(() => {
setShouldAnimate(false);
}, [width]);
// Make it so if you resize the window it wont try to move the slide with an animation
useDebouncedEffect(
() => {
setShouldAnimate(true);
},
[width],
500
);
return (
<div className="w-full flex flex-col gap-4">
<div className="w-full flex" ref={ref}>
<div
className={classNames('flex gap-8', {
'transition-transform duration-500 ': shouldAnimate,
})}
style={{ transform: `translateX(-${(width + 32) * step}px)` }}
>
{steps.map(({ type, component: StepComponent }, index) => (
<StepContainer
variant={variant}
key={index}
type={type}
width={width}
active={index === step}
step={index}
>
<StepComponent
variant={variant}
nextStep={nextStep}
prevStep={prevStep}
resetSteps={resetSteps}
active={index === step}
/>
</StepContainer>
))}
</div>
</div>
<div className="flex justify-center items-center gap-2">
{Array.from({ length: steps.length }).map((_, index) => (
<div key={index} className="flex items-center gap-2">
{index !== 0 && (
<div className="w-5 h-1 bg-background-50 rounded-full" />
)}
<StepDot
active={index === step}
done={index < step}
// onClick={() => setStep(index)}
/>
</div>
))}
</div>
</div>
);
}