@nextsparkjs/plugin-walkme 0.1.0-beta.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.env.example +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +625 -0
  4. package/components/WalkmeBeacon.tsx +64 -0
  5. package/components/WalkmeControls.tsx +111 -0
  6. package/components/WalkmeModal.tsx +144 -0
  7. package/components/WalkmeOverlay.tsx +107 -0
  8. package/components/WalkmeProgress.tsx +53 -0
  9. package/components/WalkmeProvider.tsx +674 -0
  10. package/components/WalkmeSpotlight.tsx +188 -0
  11. package/components/WalkmeTooltip.tsx +152 -0
  12. package/examples/basic-tour.ts +38 -0
  13. package/examples/conditional-tour.ts +56 -0
  14. package/examples/cross-window-tour.ts +54 -0
  15. package/hooks/useTour.ts +52 -0
  16. package/hooks/useTourProgress.ts +38 -0
  17. package/hooks/useTourState.ts +146 -0
  18. package/hooks/useWalkme.ts +52 -0
  19. package/jest.config.cjs +27 -0
  20. package/lib/conditions.ts +113 -0
  21. package/lib/core.ts +323 -0
  22. package/lib/plugin-env.ts +87 -0
  23. package/lib/positioning.ts +172 -0
  24. package/lib/storage.ts +203 -0
  25. package/lib/targeting.ts +186 -0
  26. package/lib/triggers.ts +127 -0
  27. package/lib/validation.ts +122 -0
  28. package/messages/en.json +21 -0
  29. package/messages/es.json +21 -0
  30. package/package.json +18 -0
  31. package/plugin.config.ts +26 -0
  32. package/providers/walkme-context.ts +17 -0
  33. package/tests/lib/conditions.test.ts +172 -0
  34. package/tests/lib/core.test.ts +514 -0
  35. package/tests/lib/positioning.test.ts +43 -0
  36. package/tests/lib/storage.test.ts +232 -0
  37. package/tests/lib/targeting.test.ts +191 -0
  38. package/tests/lib/triggers.test.ts +198 -0
  39. package/tests/lib/validation.test.ts +249 -0
  40. package/tests/setup.ts +52 -0
  41. package/tests/tsconfig.json +32 -0
  42. package/tsconfig.json +47 -0
  43. package/types/walkme.types.ts +316 -0
@@ -0,0 +1,674 @@
1
+ 'use client'
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ReactNode,
10
+ } from 'react'
11
+ import { usePathname, useRouter } from 'next/navigation'
12
+ import { WalkmeContext } from '../providers/walkme-context'
13
+ import { useTourState } from '../hooks/useTourState'
14
+ import { validateTours } from '../lib/validation'
15
+ import { shouldTriggerTour, type TriggerEvaluationContext } from '../lib/triggers'
16
+ import { evaluateConditions } from '../lib/conditions'
17
+ import {
18
+ getActiveTour,
19
+ getActiveStep,
20
+ isFirstStep,
21
+ isLastStep,
22
+ } from '../lib/core'
23
+ import { findTarget, waitForTarget, scrollToElement } from '../lib/targeting'
24
+ import type {
25
+ WalkmeProviderProps,
26
+ WalkmeContextValue,
27
+ TourEvent,
28
+ ConditionContext,
29
+ WalkmeLabels,
30
+ } from '../types/walkme.types'
31
+ import { WalkmeOverlay } from './WalkmeOverlay'
32
+ import { WalkmeTooltip } from './WalkmeTooltip'
33
+ import { WalkmeModal } from './WalkmeModal'
34
+ import { WalkmeSpotlight } from './WalkmeSpotlight'
35
+ import { WalkmeBeacon } from './WalkmeBeacon'
36
+
37
+ /**
38
+ * WalkMe Provider Component
39
+ *
40
+ * Wraps the application (or a section) to provide guided tour functionality.
41
+ * Manages tour state, trigger evaluation, cross-page navigation, and rendering.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * <WalkmeProvider tours={[introTour, featureTour]} autoStart>
46
+ * <App />
47
+ * </WalkmeProvider>
48
+ * ```
49
+ */
50
+ const DEFAULT_LABELS: WalkmeLabels = {
51
+ next: 'Next',
52
+ prev: 'Previous',
53
+ skip: 'Skip',
54
+ complete: 'Complete',
55
+ close: 'Close',
56
+ progress: 'Step {current} of {total}',
57
+ tourAvailable: 'Tour available',
58
+ }
59
+
60
+ export function WalkmeProvider({
61
+ tours: rawTours,
62
+ children,
63
+ debug = false,
64
+ autoStart = true,
65
+ autoStartDelay = 1000,
66
+ persistState = true,
67
+ onTourStart,
68
+ onTourComplete,
69
+ onTourSkip,
70
+ onStepChange,
71
+ conditionContext: externalConditionContext,
72
+ labels: userLabels,
73
+ userId,
74
+ }: WalkmeProviderProps) {
75
+ const labels = useMemo<WalkmeLabels>(
76
+ () => ({ ...DEFAULT_LABELS, ...userLabels }),
77
+ [userLabels],
78
+ )
79
+
80
+ const pathname = usePathname()
81
+ const router = useRouter()
82
+ const prevPathRef = useRef(pathname)
83
+ const customEventsRef = useRef<Set<string>>(new Set())
84
+ const previousFocusRef = useRef<HTMLElement | null>(null)
85
+ const triggerTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
86
+ const [targetElement, setTargetElement] = useState<HTMLElement | null>(null)
87
+
88
+ // Validate tours on mount
89
+ const validatedTours = useMemo(() => {
90
+ const result = validateTours(rawTours)
91
+ if (!result.valid && debug) {
92
+ console.warn(
93
+ '[WalkMe] Some tours failed validation and were excluded:',
94
+ result.errors,
95
+ )
96
+ }
97
+ return result.validTours
98
+ }, [rawTours, debug])
99
+
100
+ // Core state management
101
+ const { state, dispatch, storage } = useTourState(validatedTours, {
102
+ persistState,
103
+ debug,
104
+ userId,
105
+ })
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Context value helpers
109
+ // ---------------------------------------------------------------------------
110
+
111
+ const startTour = useCallback(
112
+ (tourId: string) => {
113
+ const tour = state.tours[tourId]
114
+ if (!tour) {
115
+ if (debug) console.warn(`[WalkMe] Tour "${tourId}" not found`)
116
+ return
117
+ }
118
+
119
+ previousFocusRef.current = document.activeElement as HTMLElement | null
120
+ dispatch({ type: 'START_TOUR', tourId })
121
+
122
+ onTourStart?.({
123
+ type: 'tour_started',
124
+ tourId,
125
+ tourName: tour.name,
126
+ stepIndex: 0,
127
+ totalSteps: tour.steps.length,
128
+ timestamp: Date.now(),
129
+ })
130
+ },
131
+ [state.tours, dispatch, debug, onTourStart],
132
+ )
133
+
134
+ const pauseTour = useCallback(() => {
135
+ dispatch({ type: 'PAUSE_TOUR' })
136
+ }, [dispatch])
137
+
138
+ const resumeTour = useCallback(() => {
139
+ dispatch({ type: 'RESUME_TOUR' })
140
+ }, [dispatch])
141
+
142
+ const skipTour = useCallback(() => {
143
+ const tour = getActiveTour(state)
144
+ if (tour) {
145
+ onTourSkip?.({
146
+ type: 'tour_skipped',
147
+ tourId: tour.id,
148
+ tourName: tour.name,
149
+ stepIndex: state.activeTour?.currentStepIndex,
150
+ totalSteps: tour.steps.length,
151
+ timestamp: Date.now(),
152
+ })
153
+ tour.onSkip?.()
154
+ }
155
+ dispatch({ type: 'SKIP_TOUR' })
156
+ restoreFocus()
157
+ }, [state, dispatch, onTourSkip])
158
+
159
+ const completeTour = useCallback(() => {
160
+ const tour = getActiveTour(state)
161
+ if (tour) {
162
+ onTourComplete?.({
163
+ type: 'tour_completed',
164
+ tourId: tour.id,
165
+ tourName: tour.name,
166
+ totalSteps: tour.steps.length,
167
+ timestamp: Date.now(),
168
+ })
169
+ tour.onComplete?.()
170
+ }
171
+ dispatch({ type: 'COMPLETE_TOUR' })
172
+ restoreFocus()
173
+ }, [state, dispatch, onTourComplete])
174
+
175
+ const nextStep = useCallback(() => {
176
+ if (isLastStep(state)) {
177
+ completeTour()
178
+ } else {
179
+ dispatch({ type: 'NEXT_STEP' })
180
+ }
181
+ }, [state, dispatch, completeTour])
182
+
183
+ const prevStep = useCallback(() => {
184
+ dispatch({ type: 'PREV_STEP' })
185
+ }, [dispatch])
186
+
187
+ const goToStep = useCallback(
188
+ (stepIndex: number) => {
189
+ dispatch({ type: 'NAVIGATE_TO_STEP', stepIndex })
190
+ },
191
+ [dispatch],
192
+ )
193
+
194
+ const resetTour = useCallback(
195
+ (tourId: string) => {
196
+ dispatch({ type: 'RESET_TOUR', tourId })
197
+ },
198
+ [dispatch],
199
+ )
200
+
201
+ const resetAllTours = useCallback(() => {
202
+ dispatch({ type: 'RESET_ALL' })
203
+ }, [dispatch])
204
+
205
+ const isTourCompleted = useCallback(
206
+ (tourId: string) => state.completedTours.includes(tourId),
207
+ [state.completedTours],
208
+ )
209
+
210
+ const isTourActive = useCallback(
211
+ (tourId: string) => state.activeTour?.tourId === tourId,
212
+ [state.activeTour],
213
+ )
214
+
215
+ const emitEvent = useCallback(
216
+ (eventName: string) => {
217
+ customEventsRef.current.add(eventName)
218
+ // Re-evaluate triggers after event emission
219
+ evaluateTriggers()
220
+ },
221
+ [], // eslint-disable-line react-hooks/exhaustive-deps
222
+ )
223
+
224
+ function restoreFocus() {
225
+ if (previousFocusRef.current) {
226
+ previousFocusRef.current.focus()
227
+ previousFocusRef.current = null
228
+ }
229
+ }
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Trigger Evaluation
233
+ // ---------------------------------------------------------------------------
234
+
235
+ const evaluateTriggers = useCallback(() => {
236
+ if (!state.initialized || state.activeTour || !autoStart) return
237
+
238
+ const triggerContext: TriggerEvaluationContext = {
239
+ currentRoute: pathname,
240
+ visitCount: storage.getVisitCount(),
241
+ firstVisitDate: storage.getFirstVisitDate(),
242
+ completedTourIds: state.completedTours,
243
+ customEvents: customEventsRef.current,
244
+ }
245
+
246
+ const conditionCtx: ConditionContext = {
247
+ ...externalConditionContext,
248
+ completedTourIds: state.completedTours,
249
+ visitCount: storage.getVisitCount(),
250
+ firstVisitDate: storage.getFirstVisitDate() ?? undefined,
251
+ }
252
+
253
+ // Sort tours by priority (lower number = higher priority)
254
+ const sortedTours = Object.values(state.tours).sort(
255
+ (a, b) => (a.priority ?? 999) - (b.priority ?? 999),
256
+ )
257
+
258
+ for (const tour of sortedTours) {
259
+ if (state.completedTours.includes(tour.id)) continue
260
+ if (state.skippedTours.includes(tour.id)) continue
261
+
262
+ if (shouldTriggerTour(tour, triggerContext)) {
263
+ if (evaluateConditions(tour.conditions, conditionCtx)) {
264
+ const delay = tour.trigger.delay ?? autoStartDelay ?? 0
265
+
266
+ if (triggerTimeoutRef.current) {
267
+ clearTimeout(triggerTimeoutRef.current)
268
+ }
269
+
270
+ triggerTimeoutRef.current = setTimeout(() => {
271
+ startTour(tour.id)
272
+ }, delay)
273
+
274
+ break // Only trigger one tour at a time
275
+ }
276
+ }
277
+ }
278
+ }, [
279
+ state.initialized,
280
+ state.activeTour,
281
+ state.tours,
282
+ state.completedTours,
283
+ state.skippedTours,
284
+ autoStart,
285
+ pathname,
286
+ storage,
287
+ externalConditionContext,
288
+ autoStartDelay,
289
+ startTour,
290
+ ])
291
+
292
+ // Evaluate triggers on init and route change
293
+ useEffect(() => {
294
+ evaluateTriggers()
295
+ }, [evaluateTriggers])
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // Cross-Page Navigation
299
+ // ---------------------------------------------------------------------------
300
+
301
+ useEffect(() => {
302
+ if (!state.activeTour || state.activeTour.status !== 'active') return
303
+
304
+ const activeStep = getActiveStep(state)
305
+ if (!activeStep?.route) return
306
+
307
+ // If step requires a different route, navigate
308
+ if (activeStep.route !== pathname) {
309
+ try {
310
+ router.push(activeStep.route)
311
+ } catch (err) {
312
+ if (debug) console.error('[WalkMe] Navigation failed:', err)
313
+ // Skip to next step on navigation failure
314
+ dispatch({ type: 'NEXT_STEP' })
315
+ }
316
+ }
317
+ }, [state.activeTour?.currentStepIndex, Object.keys(state.tours).length]) // eslint-disable-line react-hooks/exhaustive-deps
318
+
319
+ // Wait for target after route change
320
+ useEffect(() => {
321
+ if (prevPathRef.current === pathname) return
322
+ prevPathRef.current = pathname
323
+
324
+ if (!state.activeTour || state.activeTour.status !== 'active') return
325
+
326
+ const activeStep = getActiveStep(state)
327
+ if (!activeStep?.target) return
328
+
329
+ waitForTarget(activeStep.target, { timeout: 5000 }).then((result) => {
330
+ if (result.found && result.element) {
331
+ setTargetElement(result.element)
332
+ scrollToElement(result.element)
333
+ } else if (debug) {
334
+ console.warn(
335
+ `[WalkMe] Target "${activeStep.target}" not found after navigation`,
336
+ )
337
+ }
338
+ })
339
+ }, [pathname, Object.keys(state.tours).length]) // eslint-disable-line react-hooks/exhaustive-deps
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // Target Element Resolution
343
+ // ---------------------------------------------------------------------------
344
+
345
+ const toursCount = Object.keys(state.tours).length
346
+
347
+ useEffect(() => {
348
+ // Clear stale target so floating tooltip unmounts and remounts fresh
349
+ setTargetElement(null)
350
+
351
+ if (!state.activeTour || state.activeTour.status !== 'active') return
352
+
353
+ const activeStep = getActiveStep(state)
354
+ if (!activeStep?.target) return
355
+
356
+ let cancelled = false
357
+
358
+ const applyTarget = (element: HTMLElement) => {
359
+ if (cancelled) return
360
+ scrollToElement(element)
361
+ setTargetElement(element)
362
+ }
363
+
364
+ // Short delay lets React flush the null state (unmounts tooltip)
365
+ const findTimer = setTimeout(() => {
366
+ if (cancelled) return
367
+ const result = findTarget(activeStep.target!)
368
+ if (result.found && result.element) {
369
+ applyTarget(result.element)
370
+ } else {
371
+ waitForTarget(activeStep.target!, { timeout: 5000 }).then((waitResult) => {
372
+ if (waitResult.found && waitResult.element) {
373
+ applyTarget(waitResult.element)
374
+ } else if (debug) {
375
+ console.warn(
376
+ `[WalkMe] Target "${activeStep.target}" not found, step may display without anchor`,
377
+ )
378
+ }
379
+ })
380
+ }
381
+ }, 50)
382
+
383
+ return () => {
384
+ cancelled = true
385
+ clearTimeout(findTimer)
386
+ }
387
+ }, [state.activeTour?.currentStepIndex, state.activeTour?.status, toursCount]) // eslint-disable-line react-hooks/exhaustive-deps
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // Step Change Analytics
391
+ // ---------------------------------------------------------------------------
392
+
393
+ useEffect(() => {
394
+ if (!state.activeTour || state.activeTour.status !== 'active') return
395
+
396
+ const tour = state.tours[state.activeTour.tourId]
397
+ if (!tour) return
398
+
399
+ const activeStep = getActiveStep(state)
400
+
401
+ onStepChange?.({
402
+ type: 'step_changed',
403
+ tourId: tour.id,
404
+ tourName: tour.name,
405
+ stepId: activeStep?.id,
406
+ stepIndex: state.activeTour.currentStepIndex,
407
+ totalSteps: tour.steps.length,
408
+ timestamp: Date.now(),
409
+ })
410
+ }, [state.activeTour?.currentStepIndex]) // eslint-disable-line react-hooks/exhaustive-deps
411
+
412
+ // ---------------------------------------------------------------------------
413
+ // Keyboard Navigation
414
+ // ---------------------------------------------------------------------------
415
+
416
+ useEffect(() => {
417
+ if (!state.activeTour || state.activeTour.status !== 'active') return
418
+
419
+ const handleKeyDown = (e: KeyboardEvent) => {
420
+ switch (e.key) {
421
+ case 'ArrowRight':
422
+ e.preventDefault()
423
+ nextStep()
424
+ break
425
+ case 'ArrowLeft':
426
+ e.preventDefault()
427
+ prevStep()
428
+ break
429
+ case 'Escape':
430
+ e.preventDefault()
431
+ skipTour()
432
+ break
433
+ }
434
+ }
435
+
436
+ document.addEventListener('keydown', handleKeyDown)
437
+ return () => document.removeEventListener('keydown', handleKeyDown)
438
+ }, [state.activeTour, nextStep, prevStep, skipTour])
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // Body Scroll Lock (only for modal/floating steps that cover the page)
442
+ // ---------------------------------------------------------------------------
443
+
444
+ const activeStepType = getActiveStep(state)?.type
445
+ useEffect(() => {
446
+ const isActive = state.activeTour?.status === 'active'
447
+ if (!isActive) return
448
+
449
+ // Only lock scroll for step types that cover the viewport
450
+ if (activeStepType !== 'modal' && activeStepType !== 'floating') return
451
+
452
+ const { style } = document.body
453
+ const originalOverflow = style.overflow
454
+ const originalPaddingRight = style.paddingRight
455
+
456
+ // Compensate for scrollbar width to prevent layout shift
457
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
458
+ style.overflow = 'hidden'
459
+ if (scrollbarWidth > 0) {
460
+ style.paddingRight = `${scrollbarWidth}px`
461
+ }
462
+
463
+ return () => {
464
+ style.overflow = originalOverflow
465
+ style.paddingRight = originalPaddingRight
466
+ }
467
+ }, [state.activeTour?.status, activeStepType])
468
+
469
+ // ---------------------------------------------------------------------------
470
+ // Cleanup
471
+ // ---------------------------------------------------------------------------
472
+
473
+ useEffect(() => {
474
+ return () => {
475
+ if (triggerTimeoutRef.current) {
476
+ clearTimeout(triggerTimeoutRef.current)
477
+ }
478
+ setTargetElement(null)
479
+ previousFocusRef.current = null
480
+ }
481
+ }, [])
482
+
483
+ // ---------------------------------------------------------------------------
484
+ // Context Value
485
+ // ---------------------------------------------------------------------------
486
+
487
+ const contextValue = useMemo<WalkmeContextValue>(
488
+ () => ({
489
+ state,
490
+ startTour,
491
+ pauseTour,
492
+ resumeTour,
493
+ skipTour,
494
+ completeTour,
495
+ resetTour,
496
+ resetAllTours,
497
+ nextStep,
498
+ prevStep,
499
+ goToStep,
500
+ isTourCompleted,
501
+ isTourActive,
502
+ getActiveTour: () => getActiveTour(state),
503
+ getActiveStep: () => getActiveStep(state),
504
+ emitEvent,
505
+ }),
506
+ [
507
+ state,
508
+ startTour,
509
+ pauseTour,
510
+ resumeTour,
511
+ skipTour,
512
+ completeTour,
513
+ resetTour,
514
+ resetAllTours,
515
+ nextStep,
516
+ prevStep,
517
+ goToStep,
518
+ isTourCompleted,
519
+ isTourActive,
520
+ emitEvent,
521
+ ],
522
+ )
523
+
524
+ // ---------------------------------------------------------------------------
525
+ // Render
526
+ // ---------------------------------------------------------------------------
527
+
528
+ const activeTour = getActiveTour(state)
529
+ const activeStep = getActiveStep(state)
530
+ const showStep = activeTour && activeStep && state.activeTour?.status === 'active'
531
+
532
+ // Build screen reader announcement
533
+ const srAnnouncement = showStep && state.activeTour
534
+ ? labels.progress
535
+ .replace('{current}', String(state.activeTour.currentStepIndex + 1))
536
+ .replace('{total}', String(activeTour!.steps.length))
537
+ + ': ' + activeStep.title
538
+ : ''
539
+
540
+ return (
541
+ <WalkmeContext.Provider value={contextValue}>
542
+ {children}
543
+
544
+ {/* Screen reader live region for step announcements */}
545
+ <div
546
+ aria-live="polite"
547
+ aria-atomic="true"
548
+ className="sr-only"
549
+ style={{
550
+ position: 'absolute',
551
+ width: '1px',
552
+ height: '1px',
553
+ padding: 0,
554
+ margin: '-1px',
555
+ overflow: 'hidden',
556
+ clip: 'rect(0, 0, 0, 0)',
557
+ whiteSpace: 'nowrap',
558
+ borderWidth: 0,
559
+ }}
560
+ >
561
+ {srAnnouncement}
562
+ </div>
563
+
564
+ {showStep && state.activeTour && (
565
+ <StepRenderer
566
+ step={activeStep}
567
+ targetElement={targetElement}
568
+ onNext={nextStep}
569
+ onPrev={prevStep}
570
+ onSkip={skipTour}
571
+ onComplete={completeTour}
572
+ isFirst={isFirstStep(state)}
573
+ isLast={isLastStep(state)}
574
+ currentIndex={state.activeTour.currentStepIndex}
575
+ totalSteps={activeTour.steps.length}
576
+ labels={labels}
577
+ />
578
+ )}
579
+ </WalkmeContext.Provider>
580
+ )
581
+ }
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // Step Renderer (internal)
585
+ // ---------------------------------------------------------------------------
586
+
587
+ interface StepRendererProps {
588
+ step: NonNullable<ReturnType<typeof getActiveStep>>
589
+ targetElement: HTMLElement | null
590
+ onNext: () => void
591
+ onPrev: () => void
592
+ onSkip: () => void
593
+ onComplete: () => void
594
+ isFirst: boolean
595
+ isLast: boolean
596
+ currentIndex: number
597
+ totalSteps: number
598
+ labels: WalkmeLabels
599
+ }
600
+
601
+ function StepRenderer({
602
+ step,
603
+ targetElement,
604
+ onNext,
605
+ onPrev,
606
+ onSkip,
607
+ onComplete,
608
+ isFirst,
609
+ isLast,
610
+ currentIndex,
611
+ totalSteps,
612
+ labels,
613
+ }: StepRendererProps) {
614
+ const commonProps = {
615
+ step,
616
+ onNext,
617
+ onPrev,
618
+ onSkip,
619
+ onComplete,
620
+ isFirst,
621
+ isLast,
622
+ currentIndex,
623
+ totalSteps,
624
+ labels,
625
+ }
626
+
627
+ switch (step.type) {
628
+ case 'modal':
629
+ return (
630
+ <>
631
+ <WalkmeOverlay visible />
632
+ <WalkmeModal {...commonProps} />
633
+ </>
634
+ )
635
+
636
+ case 'tooltip':
637
+ return (
638
+ <>
639
+ <WalkmeOverlay
640
+ visible
641
+ spotlightTarget={targetElement}
642
+ spotlightPadding={8}
643
+ />
644
+ <WalkmeTooltip {...commonProps} targetElement={targetElement} />
645
+ </>
646
+ )
647
+
648
+ case 'spotlight':
649
+ return (
650
+ <WalkmeSpotlight {...commonProps} targetElement={targetElement} />
651
+ )
652
+
653
+ case 'beacon':
654
+ return (
655
+ <WalkmeBeacon
656
+ step={step}
657
+ targetElement={targetElement}
658
+ onClick={onNext}
659
+ labels={labels}
660
+ />
661
+ )
662
+
663
+ case 'floating':
664
+ return (
665
+ <>
666
+ <WalkmeOverlay visible />
667
+ <WalkmeModal {...commonProps} />
668
+ </>
669
+ )
670
+
671
+ default:
672
+ return null
673
+ }
674
+ }