Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import type React from "react";
import { useEffect, useContext, useCallback } from "react";
import JoyRide, { ACTIONS, EVENTS, STATUS, type BaseProps } from "react-joyride";
import { TourContext, userTourStateReducer } from "./TourProvider";
 
interface TourProps extends BaseProps {
  /** Unique identifier for the tour. Should match the name of the tour in the product tour YAML file (note that my-tour-name can match my_tour_name.yml) */
  name: string;
  /** Whether to automatically start the tour when component mounts. Defaults to false */
  autoStart?: boolean;
  /** Delay in milliseconds before the tour starts when autoStart is true. Defaults to 0 */
  delay?: number;
 
  /* Check all other props here https://docs.react-joyride.com/props */
}
 
export const Tour: React.FC<TourProps> = ({ name, autoStart = false, delay = 0, ...props }) => {
  const tourContext = useContext(TourContext);
  if (!tourContext) {
    console.error("Tour context not found");
    return null;
  }
  const [state, dispatch] = userTourStateReducer();
 
  useEffect(() => {
    if (tourContext) {
      tourContext.registerTour(name, dispatch);
 
      let timeout = null;
      if (autoStart) {
        timeout = setTimeout(() => {
          tourContext.startTour(name);
        }, delay);
      }
 
      return () => {
        if (timeout) {
          clearTimeout(timeout);
        }
        tourContext.unregisterTour(name);
      };
    }
  }, []);
 
  /**
   * Handles tour navigation and completion events
   * @param {Object} data Tour callback data
   * @param {string} data.action The action that triggered the callback
   *   Available actions:
   *   - ACTIONS.CLOSE: User closed the tour
   *   - ACTIONS.NEXT: User clicked next
   *   - ACTIONS.PREV: User clicked back
   *   - ACTIONS.RESET: Tour was reset
   *   - ACTIONS.SKIP: User skipped the tour
   *   - ACTIONS.START: Tour started
   *   - ACTIONS.STOP: Tour stopped
   * @param {number} data.index Current step index
   * @param {string} data.type Event type
   *   Available events:
   *   - EVENTS.STEP_AFTER: After a step is completed
   *   - EVENTS.STEP_BEFORE: Before a step starts
   *   - EVENTS.TARGET_NOT_FOUND: Step target element not found
   *   - EVENTS.TOUR_START: Tour started
   *   - EVENTS.TOUR_END: Tour ended
   * @param {string} data.status Tour status
   *   Available statuses:
   *   - STATUS.IDLE: Tour is idle/not started
   *   - STATUS.RUNNING: Tour is running
   *   - STATUS.PAUSED: Tour is paused
   *   - STATUS.SKIPPED: Tour was skipped
   *   - STATUS.FINISHED: Tour completed normally
   *   - STATUS.ERROR: Tour encountered an error
   *
   * This handler manages:
   * - Tour completion (close/skip/finish) by marking it viewed and stopping
   * - Step navigation (next/prev) by updating the step index
   *
   * Can be extended to support:
   * - Conditional step logic based on user interactions
   * - Saving progress/state between sessions
   * - Custom analytics tracking for each step
   * - Dynamic step content based on application state
   */
  const handleTourCallback = useCallback(
    (data: {
      action: string;
      index: number;
      type: string;
      status: string;
    }) => {
      const { action, index, type, status } = data;
 
      // tour ends when
      const shouldEndTour =
        (status === STATUS.SKIPPED && state.run) || action === ACTIONS.CLOSE || status === STATUS.FINISHED;
 
      if (shouldEndTour) {
        // mark tour as viewed and update onboarding state if it's the final step or the tour was skipped
        if (status === STATUS.SKIPPED || status === STATUS.FINISHED) {
          tourContext?.setTourViewed(name, status === STATUS.SKIPPED, { index, action, type, status });
        }
        dispatch({ type: "STOP" });
        return;
      }
 
      const isStepChange = type === EVENTS.STEP_AFTER || type === EVENTS.TARGET_NOT_FOUND;
      if (isStepChange) {
        const nextIndex = index + (action === ACTIONS.PREV ? -1 : 1);
        dispatch({
          type: "GOTO",
          payload: { stepIndex: nextIndex },
        });
      }
    },
    [name, state.run],
  );
 
  const { key, ...joyrideState } = state;
 
  // Disable tours when running in Cypress tests
  const isCypressTest = typeof window !== "undefined" && !!(window as any).Cypress;
  const shouldRunTour = !isCypressTest && joyrideState.run;
 
  return state.steps.length > 0 ? (
    <JoyRide
      key={key}
      {...joyrideState}
      run={shouldRunTour}
      {...props}
      callback={handleTourCallback}
      styles={{
        tooltip: {
          width: "468px",
        },
        options: {
          backgroundColor: "var(--color-neutral-background)",
          primaryColor: "var(--color-primary-surface)",
          textColor: "var(--color-neutral-content)",
          overlayColor: "rgba(var(--color-neutral-shadow-raw) / calc( 50% * var(--shadow-intensity)))",
          arrowColor: "var(--color-primary-surface)",
        },
      }}
      hideCloseButton={true}
    />
  ) : null;
};