kleckrelay/src/components/MultiStepForm.tsx
2022-10-15 10:36:17 +02:00

171 lines
3.7 KiB
TypeScript

import React, {ReactElement, useEffect, useRef, useState} from "react"
import {Paper} from "@mui/material"
import {whenElementHasBounds} from "~/utils"
export interface MultiStepFormProps {
steps: ReactElement[]
index: number
duration?: number
easing?: string
}
function MultiStepForm({
steps,
index,
duration = 900,
easing = "ease-in-out",
}: MultiStepFormProps): ReactElement {
const $currentElement = useRef<HTMLDivElement>(null)
const $timeout = useRef<any>()
const [currentIndex, setCurrentIndex] = useState<number>(index)
const [currentSize, setCurrentSize] = useState<any>(null)
const [nextSize, setNextSize] = useState<any>(null)
const isTransitioning = currentIndex < index
useEffect(() => {
if (index > currentIndex) {
$timeout.current = setTimeout(() => {
setCurrentSize(null)
setNextSize(null)
setCurrentIndex(index)
}, duration)
} else if (index < currentIndex) {
// "Going-back" animation is not supported
setCurrentIndex(index)
setCurrentSize(null)
setNextSize(null)
}
return $timeout.current?.cancel!
}, [index, currentIndex])
const hasSize = Boolean(
(currentSize?.width || nextSize?.width) &&
(currentSize?.height || nextSize?.height),
)
return (
<div
style={{
position: "relative",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width:
Math.max(currentSize?.width || 0, nextSize?.width || 0) ||
"100%",
height:
Math.max(currentSize?.height || 0, nextSize?.height || 0) ||
"100%",
overflow: hasSize ? "hidden" : "visible",
}}
>
<Paper
key="paper"
style={{
...currentSize,
position: "absolute",
transition: easing,
transitionDuration: isTransitioning
? `${duration}ms`
: "0ms",
transformOrigin: isTransitioning
? `${nextSize?.x - currentSize.x}px ${
nextSize?.y - currentSize.y
}px`
: "",
// Scale the difference between currentSize and nextSize
transform: isTransitioning
? `
scaleX(${nextSize.width / currentSize.width})
scaleY(${nextSize.height / currentSize.height})
translateX(${nextSize.x - currentSize.x}px)
translateY(${nextSize.y - currentSize.y}px)
`
: "scale(1)",
}}
></Paper>
<div
// @ts-ignore
ref={ref => {
if (ref === null) {
return
}
// @ts-ignore
$currentElement.current = ref
const {
width = 0,
height = 0,
x,
y,
} = ref.getBoundingClientRect()
if (
width !== 0 &&
height !== 0 &&
width !== currentSize?.width &&
height !== currentSize?.height
) {
setCurrentSize({
width: width,
height: height,
x: x,
y: y,
})
}
}}
style={{
position: "absolute",
width: "max-content",
height: "max-content",
transition: easing,
transitionDuration: isTransitioning ? `${duration}ms` : "",
transform: isTransitioning ? "translateX(-100%)" : "",
}}
>
{steps[currentIndex]}
</div>
<div
// @ts-ignore
ref={whenElementHasBounds(rect => {
if (nextSize === null) {
setNextSize({
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y,
})
}
})}
style={{
position: "absolute",
width: "max-content",
height: "max-content",
transition: easing,
transitionDuration: isTransitioning ? `${duration}ms` : "",
transform: isTransitioning
? ""
: nextSize?.x
? "translateX(100%)"
: "",
}}
>
{currentIndex === steps.length - 1
? null
: steps[currentIndex + 1]}
</div>
</div>
)
}
export default MultiStepForm