stack/apps/dashboard/src/components/stepper.tsx
BilalG1 4409996381
stepper ui fix (#881)
<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->

<!-- ELLIPSIS_HIDDEN -->

----

> [!IMPORTANT]
> Fix dimension calculation in `Stepper` component by using
`offsetWidth` and `offsetHeight`.
> 
>   - **UI Fix**:
> - In `Stepper` component, replace `getBoundingClientRect()` with
`offsetWidth` and `offsetHeight` for dimension calculation in
`useEffect`.
>     - Affects how dimensions are set in `setDimensions()` function.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 4c5673a350. You can
[customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this
summary. It will automatically update as commits are pushed.</sup>

<!-- ELLIPSIS_HIDDEN -->

<!-- RECURSEML_SUMMARY:START -->
## Review by RecurseML

_🔍 Review performed on
[9318e2b..4c5673a](9318e2b6ce...4c5673a350)_

 No bugs found, your code is sparkling clean

<details>
<summary> Files analyzed, no issues (1)</summary>

  • `apps/dashboard/src/components/stepper.tsx`
</details>

[![Need help? Join our
Discord](https://img.shields.io/badge/Need%20help%3F%20Join%20our%20Discord-5865F2?style=plastic&logo=discord&logoColor=white)](https://discord.gg/n3SsVDAW6U)
<!-- RECURSEML_SUMMARY:END -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Bug Fixes
- Improved Stepper sizing to accurately reflect element layout, reducing
misalignment, clipping, and overflow in various layouts and themes.
- Increased stability during window/container resizes, minimizing visual
jitter and reflow glitches.

- Performance
- More efficient measurement path for Stepper dimensions, reducing
unnecessary calculations while preserving responsive updates.

- Style
- Subtle visual consistency improvements from more precise width/height
handling, leading to cleaner alignment and spacing across steps.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
2025-09-09 19:21:48 -07:00

147 lines
3.9 KiB
TypeScript

"use client";
import { cn } from "@/lib/utils";
import React, { createContext, useContext, useEffect, useRef, useState } from "react";
type StepperContextType = {
currentStep: number,
totalSteps: number,
goToStep: (step: number) => void,
nextStep: () => void,
previousStep: () => void,
direction: 'forward' | 'backward',
};
const StepperContext = createContext<StepperContextType | null>(null);
export function useStepperContext() {
const context = useContext(StepperContext);
if (!context) {
throw new Error("useStepperContext must be used within a Stepper");
}
return context;
}
type StepperProps = {
children: React.ReactNode,
currentStep: number,
onStepChange: (step: number) => void,
className?: string,
};
export function Stepper({ children, currentStep, onStepChange, className }: StepperProps) {
const [direction, setDirection] = useState<'forward' | 'backward'>('forward');
const [dimensions, setDimensions] = useState<{ width: number, height: number }>({ width: 0, height: 0 });
const containerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const previousStepRef = useRef(currentStep);
const childrenArray = React.Children.toArray(children);
const totalSteps = childrenArray.length;
useEffect(() => {
if (currentStep > previousStepRef.current) {
setDirection('forward');
} else if (currentStep < previousStepRef.current) {
setDirection('backward');
}
previousStepRef.current = currentStep;
}, [currentStep]);
useEffect(() => {
const updateDimensions = () => {
if (contentRef.current) {
setDimensions({
width: contentRef.current.offsetWidth,
height: contentRef.current.offsetHeight,
});
}
};
updateDimensions();
// Use ResizeObserver for smooth size transitions
const resizeObserver = new ResizeObserver(updateDimensions);
if (contentRef.current) {
resizeObserver.observe(contentRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, [currentStep]);
const goToStep = (step: number) => {
if (step >= 0 && step < totalSteps) {
onStepChange(step);
}
};
const nextStep = () => {
goToStep(currentStep + 1);
};
const goToPreviousStep = () => {
goToStep(currentStep - 1);
};
const contextValue: StepperContextType = {
currentStep,
totalSteps,
goToStep,
nextStep,
previousStep: goToPreviousStep,
direction,
};
return (
<StepperContext.Provider value={contextValue}>
<div
ref={containerRef}
className={cn("relative overflow-hidden transition-all duration-300 ease-in-out", className)}
style={{
width: dimensions.width || 'auto',
height: dimensions.height || 'auto',
}}
>
<div className="relative">
{childrenArray.map((child, index) => (
<div
key={index}
ref={index === currentStep ? contentRef : undefined}
className={cn(
"transition-all duration-300 ease-in-out",
index === currentStep ? "" : "absolute inset-0 pointer-events-none"
)}
style={{
opacity: index === currentStep ? 1 : 0,
transform: index === currentStep
? 'translateX(0)'
: index < currentStep
? 'translateX(-20px)'
: 'translateX(20px)',
}}
>
{index === currentStep && child}
</div>
))}
</div>
</div>
</StepperContext.Provider>
);
}
type StepperPageProps = {
children: React.ReactNode,
className?: string,
};
export function StepperPage({ children, className }: StepperPageProps) {
return (
<div className={cn("w-full", className)}>
{children}
</div>
);
}