-
Notifications
You must be signed in to change notification settings - Fork 4.6k
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
base: main
Are you sure you want to change the base?
Conversation
@damianricobelli is attempting to deploy a commit to the shadcn-pro Team on Vercel. A member of the Team first needs to authorize it. |
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Ignored Deployment
|
love it 👀 |
This looks incredible @damianricobelli I'll review. |
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. 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 🫶 |
@shadcn What do you think about this component? Do you think we should adjust anything so that it can be launched on prod? |
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. |
This looks good! |
Hi @damianricobelli, this component looks very good. Could you please update it to the new version (different themes, registry, docs, etc.)? |
@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. |
@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 🤩 🚀 |
Done @dan5py! 🥳 |
Hello 👋 for those who are impatient for the release of this component: I understand you and I am too, but I am also working hard on a library with many improvements for this component. If you really can't wait and want to use this component, feel free to use the code from this PR or the one from the repository at this link: https://shadcn-stepper.vercel.app/ I am very close to finish the library, I just ask you not to keep commenting with messages like: "any news?" since I am actively working on the library I have mentioned. @shadcn is already aware of this and I'm sure he is also waiting for the release of the library. If you want to contribute value in your comments, please do it with problems you find in the code as it helps me to understand all the improvements I have to take into account in the library 🙌 |
Heads up: I just finished version 1.0 of the library! Now I have to document everything and after publishing it in npm I will be adding the library to this PR. This will take a few more days, patience, the best is yet to come! 🙌 |
Looking forward to using the official version of this someday. Enjoying this so far, but I had an issue of forms clearing on orientation change. Basically if you use a smaller device and input the form, you'd have to re-input everything if you were to turn it sideways. I managed to deal this issue by using react-reverse-portal to render the step content to wherever it needs to be rendered, but is there some official / better way to deal with this issue? Maybe the 1.0 fixes the orientation issue? 😃 |
@damianricobelli When will this be official? |
@warisareshi this week I think |
I'm waiting for this 🙏 |
I want to announce to everyone that the first version of the library is here! It is called @shadcn I will try to finish all the examples of the library and then I will add the components to this PR |
You rock bro!! |
New docs and improved API! The v1.0 is here! More examples coming soon.. |
👀 |
Just a reminder that I will shortly be posting an example on @stepperize/react that addresses this case 🤝 |
Hi, if you don't mind me asking, what does the progress look like on this at the moment? |
@Woofer21 I am trying to finish the library docs and examples and then I will refactor this PR with the library. |
@damianricobelli Thanks for the stepper, can you please bring back this site https://shadcn-stepper.vercel.app/ I'm in the middle of integrating stepper from one of the examples given. as the examples in the library are still in progress. Thanks in advance. https://stepperize.vercel.app/ |
Any updates on that? |
@damianricobelli well done. Any update on when this PR might be approved? |
Ok look, I want this component to get released as well but maybe some improvements can be made, over the past year I have worked on this for work and want to contribute the source to this endeavour, maybe you can get some takeaways from it. stepper-types.tsimport type { HTMLAttributes, ReactNode } from 'react';
import type { STEPPER_TRANSLATION_KEYS } from './useStepper';
import type { Options } from 'nuqs';
interface StepperProps extends HTMLAttributes<HTMLDivElement> {
queryKey?: string;
/**
* The steps of the stepper.
* @default []
*/
steps: Step[];
/** A function to translate the keys of the stepper. */
t?: (key: STEPPER_TRANSLATION_KEYS) => string;
}
interface Step {
/** The component of the step. */
component: ReactNode;
/** The description of the step. */
description?: string;
/** The title of the step. */
title: string;
/** Whether the step should be skipped. */
skip?: boolean;
}
type StepperDirection = 1 | -1 | 0;
/** the first number is the active step, the second number is the direction and starts with 0 */
type StepTuple = [number, StepperDirection, skipped?: boolean];
export type { StepperProps, Step, StepTuple, StepperDirection }; stepper.tsximport { animationFade, animationSlideInOut } from '#animations/index.ts';
import { Alert, AlertDescription, AlertTitle } from '#components/ui/alert.tsx';
import { cn } from '#lib/utils.ts';
import { AnimatePresence, motion } from 'framer-motion';
import { useQueryState } from 'nuqs';
import { GiCheckMark } from 'react-icons/gi';
import { LuFastForward } from 'react-icons/lu';
import type { ButtonHTMLAttributes, CSSProperties } from 'react';
import type { StepperProps, StepTuple } from './stepper-types';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
import { StepperContextProvider, useStepper } from './useStepper';
const BaseLine = ({ width }: { width: CSSProperties['width'] }) => (
<div className="bg-muted-foreground absolute left-1/2 top-1/2 h-[4px] -translate-y-1/2" style={{ width }} />
);
const ActiveLine = (props: { width: CSSProperties['width'] }) => (
<motion.div
animate={{ width: props.width }}
className="bg-primary/50 absolute left-1/2 top-1/2 h-[4px] -translate-y-1/2"
/>
);
const CompletedLine = (props: { width: CSSProperties['width'] }) => (
<motion.div
animate={{ width: props.width }}
className="bg-primary absolute left-1/2 top-1/2 h-[4px] -translate-y-1/2"
/>
);
/**
* A stepper component that allows you to create a step-by-step process for example a form.
*/
const Stepper = ({ steps, className, children, t, queryKey = 'stepper', ...restOfProps }: StepperProps) => {
const [activeStepTuple, setActiveStepTuple] = useQueryState(queryKey, {
parse: (query: string) => query.split(',').map(Number) as StepTuple,
serialize: value => value.join(','),
history: 'push',
defaultValue: [0, 0] as StepTuple,
});
const activeStep = activeStepTuple[0];
const direction = activeStepTuple[1];
const goToNextStep = () => {
// Check if the current active step is not the last step
if (activeStep < steps.length - 1) {
let nextStep = activeStep + 1;
// Skip steps that are marked to be skipped
while (nextStep < steps.length && steps[nextStep]?.skip) {
nextStep += 1;
}
// If the next step is valid, update the active step tuple
if (nextStep < steps.length) {
setActiveStepTuple([nextStep, 1]);
}
}
};
const goToPreviousStep = (step?: number) => {
// Check if a specific step is provided and valid
if (step !== undefined && step >= 0 && step < activeStep) {
// Skip any steps that are marked to be skipped
while (step >= 0 && steps[step]?.skip) {
step -= 1;
}
// If a valid previous step is found, update the active step tuple
if (step >= 0) {
setActiveStepTuple([step, -1]);
}
} else if (activeStep > 0) {
// If no specific step is provided, move to the previous step
let previousStep = activeStep - 1;
// Skip any steps that are marked to be skipped
while (previousStep >= 0 && steps[previousStep]?.skip) {
previousStep -= 1;
}
// If a valid previous step is found, update the active step tuple
if (previousStep >= 0) {
setActiveStepTuple([previousStep, -1]);
}
}
};
return (
<StepperContextProvider
value={{
activeStepTuple,
goToNextStep,
goToPreviousStep,
setActiveStepTuple,
t,
}}>
<div className={cn('flex flex-col gap-8', className)} {...restOfProps}>
<Alert className="overflow-hidden">
<AnimatePresence custom={direction} initial={false} mode="wait">
<motion.div key={steps[activeStep]!.title} custom={direction} {...animationSlideInOut}>
<AlertTitle className="text-balance text-center text-2xl">{steps[activeStep]!.title}</AlertTitle>
<AlertDescription className="text-balance text-center">{steps[activeStep]!.description}</AlertDescription>
</motion.div>
</AnimatePresence>
</Alert>
<div className="flex justify-between">
{steps.map((_, index) => {
const isCompleted = index < activeStep;
return (
<div
key={index}
className={cn('group relative flex grow justify-center', [
index === activeStep && 'active',
isCompleted && 'completed',
])}>
{index === 0 && (
<>
<BaseLine width={`${100 * (steps.length - 1)}%`} />
<ActiveLine width={`${100 * activeStep}%`} />
<CompletedLine width={`${100 * (activeStep <= 0 ? 0 : activeStep - 1)}%`} />
</>
)}
<StepButton
skipped={steps[index]?.skip}
disabled={index >= activeStep}
delayed={index === activeStep}
onClick={() => {
goToPreviousStep(index);
}}>
{index + 1}
</StepButton>
</div>
);
})}
</div>
</div>
<div>
<AnimatePresence custom={direction} initial={false} mode="wait">
<motion.div key={activeStep} custom={direction} {...animationFade}>
<div className="py-0">{steps[activeStep]?.component}</div>
</motion.div>
</AnimatePresence>
{children}
</div>
</StepperContextProvider>
);
};
interface StepButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
delayed?: boolean;
skipped?: boolean;
}
const StepButton = ({ children, skipped, delayed, className, ...restOfProps }: StepButtonProps) => {
const { t } = useStepper();
if (skipped)
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
{...restOfProps}
className={cn(
'bg-primary z-10 grid h-[40px] w-[40px] cursor-not-allowed place-items-center rounded-full',
className,
)}>
<LuFastForward className="fill-primary-foreground stroke-primary-foreground h-6 w-6" />
</button>
</TooltipTrigger>
<TooltipContent>{t?.('skip') || 'Deze stap is overgeslagen.'}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
return (
<button
{...restOfProps}
className={cn(
delayed && 'delay-500',
'bg-muted relative z-10 grid h-[40px] w-[40px] place-items-center rounded-full transition-all',
'text-muted-foreground font-bold',
'group/button',
'ring-primary ring-offset-background/90 group-[.active]:bg-primary group-[.active]:text-primary-foreground ring-offset-4 group-[.active]:ring-2',
'group-[.completed]:bg-primary group-[.completed]:text-primary-foreground',
className,
)}
type="button">
<GiCheckMark
className={cn(
'absolute grid place-items-center opacity-0 transition-opacity',
'group-[.completed]:opacity-100 group-has-[button:hover,button:focus]:opacity-0',
)}
/>
<span
className={cn(
'transition-opacity',
'group-[.completed]:opacity-0 group-has-[button:hover,button:focus]:opacity-100',
)}>
{children}
</span>
</button>
);
};
export { Stepper }; useStepper.ts'use client';
import { createContext, useContext } from 'react';
import type { StepTuple } from './stepper-types';
export type STEPPER_TRANSLATION_KEYS = 'skip';
interface StepperContextType {
goToNextStep: () => void;
goToPreviousStep: (step?: number) => void;
setActiveStepTuple: (value: StepTuple) => void;
activeStepTuple: StepTuple;
t?: (key: STEPPER_TRANSLATION_KEYS) => string;
}
const StepperContext = createContext<StepperContextType | null>(null);
export const useStepper = () => {
const context = useContext(StepperContext);
if (!context) {
throw new Error('useStepper must be used within a Stepper');
}
return context;
};
export const StepperContextProvider = StepperContext.Provider; Usage'use client';
import { Stepper } from '@repo/ui/stepper';
import { div } from 'framer-motion/m';
import { useTranslations } from 'next-intl';
import { useQueryState } from 'nuqs';
import { useShallow } from 'zustand/react/shallow';
import { useEffect, useMemo } from 'react';
import type { OrderType } from '@/types/models/order';
import type { StepperProps, StepTuple } from '@repo/ui/stepper-types';
import { ReturnStep4RequestLabel } from '@/app/[slug]/return/components/return-steps/return-step-4-request-label';
import { useOrganisation } from '@/providers/organisation-provider';
import { useReturnFormStore } from '@/stores/return-form-store';
import { ReturnStep1SelectOrderItemsForm } from './return-steps/return-step-1-select-order-items-form';
import { ReturnStep2SelectReasonsForm } from './return-steps/return-step-2-select-reasons-form';
export const ReturnStepper = ({ order }: { order: OrderType }) => {
const {
organisation: {
return_settings: { reasons },
},
} = useOrganisation();
const t = useTranslations('ReturnStepper');
const [setOrder, setBillingCustomer] = useReturnFormStore(
useShallow(state => [state.setOrder, state.setBillingCustomer]),
);
useEffect(() => {
if (!useReturnFormStore.persist.hasHydrated()) useReturnFormStore.persist.rehydrate();
setOrder(order);
setBillingCustomer(order.destination);
}, [order]);
const steps: StepperProps['steps'] = useMemo(
() => [
{
title: t('step1.title'),
description: t('step1.description'),
component: (
<div className="container max-w-screen-md">
<ReturnStep1SelectOrderItemsForm order={order} />
</div>
),
skip: !order?.order_items?.length,
},
{
title: t('step2.title'),
description: t('step2.description'),
component: (
<div className="container max-w-screen-lg">
<ReturnStep2SelectReasonsForm reasons={reasons} />
</div>
),
},
// {
// title: t('step3.title'),
// description: t('step3.description'),
//
// component: <ReturnStep3SelectLocation order={order} />,
// },
{
title: t('step3.title'),
description: t('step3.description'),
component: (
<div className="container max-w-screen-lg">
<ReturnStep4RequestLabel />
</div>
),
},
],
[],
);
return <Stepper className="container mb-8 max-w-screen-md" steps={steps} t={t} />;
}; |
Just a small update to put the community at ease: I'm working on this, but a bit slower than it could be as I'm having very busy weeks with my work. I hope to have everything this week for both the ‘@stepperize/react’ library and shadcn 🤝 |
@SanderCokart I want to reassure you that I will be refactoring this PR using my new library @stepperize/react. In the next few days there will be a big update of just documentation to make everything even clearer and more examples, but I think with that documentation you can achieve some great things. |
Update --> New docs and improvements in the stepperize API! --> https://stepperize.vercel.app/ Next step (finally!) --> using the library in this PR to create the primitives for Shadcn. Again I apologize for my delay, I've had a busy few weeks and couldn't dedicate the time I needed to all of this |
Thanks Damian, really looking forward to testing this! |
@SanderCokart This is a compatibility issue mentioned in the Stackblitz docs. I could add some flag indicating that maybe |
Building the API for Shadcn UI using |
@damianricobelli Any idea when it will be available? |
@yah-23 not yet, I'm trying to find some free time in my week to be able to continue with the API. A lot of workload unfortunately. But I'm working on it |
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:
Here is a complete video of the different use cases:
Grabacion.de.pantalla.2023-05-08.a.la.s.13.14.45.mov