2

I'm trying to replicate this stepper like functionality using react.

https://www.commbank.com.au/retail/complaints-compliments-form?ei=CTA-MakeComplaint

Below is my stackblitz, How can I achieve this functionality without using any 3rd Party plugins.

https://stackblitz.com/edit/react-ts-9cfjs3

Arun Kumar
  • 187
  • 1
  • 9
  • There are many packages out there. For example: https://mui.com/components/steppers/ and https://www.npmjs.com/package/react-stepper-horizontal – Harry Mar 08 '22 at 19:52
  • I want to achieve it without using any 3rd part plugins. Also those plugin functionality is not the same – Arun Kumar Mar 09 '22 at 02:44
  • Yeah right. I read that as "How can I achieve this with any 3rd party plugins :D" – Harry Mar 09 '22 at 05:54

1 Answers1

2

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;
Zachary Haber
  • 10,376
  • 1
  • 17
  • 31
  • your code works perfectly fine, but when I implement it smooth scroll is not working, instead it is jumping. Any idea why >?? – Arun Kumar Mar 09 '22 at 08:00
  • also I'm getting this error on console `index.js:1 Warning: Each child in a list should have a unique "key" prop.` – Arun Kumar Mar 09 '22 at 08:07
  • @ArunKumar, I can't know why it's jumping for you without a codesandbox/stackblitz with your current code. And as for the warning, I totally forgot to set up the `key` prop on the array maps, I fixed it now. – Zachary Haber Mar 09 '22 at 16:30
  • this my stackblitz link I've replicated the issue. Could you please check https://stackblitz.com/edit/react-ts-kvyax9 – Arun Kumar Mar 10 '22 at 07:22
  • Aslo, We can we use this `react-scroll-to-component` for smoth scrolling ?? – Arun Kumar Mar 10 '22 at 07:27
  • @ArunKumar, it runs fine for me on that link just replacing the `react-scroll-to-component` use with the DOM's `scrollIntoView` works fine for me. And sure, you can use whatever 3rd party plugins you want, it's your code! I wouldn't necessarily look for a React specific library for it as it's a generic Javascript/HTML concern. You can also look at https://stackoverflow.com/questions/17722497/scroll-smoothly-to-specific-element-on-page for more thoughts – Zachary Haber Mar 10 '22 at 15:38
  • @Zachry Haber - Your solutions works perfect, but I also wanted to jump from step 1 to step 3 ? Please help me to modify your logic. Below is the stackblitz link I've created https://stackblitz.com/edit/react-ts-hn8ev4?file=app.tsx please help – Arun Kumar Jun 23 '22 at 18:43
  • I'd suggest just making a separate question at this point, but if you want to go straight from step 1 to step 3, `setStep(2)` should work, or `scrollToComponent(refs.current[index+1])` if that step is already available. – Zachary Haber Jun 23 '22 at 22:59