import { useResponsiveTokens } from '../../../hooks/useResponsiveTokens' import { StepperV2TokensType } from '../stepperV2.tokens' import { Check, Lock, ChevronDown } from 'lucide-react' import Text from '../../Text/Text' import { composeRefs, toPixels } from '../../../global-utils/GlobalUtils' import VerticalLineV2 from './VerticalLineV2' import { forwardRef, useEffect, useMemo, useRef, useState } from 'react' import { StepperV2StepProps, StepperV2StepStatus, StepperV2Type, } from '../stepperV2.types' import Block from '../../Primitives/Block/Block' import { getStepState } from '../utils' import { StepStatusCircle } from './StepStatusCircle' import { StepsHorizontalBody } from './StepsHorizontalBody' import { StepsVerticalSubstepRails } from './StepsVerticalSubstepRails' import { StepsSubstepList } from './StepsSubstepList' export const Steps = forwardRef< HTMLDivElement, StepperV2StepProps & { onKeyDown?: (event: React.KeyboardEvent, stepIndex: number) => void stepperInstanceId: string stepperType: StepperV2Type stepsLength: number } >( ( { step, stepIndex, stepsLength, isCompleted, isCurrent, isLast, onClick, onSubstepClick, clickable, onKeyDown, stepperInstanceId, stepperType, }, ref ) => { const isHorizontal = stepperType === StepperV2Type.HORIZONTAL const stepperTokens = useResponsiveTokens('STEPPERV2') const verticalLineRef = useRef(null) const descriptionRef = useRef(null) const stepContentRef = useRef(null) const setStepContentRef = useMemo( () => composeRefs(stepContentRef, ref), [ref] ) const [verticalLineHeight, setVerticalLineHeight] = useState(0) const [descriptionHeight, setDescriptionHeight] = useState(0) const hasSubsteps = !!step?.substeps && step.substeps.length > 0 const isExpandable = step.isExpandable ?? hasSubsteps const [isExpanded, setIsExpanded] = useState( step.isExpanded ?? hasSubsteps ) const focusTimeoutRef = useRef | null>( null ) useEffect(() => { if (step.isExpanded !== undefined) { setIsExpanded(step.isExpanded) } }, [step.isExpanded]) useEffect(() => { if (!isExpanded) { setVerticalLineHeight(0) setDescriptionHeight(0) return } const animationFrameId = requestAnimationFrame(() => { setVerticalLineHeight( verticalLineRef.current?.clientHeight || 0 ) setDescriptionHeight(descriptionRef.current?.clientHeight || 0) }) return () => cancelAnimationFrame(animationFrameId) }, [isExpanded, hasSubsteps, step.substeps?.length]) useEffect(() => { return () => { if (focusTimeoutRef.current) { clearTimeout(focusTimeoutRef.current) } } }, []) const stepState = getStepState(step, isCompleted, isCurrent) const isClickable = Boolean(clickable && !step.disabled && onClick) const substepRefs = useRef<(HTMLDivElement | null)[]>([]) const [focusedSubstepIndex, setFocusedSubstepIndex] = useState< number | null >(null) const toggleExpand = () => { if (!isExpandable) return setIsExpanded((v) => { const newExpanded = !v if (!newExpanded) { setFocusedSubstepIndex(null) } return newExpanded }) } const handleClick = () => { if (isClickable) { onClick!(stepIndex) } } const focusFirstSubstep = () => { if ( step.substeps && step.substeps.length > 0 && !step.substeps[0]?.disabled && substepRefs.current[0] ) { setFocusedSubstepIndex(0) substepRefs.current[0]?.focus() } } const handleKeyDown = (event: React.KeyboardEvent) => { if (!isClickable) return if ( isExpanded && hasSubsteps && focusedSubstepIndex !== null && step.substeps ) { switch (event.key) { case 'ArrowDown': { event.preventDefault() const nextSubstepIndex = Math.min( focusedSubstepIndex + 1, step.substeps.length - 1 ) if ( !step.substeps[nextSubstepIndex]?.disabled && substepRefs.current[nextSubstepIndex] ) { setFocusedSubstepIndex(nextSubstepIndex) substepRefs.current[nextSubstepIndex]?.focus() } return } case 'ArrowUp': { event.preventDefault() const prevSubstepIndex = Math.max( focusedSubstepIndex - 1, 0 ) if ( !step.substeps[prevSubstepIndex]?.disabled && substepRefs.current[prevSubstepIndex] ) { setFocusedSubstepIndex(prevSubstepIndex) substepRefs.current[prevSubstepIndex]?.focus() } return } case 'ArrowLeft': case 'ArrowRight': event.preventDefault() setFocusedSubstepIndex(null) stepContentRef.current?.focus() return case 'Enter': case ' ': event.preventDefault() if ( onSubstepClick && step.substeps[focusedSubstepIndex] && !step.substeps[focusedSubstepIndex].disabled ) { onSubstepClick(stepIndex, focusedSubstepIndex) } return } } switch (event.key) { case 'Enter': case ' ': event.preventDefault() onClick!(stepIndex) break case 'ArrowRight': // Substep navigation only applies in vertical mode if (!isHorizontal && hasSubsteps && !isExpanded) { event.preventDefault() toggleExpand() focusTimeoutRef.current = setTimeout( () => focusFirstSubstep(), 100 ) return } if (!isHorizontal && hasSubsteps && isExpanded) { event.preventDefault() focusFirstSubstep() return } onKeyDown?.(event, stepIndex) break case 'ArrowLeft': onKeyDown?.(event, stepIndex) break case 'ArrowDown': case 'ArrowUp': case 'Home': case 'End': onKeyDown?.(event, stepIndex) break } } const renderStepIcon = () => { if (step.icon) return step.icon switch (stepState) { case StepperV2StepStatus.COMPLETED: return (