import { useEffect, useRef } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import StepCard from "./StepCard"; import { useOnboarding } from "../../contexts/OnboardingContext"; import { cardNext, cardStep, readOnboardingNavState, showsStepCard, type StepCardModel, } from "../../utils/onboardingPath"; import { VISIT_STEP_KEYS } from "../../shared/types"; export interface OnboardingStepCardProps { /** The page's card model; null for no card yet (the page's own data is not read). */ model: StepCardModel | null; } /** * The step card of a page (#398), connected to the getting-started path. Each * page that carries a card renders it as its first child, before its header, * and passes only its model; this component reads `useOnboarding()`, the only * one of the card's pieces to do so. * * - It renders only in card mode, latched on the current route: the latch, * not the next step, keeps it on screen once the page's steps are done. * - After an arrival by "Next step" (`readOnboardingNavState(location.state) * .focus`), it moves the focus to its title once the card is on screen, * then consumes the navigation state, once per arrival: StrictMode runs the * effect twice on mount, a guard on the location key keeps it to one focus * and one replace. An ordinary visit carries no state and does nothing. * - It wires "Skip this step" to the step it shows, "Got it" to * `acknowledge` when that step is a visit step (#425), "Close" to `hide`, * and the next step's link to the path's next step. * - Its rank "Step n of N" and its end wait for the Budget entitlement and * the visit steps' initialization (#425): N depends on both. */ export default function OnboardingStepCard({ model }: OnboardingStepCardProps) { const onboarding = useOnboarding(); const location = useLocation(); const navigate = useNavigate(); const titleRef = useRef(null); const focusedFor = useRef(null); const { pathname } = location; const current = onboarding.checklist ? cardStep(onboarding.checklist, pathname) : null; const shown = model !== null && current !== null && showsStepCard(onboarding.mode, onboarding.cardPage, pathname); const focusRequested = readOnboardingNavState(location.state).focus; useEffect(() => { if (!shown || !focusRequested || focusedFor.current === location.key) return; focusedFor.current = location.key; titleRef.current?.focus(); navigate(pathname, { replace: true, state: null }); }, [shown, focusRequested, location.key, pathname, navigate]); if (!shown) return null; const { skip, acknowledge, hide } = onboarding; const totalKnown = onboarding.budgetReady && onboarding.visitStepsInitialized; const isVisitStep = VISIT_STEP_KEYS.includes(current.step.key); return ( void skip(current.step.key)} onAcknowledge={isVisitStep ? () => void acknowledge(current.step.key) : undefined} onHide={() => void hide()} titleRef={titleRef} /> ); }