Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(stepper): new stepper component #318

Open
wants to merge 123 commits into
base: main
Choose a base branch
from

Conversation

damianricobelli
Copy link
Contributor

@damianricobelli damianricobelli commented May 8, 2023

Hi! In this opportunity I present a new component: Stepper.

The idea of this in its beginnings was to make it as modular and flexible as possible for development.

A basic example of the application is this:

const steps = [
  { label: "Step 1" },
  { label: "Step 2" },
  { label: "Step 3" },
] satisfies StepConfig[]

export const StepperDemo = () => {
  const {
    nextStep,
    prevStep,
    resetSteps,
    setStep,
    activeStep,
    isDisabledStep,
    isLastStep,
    isOptionalStep,
  } = useStepper({
    initialStep: 0,
    steps,
  })

  return (
    <>
      <Steps activeStep={activeStep}>
        {steps.map((step, index) => (
          <Step index={index} key={index} {...step}>
            <div className="bg-muted h-40 w-full p-4">
              <p>Step {index + 1} content</p>
            </div>
          </Step>
        ))}
      </Steps>
      <div className="flex items-center justify-end gap-2">
        {activeStep === steps.length ? (
          <>
            <h2>All steps completed!</h2>
            <Button onClick={resetSteps}>Reset</Button>
          </>
        ) : (
          <>
            <Button disabled={isDisabledStep} onClick={prevStep}>
              Prev
            </Button>
            <Button onClick={nextStep}>
              {isLastStep ? "Finish" : isOptionalStep ? "Skip" : "Next"}
            </Button>
          </>
        )}
      </div>
    </>
  )
}

Here is a complete video of the different use cases:

Grabacion.de.pantalla.2023-05-08.a.la.s.13.14.45.mov

@vercel
Copy link

vercel bot commented May 8, 2023

@damianricobelli is attempting to deploy a commit to the shadcn-pro Team on Vercel.

A member of the Team first needs to authorize it.

@damianricobelli damianricobelli changed the title feat(stepper): add component with docs feat(stepper): new stepper component May 8, 2023
@vercel
Copy link

vercel bot commented May 8, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
ui ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 17, 2024 7:14pm
1 Ignored Deployment
Name Status Preview Comments Updated (UTC)
next-template ⬜️ Ignored (Inspect) Visit Preview Apr 17, 2024 7:14pm

@jocarrd
Copy link

jocarrd commented May 9, 2023

love it 👀

@shadcn
Copy link
Collaborator

shadcn commented May 9, 2023

This looks incredible @damianricobelli I'll review.

@its-monotype
Copy link
Contributor

its-monotype commented May 9, 2023

Looks amazing 😍 Unfortunately I don't have time to review and research about this component. However, look what I recently found: https://saas-ui.dev/docs/components/navigation/stepper.
https://github.com/saas-js/saas-ui/tree/main/packages/saas-ui-core/src/stepper

This can serve as reference to improve or borrow ideas to simplify the implementation. From what I can suggest it is to rename Step to StepperStep and useSteps to useStepper to comply with the general API conventions of the components and it will be more unique name to prevent conflicts.

@damianricobelli
Copy link
Contributor Author

Looks amazing 😍 Unfortunately I don't have time to review and research about this component. However, look what I recently found: https://saas-ui.dev/docs/components/navigation/stepper. https://github.com/saas-js/saas-ui/tree/main/packages%2Fsaas-ui-stepper

This can serve as reference to improve or borrow ideas to simplify the implementation. From what I can suggest it is to rename Step to StepperStep and useSteps to useStepper to comply with the general API conventions of the components and it will be more unique name to prevent conflicts.

Thank you very much for your feedback! I'll be reviewing tomorrow what you just shared and your suggestions 🫶

@damianricobelli
Copy link
Contributor Author

@shadcn What do you think about this component? Do you think we should adjust anything so that it can be launched on prod?

@destino92
Copy link

Is this is still in progress?

@damianricobelli
Copy link
Contributor Author

damianricobelli commented Jun 20, 2023

Is this is still in progress?

@destino92 From my side the component is ready. Just need to know if @shadcn agrees to move forward and add it to the CLI that brings and details that you think are missing in terms of documentation.

@drewhoffer
Copy link

This looks good!

@dan5py
Copy link
Contributor

dan5py commented Jun 30, 2023

Hi @damianricobelli, this component looks very good. Could you please update it to the new version (different themes, registry, docs, etc.)?

@damianricobelli
Copy link
Contributor Author

@dan5py yes of course. Between today and Monday I will be making the necessary changes so that the component allows the last addition you mention.

@damianricobelli
Copy link
Contributor Author

@shadcn Could you check this? I've already updated the code with all the latest stuff in the main branch. There are already several people watching the release of this component 🤩 🚀

@damianricobelli
Copy link
Contributor Author

damianricobelli commented Jul 4, 2023

Hi @damianricobelli, this component looks very good. Could you please update it to the new version (different themes, registry, docs, etc.)?

Done @dan5py! 🥳

@damianricobelli
Copy link
Contributor Author

My goal is to maximize the flexibility of this component by allowing developers to use basic cases, but if they require something more advanced, they can do so as well

@damianricobelli
Copy link
Contributor Author

Stepperize 4.1 is out!

Next step: document the components and update this PR. I will do it in the next weeks

@woodsbox
Copy link

woodsbox commented Jan 6, 2025

@damianricobelli thnx for the good work only how to implement this in because your code with the
is not working or I'm doing it wrong?

@damianricobelli
Copy link
Contributor Author

Exploring and polishing the API for shadcn. getting closer!

I've been thinking about the power of Scoped + typesafe for a while now, and I think I've found the right API.

This is an example to generate the same circled example we have in the stepperize documentation.

The key is that by default a Scope will be created (you can avoid it, I will explain it in the docs) and you will have access both in the navigation and in the panels to the stepper methods (coming from useStepper).

export const Testing = () => {
  return (
    <Stepper
      instance={stepperInstance}
      variant="circle"
      className="space-y-6 p-6 border rounded-lg w-[450px] m-4"
    >
      <StepperNavigation>
        {({ methods }) => {
          return steps.map((step) => (
            <StepperStep
              key={step.id}
              of={step}
              onClick={() => methods.goTo(step.id)}
              icon={step.icon}
            >
              <StepperTitle>{step.title}</StepperTitle>
              <StepperDescription>{step.description}</StepperDescription>
            </StepperStep>
          ))
        }}
      </StepperNavigation>
      {steps.map((step) => {
        return (
          <StepperPanel key={step.id} when={step}>
            {({ step, methods }) => {
              return (
                <div className="flex flex-col gap-2">
                  <span>Content for {step.title} step</span>
                  <div className="space-y-4">
                    {!methods.isLast ? (
                      <div className="flex justify-end gap-4">
                        <Button
                          variant="secondary"
                          onClick={methods.prev}
                          disabled={methods.isFirst}
                        >
                          Back
                        </Button>
                        <Button onClick={methods.next}>
                          {methods.isLast ? "Complete" : "Next"}
                        </Button>
                      </div>
                    ) : (
                      <Button onClick={methods.reset}>Reset</Button>
                    )}
                  </div>
                </div>
              )
            }}
          </StepperPanel>
        )
      })}
    </Stepper>
  )
}

And the result:

image

@kmalloy24
Copy link

This looks awesome! Is there a way to make the stepper responsive by default? Switch between horz/vert on desktop/mobile?

@damianricobelli
Copy link
Contributor Author

This looks awesome! Is there a way to make the stepper responsive by default? Switch between horz/vert on desktop/mobile?

@kmalloy24 I will make this possible through a hook that checks matchMedia API 🤝

@damianricobelli
Copy link
Contributor Author

I am cleaning up the branch to initialize the new changes with the final version using @stepperize/react. So, those who need the previous version can still check this PR point with the hash f8a0b2edbb304b0db6b13df1c7f937922e4172fc

- Introduced a new UI component "stepper" with its dependencies and file path.
- Added an example component "stepper-demo" that utilizes the "stepper".
- Updated registry files to include both components in the respective JSON and TypeScript files.
@resatyildiz
Copy link

resatyildiz commented Jan 23, 2025

@damianricobelli hi! It's looking awesome, thank you for your effort. Have you thought before about bind popstate event to this component? You know, some user would want to back to prev step when click to browser back button.

@ImanMahmoudinasab
Copy link

ImanMahmoudinasab commented Jan 24, 2025

Hey @damianricobelli and @resatyildiz, I think stepper component shouldn't get bind to routes internally. Instead, we should use a stepper in a controlled way and define routes for each step. When the routes change, pass the relevant step index or ID to the stepper. When the stepper goes to the next or previous step, update the route of the page.

@damianricobelli
Copy link
Contributor Author

@ImanMahmoudinasab yea, you're right. The component should not handle anything related to routing. Also, it is much simpler to handle query params instead of routes for something like a stepper.

@damianricobelli
Copy link
Contributor Author

Estimated date for review of the PR by the entire community and @shadcn -> tomorrow 👀

@resatyildiz
Copy link

@ImanMahmoudinasab actually I agree with you. Already it can bind to window history with query params. You can think this feature @damianricobelli

@damianricobelli
Copy link
Contributor Author

All documentation and component ready for review! @shadcn 👍

@JoelVenable
Copy link

JoelVenable commented Jan 24, 2025

Hey @damianricobelli and @resatyildiz, I think stepper component shouldn't get bind to routes internally. Instead, we should use a stepper in a controlled way and define routes for each step. When the routes change, pass the relevant step index or ID to the stepper. When the stepper goes to the next or previous step, update the route of the page.

Agreed. I'm having quite a bit of difficulty with step control with the underlying stepperize library. Either external control or alternatively an onStepChange callback affordance seem like intuitive uses, neither which seem to be part of the API (perhaps they are there, just not documented??).

My use case is a multi-step form where each step must be validated prior to allowing progression to later steps. I'm having a surprising amount of difficulty synchronizing this component with a zustand store.

Right now I've resorted to manually implementing the "canPrevious", "canNext" logic in the zustand store, with a subscription to call the goTo method. If there's something I'm missing, feedback is welcome.

import { CardContent, CardFooter, CardHeader } from '@repo/ui/components/card'
import {
  defineStepper,
  Stepper,
  StepperAction,
  StepperControls,
  StepperNavigation,
  StepperPanel,
  StepperStep,
  StepperTitle,
} from '@repo/ui/components/stepper'
import { SellPassStepOne } from './sell-pass-step-one'
import { SellPassStepThree } from './sell-pass-step-three'
import { SellPassStepTwo } from './sell-pass-step-two'
import { sellProductStore, useSellProductStore } from './sell-product.store'
import { useEffect } from 'react'

const stepper = defineStepper(
  {
    id: 'step1',
    title: 'Type',
    header: 'Sell a Pass or Enforcement?',
    Component: SellPassStepOne,
  },
  {
    id: 'step2',
    title: 'Customer',
    header: 'Please enter the customer details',
    Component: SellPassStepTwo,
  },
  {
    id: 'step3',
    title: 'Payment',
    header: 'Please enter the payment details',
    Component: SellPassStepThree,
  },
)

export const SellForm = () => {
  const s = stepper.useStepper()
  const store = useSellProductStore()

  const steps = s.all

  useEffect(() => {
    sellProductStore.subscribe((state) => {
      s.goTo(state.stepId)
    })
  }, [s])

  return (
    <Stepper instance={stepper}>
      <CardHeader>
        <StepperNavigation>
          {({ methods }) =>
            steps.map((step) => {
              return (
                <StepperStep
                  key={step.id}
                  of={step}
                  disabled={!store.enabledSteps.includes(step.id)}
                  onClick={() => store.setStep(step.id)}
                >
                  <StepperTitle>{step.title}</StepperTitle>
                </StepperStep>
              )
            })
          }
        </StepperNavigation>
      </CardHeader>
      <CardContent>
        {steps.map(({ Component, ...step }) => (
          <StepperPanel key={step.id} when={step}>
            <Component />
          </StepperPanel>
        ))}
      </CardContent>
      <CardFooter>
        <StepperControls className="flex justify-items-end w-full">
          <StepperAction action="prev" disabled={!store.canPrev}>
            Previous
          </StepperAction>
          <StepperAction action="next" disabled={!store.canNext}>
            Next
          </StepperAction>
          <StepperAction action="reset" disabled={!store.canReset}>
            Reset
          </StepperAction>
        </StepperControls>
      </CardFooter>
    </Stepper>
  )
}

@damianricobelli
Copy link
Contributor Author

damianricobelli commented Jan 25, 2025

@JoelVenable I have added in the PR an example of a form with react hook form. And I have updated a little the final logic of methods, where they are now obtained as part of the children of to maintain the typesafe API. Remember that the useStepper hook will work in these components if you use it within the Stepper component since it is a Provider.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: roadmap This looks great. We'll add it to the roadmap, review and merge. new component
Projects
None yet
Development

Successfully merging this pull request may close these issues.