I set up the basics of the UI on codesandbox.
The main part of how this works is via scrollIntoView
using a reference to the div element on each Step. It's important to note that this will work on every modern browser but safari for the smooth scrolling.
Obviously for the actual form parts and moving data around, all of that will still need to be implemented, but this demonstrates nearly all of the navigation/scrolling behaviors as your example.
For reference, here's the main code:
import { useEffect, useRef, useState } from "react";
import A from "./A";
import B from "./B";
import C from "./C";
import D from "./D";
import "./styles.css";
const steps = [A, B, C, D];
export default function App() {
const [step, setStep] = useState(0);
/** Set up a ref that refers to an array, this will be used to hold
* a reference to each step
*/
const refs = useRef<(HTMLDivElement | null)[]>([]);
/** Whenever the step changes, scroll it into view! useEffect needed to wait
* until the new component is rendered so that the ref will properly exist
*/
useEffect(() => {
refs.current[step]?.scrollIntoView({ behavior: "smooth" });
}, [step]);
return (
<div className="App">
{steps
.filter((_, index) => index <= step)
.map((Step, index) => (
<Step
key={index}
/** using `domRef` here to avoid having to set up forwardRef.
* Same behavior regardless, but with less hassle as it's an
* ordianry prop.
*/
domRef={(ref) => (refs.current[index] = ref)}
/** both prev/next handlers for scrolling into view */
toPrev={() => {
refs.current[index - 1]?.scrollIntoView({ behavior: "smooth" });
}}
toNext={() => {
if (step === index + 1) {
refs.current[index + 1]?.scrollIntoView({ behavior: "smooth" });
}
/** This mimics behavior in the reference. Clicking next sets the next step
*/
setStep(index + 1);
}}
/** an override to enable reseting the steps as needed in other ways.
* I.e. changing the initial radio resets to the 0th step
*/
setStep={setStep}
step={index}
/>
))}
</div>
);
}
And component A
import React, { useEffect, useState } from "react";
import { Step } from "./utils";
interface AProps extends Step {}
function A(props: AProps) {
const [value, setValue] = useState("");
const values = [
{ label: "Complaint", value: "complaint" },
{ label: "Compliment", value: "compliment" }
];
const { step, setStep } = props;
useEffect(() => {
setStep(step);
}, [setStep, step, value]);
return (
<div className="step" ref={props.domRef}>
<h1>Component A</h1>
<div>
{values.map((option) => (
<label key={option.value}>
{option.label}
<input
onChange={(ev) => setValue(ev.target.value)}
type="radio"
name="type"
value={option.value}
/>
</label>
))}
</div>
<button
className="next"
onClick={() => {
if (value) {
props.toNext();
}
}}
>
NEXT
</button>
</div>
);
}
export default A;