improvement(tour): remove auto-start, only trigger on explicit user action (#3823)

This commit is contained in:
Waleed
2026-03-27 19:59:47 -07:00
committed by GitHub
parent d2c3c1c39e
commit eac41ca105
3 changed files with 8 additions and 86 deletions

View File

@@ -16,7 +16,6 @@ const Joyride = dynamic(() => import('react-joyride'), {
ssr: false,
})
const NAV_TOUR_STORAGE_KEY = 'sim-nav-tour-completed-v1'
export const START_NAV_TOUR_EVENT = 'start-nav-tour'
export function NavTour() {
@@ -25,9 +24,6 @@ export function NavTour() {
const { run, stepIndex, tourKey, isTooltipVisible, isEntrance, handleCallback } = useTour({
steps: navTourSteps,
storageKey: NAV_TOUR_STORAGE_KEY,
autoStartDelay: 1200,
resettable: true,
triggerEvent: START_NAV_TOUR_EVENT,
tourName: 'Navigation tour',
disabled: isWorkflowPage,

View File

@@ -12,17 +12,11 @@ const FADE_OUT_MS = 80
interface UseTourOptions {
/** Tour step definitions */
steps: Step[]
/** localStorage key for completion persistence */
storageKey: string
/** Delay before auto-starting the tour (ms) */
autoStartDelay?: number
/** Whether this tour can be reset/retriggered */
resettable?: boolean
/** Custom event name to listen for manual triggers */
triggerEvent?: string
/** Identifier for logging */
tourName?: string
/** When true, suppresses auto-start (e.g. to avoid overlapping with another active tour) */
/** When true, stops a running tour (e.g. navigating away from the relevant page) */
disabled?: boolean
}
@@ -41,49 +35,14 @@ interface UseTourReturn {
handleCallback: (data: CallBackProps) => void
}
function isTourCompleted(storageKey: string): boolean {
try {
return localStorage.getItem(storageKey) === 'true'
} catch {
return false
}
}
function markTourCompleted(storageKey: string): void {
try {
localStorage.setItem(storageKey, 'true')
} catch {
logger.warn('Failed to persist tour completion', { storageKey })
}
}
function clearTourCompletion(storageKey: string): void {
try {
localStorage.removeItem(storageKey)
} catch {
logger.warn('Failed to clear tour completion', { storageKey })
}
}
/**
* Tracks which tours have already attempted auto-start in this page session.
* Module-level so it survives component remounts (e.g. navigating between
* workflows remounts WorkflowTour), while still resetting on full page reload.
*/
const autoStartAttempted = new Set<string>()
/**
* Shared hook for managing product tour state with smooth transitions.
*
* Handles auto-start on first visit, localStorage persistence,
* manual triggering via custom events, and coordinated fade
* Handles manual triggering via custom events and coordinated fade
* transitions between steps to prevent layout shift.
*/
export function useTour({
steps,
storageKey,
autoStartDelay = 1200,
resettable = false,
triggerEvent,
tourName = 'tour',
disabled = false,
@@ -94,15 +53,10 @@ export function useTour({
const [isTooltipVisible, setIsTooltipVisible] = useState(true)
const [isEntrance, setIsEntrance] = useState(true)
const disabledRef = useRef(disabled)
const retriggerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const transitionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const rafRef = useRef<number | null>(null)
useEffect(() => {
disabledRef.current = disabled
}, [disabled])
/**
* Schedules a two-frame rAF to reveal the tooltip after the browser
* finishes repositioning. Stores the outer frame ID in `rafRef` so
@@ -137,8 +91,7 @@ export function useTour({
setRun(false)
setIsTooltipVisible(true)
setIsEntrance(true)
markTourCompleted(storageKey)
}, [storageKey, cancelPendingTransitions])
}, [cancelPendingTransitions])
/** Transition to a new step with a coordinated fade-out/fade-in */
const transitionToStep = useCallback(
@@ -164,40 +117,17 @@ export function useTour({
/** Stop the tour when disabled becomes true (e.g. navigating away from the relevant page) */
useEffect(() => {
if (disabled && run) {
cancelPendingTransitions()
setRun(false)
setIsTooltipVisible(true)
setIsEntrance(true)
stopTour()
logger.info(`${tourName} paused — disabled became true`)
}
}, [disabled, run, tourName, cancelPendingTransitions])
/** Auto-start on first visit (once per page session per tour) */
useEffect(() => {
if (disabled || autoStartAttempted.has(storageKey) || isTourCompleted(storageKey)) return
const timer = setTimeout(() => {
if (disabledRef.current) return
autoStartAttempted.add(storageKey)
setStepIndex(0)
setIsEntrance(true)
setIsTooltipVisible(false)
setRun(true)
logger.info(`Auto-starting ${tourName}`)
scheduleReveal()
}, autoStartDelay)
return () => clearTimeout(timer)
}, [disabled, storageKey, autoStartDelay, tourName, scheduleReveal])
}, [disabled, run, tourName, stopTour])
/** Listen for manual trigger events */
useEffect(() => {
if (!triggerEvent || !resettable) return
if (!triggerEvent) return
const handleTrigger = () => {
setRun(false)
clearTourCompletion(storageKey)
setTourKey((k) => k + 1)
if (retriggerTimerRef.current) {
@@ -222,7 +152,7 @@ export function useTour({
clearTimeout(retriggerTimerRef.current)
}
}
}, [triggerEvent, resettable, storageKey, tourName, scheduleReveal])
}, [triggerEvent, tourName, scheduleReveal])
/** Clean up all pending async work on unmount */
useEffect(() => {

View File

@@ -15,19 +15,15 @@ const Joyride = dynamic(() => import('react-joyride'), {
ssr: false,
})
const WORKFLOW_TOUR_STORAGE_KEY = 'sim-workflow-tour-completed-v1'
export const START_WORKFLOW_TOUR_EVENT = 'start-workflow-tour'
/**
* Workflow tour that covers the canvas, blocks, copilot, and deployment.
* Runs on first workflow visit and can be retriggered via "Take a tour".
* Triggered via "Take a tour" in the sidebar menu.
*/
export function WorkflowTour() {
const { run, stepIndex, tourKey, isTooltipVisible, isEntrance, handleCallback } = useTour({
steps: workflowTourSteps,
storageKey: WORKFLOW_TOUR_STORAGE_KEY,
autoStartDelay: 800,
resettable: true,
triggerEvent: START_WORKFLOW_TOUR_EVENT,
tourName: 'Workflow tour',
})