@aladinbs/react-guided-tour 1.0.3 → 1.2.0
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.
- package/README.md +200 -14
- package/dist/components/TourOverlay.d.ts.map +1 -1
- package/dist/components/TourPopover.d.ts.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.esm.js +206 -12
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +206 -12
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
A modern, flexible React TypeScript tour guide library with advanced highlighting and interaction capabilities. Built for React 19 with a clean, modular architecture that supports complex user onboarding flows.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Smart Element Targeting** - Flexible element selection with CSS selectors or direct element references
|
|
8
|
+
- **Modern UI Design** - Beautiful, customizable popover with smooth animations
|
|
9
|
+
- **Pluggable Architecture** - Extensible integration system for tabs, wizards, and navigation
|
|
10
|
+
- **Responsive Positioning** - Intelligent popover placement that adapts to viewport constraints
|
|
11
|
+
- **Theme Support** - Comprehensive theming with light/dark mode compatibility
|
|
12
|
+
- **State Persistence** - localStorage integration to remember tour completion and progress
|
|
13
|
+
- **Performance Optimized** - Framework-agnostic core with efficient React integration
|
|
14
|
+
- **Action System** - Automated interactions (clicks, navigation, tab switching)
|
|
15
|
+
- **Interaction Blocking** - Prevent user interactions outside the tour target for focused guidance
|
|
16
|
+
- **Click-to-Advance** - Allow users to click target elements directly to advance tour steps
|
|
17
|
+
- **Event System** - Rich event hooks for tour lifecycle management
|
|
18
|
+
- **TypeScript First** - Full type safety with comprehensive type definitions
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
19
21
|
|
|
20
22
|
### Installation
|
|
21
23
|
|
|
@@ -141,6 +143,8 @@ interface TourConfig {
|
|
|
141
143
|
onStepChange?: (step: TourStep, index: number) => void;
|
|
142
144
|
allowKeyboardNavigation?: boolean; // Enable keyboard controls
|
|
143
145
|
allowClickOutside?: boolean; // Allow clicking outside to close
|
|
146
|
+
blockInteractions?: boolean; // Block interactions outside tour target
|
|
147
|
+
clickToAdvance?: boolean; // Enable click-to-advance globally
|
|
144
148
|
showProgress?: boolean; // Show step progress indicator
|
|
145
149
|
storage?: {
|
|
146
150
|
key?: string; // localStorage key (default: 'tour-{id}')
|
|
@@ -168,6 +172,9 @@ interface TourStep {
|
|
|
168
172
|
canSkip?: boolean; // Allow skipping this step
|
|
169
173
|
waitForElement?: boolean; // Wait for target element to appear
|
|
170
174
|
waitTimeout?: number; // Timeout for element waiting (ms)
|
|
175
|
+
blockInteractions?: boolean; // Override global interaction blocking for this step
|
|
176
|
+
clickToAdvance?: boolean; // Override global click-to-advance for this step
|
|
177
|
+
previousButton?: PreviousButtonConfig; // Configure previous button behavior
|
|
171
178
|
}
|
|
172
179
|
```
|
|
173
180
|
|
|
@@ -185,6 +192,18 @@ interface TourAction {
|
|
|
185
192
|
}
|
|
186
193
|
```
|
|
187
194
|
|
|
195
|
+
### PreviousButtonConfig
|
|
196
|
+
|
|
197
|
+
Configure the previous button behavior for individual steps:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
interface PreviousButtonConfig {
|
|
201
|
+
show?: boolean; // Show/hide the previous button (default: true)
|
|
202
|
+
label?: string; // Custom button label (default: "Previous")
|
|
203
|
+
handler?: () => Promise<void> | void; // Custom click handler
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
188
207
|
### TourTheme
|
|
189
208
|
|
|
190
209
|
Comprehensive theming options:
|
|
@@ -279,6 +298,173 @@ Navigate through multi-step wizards:
|
|
|
279
298
|
}
|
|
280
299
|
```
|
|
281
300
|
|
|
301
|
+
### Interaction Blocking
|
|
302
|
+
|
|
303
|
+
Control user interactions during the tour to create focused guidance experiences:
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
const tourConfig: TourConfig = {
|
|
307
|
+
id: 'focused-tour',
|
|
308
|
+
blockInteractions: true, // Block interactions globally
|
|
309
|
+
steps: [
|
|
310
|
+
{
|
|
311
|
+
id: 'step1',
|
|
312
|
+
title: 'Focused Step',
|
|
313
|
+
content: 'Users can only interact with the highlighted element.',
|
|
314
|
+
target: '#important-button',
|
|
315
|
+
blockInteractions: true, // Enforce blocking for this step
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'step2',
|
|
319
|
+
title: 'Free Interaction',
|
|
320
|
+
content: 'Users can click anywhere during this step.',
|
|
321
|
+
target: '#another-element',
|
|
322
|
+
blockInteractions: false, // Override global setting for this step
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
};
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Features:**
|
|
329
|
+
- **Global Control**: Set `blockInteractions: true` in tour config to block interactions for all steps
|
|
330
|
+
- **Per-Step Override**: Use `blockInteractions` in individual steps to override the global setting
|
|
331
|
+
- **Visual Feedback**: Shows a "🔒 Interactions blocked" indicator when active
|
|
332
|
+
- **Smart Targeting**: Only the highlighted element and tour UI remain interactive
|
|
333
|
+
- **Keyboard Blocking**: Prevents keyboard interactions except tour navigation keys
|
|
334
|
+
|
|
335
|
+
**When to use:**
|
|
336
|
+
- Complex interfaces where users might get distracted
|
|
337
|
+
- Critical onboarding flows that must be completed in order
|
|
338
|
+
- Preventing accidental clicks that could break the tour flow
|
|
339
|
+
- Ensuring users focus on specific elements
|
|
340
|
+
|
|
341
|
+
### Click-to-Advance
|
|
342
|
+
|
|
343
|
+
Enable users to interact directly with tour target elements to advance to the next step:
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
const tourConfig: TourConfig = {
|
|
347
|
+
id: "interactive-tour",
|
|
348
|
+
clickToAdvance: true, // Enable globally
|
|
349
|
+
steps: [
|
|
350
|
+
{
|
|
351
|
+
id: "button-interaction",
|
|
352
|
+
title: "Try the Button",
|
|
353
|
+
content: "Click the button below to continue the tour!",
|
|
354
|
+
target: "#my-button",
|
|
355
|
+
clickToAdvance: true, // Enable for this step
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: "form-interaction",
|
|
359
|
+
title: "Fill the Form",
|
|
360
|
+
content: "Complete this form field to proceed.",
|
|
361
|
+
target: "#email-input",
|
|
362
|
+
clickToAdvance: false, // Disable for this step (use Next button)
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
};
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Features:**
|
|
369
|
+
- **Global Control**: Set `clickToAdvance: true` in tour config to enable for all steps
|
|
370
|
+
- **Per-Step Override**: Use `clickToAdvance` in individual steps to override the global setting
|
|
371
|
+
- **Smart UI**: Next button is automatically hidden when click-to-advance is active
|
|
372
|
+
- **Visual Guidance**: Shows "👆 Click the highlighted element to continue" instruction
|
|
373
|
+
- **Event Handling**: Automatically captures clicks on target elements and advances tour
|
|
374
|
+
|
|
375
|
+
**When to use:**
|
|
376
|
+
- Interactive tutorials where users need to practice using actual UI elements
|
|
377
|
+
- Onboarding flows for buttons, forms, and interactive components
|
|
378
|
+
- Training scenarios where clicking the real element is part of the learning
|
|
379
|
+
- Reducing cognitive load by eliminating the need to find the Next button
|
|
380
|
+
|
|
381
|
+
**Best practices:**
|
|
382
|
+
- Use for clickable elements like buttons, links, and form controls
|
|
383
|
+
- Combine with `blockInteractions: true` to ensure users click the right element
|
|
384
|
+
- Provide clear instructions in the step content about what to click
|
|
385
|
+
- Consider accessibility - ensure target elements are keyboard accessible
|
|
386
|
+
|
|
387
|
+
### Configurable Previous Button
|
|
388
|
+
|
|
389
|
+
Control the previous button's visibility, label, and behavior for each step:
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
const tourConfig: TourConfig = {
|
|
393
|
+
id: "custom-navigation-tour",
|
|
394
|
+
steps: [
|
|
395
|
+
{
|
|
396
|
+
id: "welcome",
|
|
397
|
+
title: "Welcome",
|
|
398
|
+
content: "Welcome to our tour!",
|
|
399
|
+
placement: "center",
|
|
400
|
+
// No previous button configuration - uses defaults
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
id: "first-step",
|
|
404
|
+
title: "Getting Started",
|
|
405
|
+
content: "This is the first step with no previous button.",
|
|
406
|
+
target: "#first-element",
|
|
407
|
+
previousButton: {
|
|
408
|
+
show: false, // Hide previous button completely
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
id: "custom-previous",
|
|
413
|
+
title: "Custom Previous",
|
|
414
|
+
content: "This step has a custom previous button.",
|
|
415
|
+
target: "#second-element",
|
|
416
|
+
previousButton: {
|
|
417
|
+
label: "Go Back",
|
|
418
|
+
handler: async () => {
|
|
419
|
+
// Custom logic - could navigate to a different step,
|
|
420
|
+
// show a confirmation dialog, save data, etc.
|
|
421
|
+
const confirmed = confirm("Are you sure you want to go back?");
|
|
422
|
+
if (confirmed) {
|
|
423
|
+
// You can access tour methods here if needed
|
|
424
|
+
console.log("Custom previous action executed");
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: "normal-step",
|
|
431
|
+
title: "Normal Step",
|
|
432
|
+
content: "This step uses the default previous button behavior.",
|
|
433
|
+
target: "#third-element",
|
|
434
|
+
// previousButton not specified - uses default behavior
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
};
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Configuration Options:**
|
|
441
|
+
|
|
442
|
+
- **`show: boolean`** - Control visibility of the previous button
|
|
443
|
+
- `true` (default): Show the previous button
|
|
444
|
+
- `false`: Hide the previous button completely
|
|
445
|
+
|
|
446
|
+
- **`label: string`** - Customize the button text
|
|
447
|
+
- Default: "Previous"
|
|
448
|
+
- Examples: "Go Back", "← Back", "Return", etc.
|
|
449
|
+
|
|
450
|
+
- **`handler: () => Promise<void> | void`** - Custom click behavior
|
|
451
|
+
- When provided, overrides the default previous step navigation
|
|
452
|
+
- Can be async for complex operations
|
|
453
|
+
- Useful for confirmations, data saving, custom navigation logic
|
|
454
|
+
|
|
455
|
+
**Use Cases:**
|
|
456
|
+
- **Hide on first content step**: Prevent users from going back to welcome screen
|
|
457
|
+
- **Custom confirmations**: Ask users to confirm before losing progress
|
|
458
|
+
- **Data validation**: Save or validate data before allowing navigation
|
|
459
|
+
- **Custom navigation**: Jump to specific steps instead of sequential navigation
|
|
460
|
+
- **Analytics tracking**: Log user navigation patterns
|
|
461
|
+
- **Conditional logic**: Show different behavior based on user state
|
|
462
|
+
|
|
463
|
+
**Default Behavior:**
|
|
464
|
+
- Previous button is automatically shown on all steps except the first step
|
|
465
|
+
- Clicking previous navigates to the immediately preceding step
|
|
466
|
+
- Button is disabled when navigation is not possible (e.g., during step transitions)
|
|
467
|
+
|
|
282
468
|
### Custom Integrations
|
|
283
469
|
|
|
284
470
|
Extend the tour system with custom integrations:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TourOverlay.d.ts","sourceRoot":"","sources":["../../src/components/TourOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"TourOverlay.d.ts","sourceRoot":"","sources":["../../src/components/TourOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAC;AAIzE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,8CAsQtB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TourPopover.d.ts","sourceRoot":"","sources":["../../src/components/TourPopover.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAOjF,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"TourPopover.d.ts","sourceRoot":"","sources":["../../src/components/TourPopover.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAOjF,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,8CA2WtB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ interface TourStep {
|
|
|
15
15
|
canSkip?: boolean;
|
|
16
16
|
waitForElement?: boolean;
|
|
17
17
|
waitTimeout?: number;
|
|
18
|
+
blockInteractions?: boolean;
|
|
19
|
+
clickToAdvance?: boolean;
|
|
20
|
+
previousButton?: PreviousButtonConfig;
|
|
18
21
|
}
|
|
19
22
|
interface TourAction {
|
|
20
23
|
type: 'click' | 'navigate' | 'highlight' | 'tab-switch' | 'wizard-step' | 'custom';
|
|
@@ -23,6 +26,11 @@ interface TourAction {
|
|
|
23
26
|
handler?: () => Promise<void> | void;
|
|
24
27
|
delay?: number;
|
|
25
28
|
}
|
|
29
|
+
interface PreviousButtonConfig {
|
|
30
|
+
show?: boolean;
|
|
31
|
+
label?: string;
|
|
32
|
+
handler?: () => Promise<void> | void;
|
|
33
|
+
}
|
|
26
34
|
interface HighlightConfig {
|
|
27
35
|
selector?: string;
|
|
28
36
|
element?: HTMLElement;
|
|
@@ -52,6 +60,8 @@ interface TourConfig {
|
|
|
52
60
|
onStepChange?: (step: TourStep, index: number) => void;
|
|
53
61
|
allowKeyboardNavigation?: boolean;
|
|
54
62
|
allowClickOutside?: boolean;
|
|
63
|
+
blockInteractions?: boolean;
|
|
64
|
+
clickToAdvance?: boolean;
|
|
55
65
|
showProgress?: boolean;
|
|
56
66
|
storage?: {
|
|
57
67
|
key?: string;
|
package/dist/index.esm.js
CHANGED
|
@@ -1414,8 +1414,21 @@ function useTourHighlight(step) {
|
|
|
1414
1414
|
}
|
|
1415
1415
|
|
|
1416
1416
|
const TourOverlay = React.memo(function TourOverlay({ className }) {
|
|
1417
|
-
const { state, theme, stop } = useTour();
|
|
1417
|
+
const { state, theme, stop, config, next } = useTour();
|
|
1418
1418
|
const { targetElement, highlightStyle, isVisible } = useTourHighlight(state.currentStep);
|
|
1419
|
+
const [scrollTrigger, setScrollTrigger] = useState(0);
|
|
1420
|
+
// Determine if interactions should be blocked
|
|
1421
|
+
const shouldBlockInteractions = useMemo(() => {
|
|
1422
|
+
const stepBlocking = state.currentStep?.blockInteractions;
|
|
1423
|
+
const globalBlocking = config.blockInteractions;
|
|
1424
|
+
return stepBlocking !== undefined ? stepBlocking : globalBlocking;
|
|
1425
|
+
}, [state.currentStep?.blockInteractions, config.blockInteractions]);
|
|
1426
|
+
// Determine if click-to-advance is enabled
|
|
1427
|
+
const shouldClickToAdvance = useMemo(() => {
|
|
1428
|
+
const stepClickToAdvance = state.currentStep?.clickToAdvance;
|
|
1429
|
+
const globalClickToAdvance = config.clickToAdvance;
|
|
1430
|
+
return stepClickToAdvance !== undefined ? stepClickToAdvance : globalClickToAdvance;
|
|
1431
|
+
}, [state.currentStep?.clickToAdvance, config.clickToAdvance]);
|
|
1419
1432
|
const overlayStyle = useMemo(() => ({
|
|
1420
1433
|
position: 'fixed',
|
|
1421
1434
|
top: 0,
|
|
@@ -1427,15 +1440,148 @@ const TourOverlay = React.memo(function TourOverlay({ className }) {
|
|
|
1427
1440
|
zIndex: (theme.zIndex || 9999) - 1,
|
|
1428
1441
|
pointerEvents: 'auto',
|
|
1429
1442
|
}), [theme.overlay?.backgroundColor, theme.overlay?.opacity, theme.zIndex]);
|
|
1430
|
-
|
|
1431
|
-
|
|
1443
|
+
// Create cutout style for the highlighted element
|
|
1444
|
+
const cutoutOverlayStyle = useMemo(() => {
|
|
1445
|
+
if ((!shouldBlockInteractions && !shouldClickToAdvance) || !targetElement || !isVisible) {
|
|
1446
|
+
return overlayStyle;
|
|
1447
|
+
}
|
|
1448
|
+
// Use getBoundingClientRect for viewport-relative coordinates (perfect for fixed overlay)
|
|
1449
|
+
const rect = targetElement.getBoundingClientRect();
|
|
1450
|
+
const padding = state.currentStep?.highlight?.padding || 8;
|
|
1451
|
+
// Ensure coordinates are within viewport bounds
|
|
1452
|
+
const left = Math.max(0, rect.left - padding);
|
|
1453
|
+
const top = Math.max(0, rect.top - padding);
|
|
1454
|
+
const right = Math.min(window.innerWidth, rect.right + padding);
|
|
1455
|
+
const bottom = Math.min(window.innerHeight, rect.bottom + padding);
|
|
1456
|
+
return {
|
|
1457
|
+
...overlayStyle,
|
|
1458
|
+
clipPath: `polygon(
|
|
1459
|
+
0% 0%,
|
|
1460
|
+
0% 100%,
|
|
1461
|
+
${left}px 100%,
|
|
1462
|
+
${left}px ${top}px,
|
|
1463
|
+
${right}px ${top}px,
|
|
1464
|
+
${right}px ${bottom}px,
|
|
1465
|
+
${left}px ${bottom}px,
|
|
1466
|
+
${left}px 100%,
|
|
1467
|
+
100% 100%,
|
|
1468
|
+
100% 0%
|
|
1469
|
+
)`,
|
|
1470
|
+
};
|
|
1471
|
+
}, [overlayStyle, shouldBlockInteractions, shouldClickToAdvance, targetElement, isVisible, state.currentStep?.highlight?.padding, scrollTrigger]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
1472
|
+
// Force re-render of cutout overlay on scroll to maintain proper positioning
|
|
1473
|
+
useEffect(() => {
|
|
1474
|
+
if (!shouldBlockInteractions && !shouldClickToAdvance) {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
const handleScroll = () => {
|
|
1478
|
+
// Force recalculation by updating scroll trigger
|
|
1479
|
+
setScrollTrigger(prev => prev + 1);
|
|
1480
|
+
};
|
|
1481
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
1482
|
+
window.addEventListener('resize', handleScroll, { passive: true });
|
|
1483
|
+
return () => {
|
|
1484
|
+
window.removeEventListener('scroll', handleScroll);
|
|
1485
|
+
window.removeEventListener('resize', handleScroll);
|
|
1486
|
+
};
|
|
1487
|
+
}, [shouldBlockInteractions, shouldClickToAdvance]);
|
|
1488
|
+
const handleOverlayClick = useCallback((e) => {
|
|
1489
|
+
// If interactions are blocked, prevent the click from propagating
|
|
1490
|
+
if (shouldBlockInteractions) {
|
|
1491
|
+
e.preventDefault();
|
|
1492
|
+
e.stopPropagation();
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
// Allow clicking outside to close tour if not blocking interactions
|
|
1496
|
+
if (config.allowClickOutside !== false && state.currentStep?.canSkip !== false) {
|
|
1432
1497
|
stop();
|
|
1433
1498
|
}
|
|
1434
|
-
}, [state.currentStep?.canSkip, stop]);
|
|
1499
|
+
}, [shouldBlockInteractions, config.allowClickOutside, state.currentStep?.canSkip, stop]);
|
|
1500
|
+
// Handle click-to-advance functionality
|
|
1501
|
+
useEffect(() => {
|
|
1502
|
+
if (!shouldClickToAdvance || !targetElement || !state.isRunning) {
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
// Ensure target element is above the overlay when click-to-advance is enabled
|
|
1506
|
+
const originalZIndex = targetElement.style.zIndex;
|
|
1507
|
+
const originalPosition = targetElement.style.position;
|
|
1508
|
+
targetElement.style.position = targetElement.style.position || 'relative';
|
|
1509
|
+
targetElement.style.zIndex = String((theme.zIndex || 9999) + 1);
|
|
1510
|
+
const handleTargetClick = (_e) => {
|
|
1511
|
+
setTimeout(() => {
|
|
1512
|
+
next();
|
|
1513
|
+
}, 100);
|
|
1514
|
+
};
|
|
1515
|
+
targetElement.addEventListener('click', handleTargetClick, true);
|
|
1516
|
+
return () => {
|
|
1517
|
+
targetElement.removeEventListener('click', handleTargetClick, true);
|
|
1518
|
+
// Restore original styles
|
|
1519
|
+
targetElement.style.zIndex = originalZIndex;
|
|
1520
|
+
targetElement.style.position = originalPosition;
|
|
1521
|
+
};
|
|
1522
|
+
}, [shouldClickToAdvance, targetElement, state.isRunning, next, theme.zIndex]);
|
|
1523
|
+
// Block interactions on the entire page when blocking is enabled
|
|
1524
|
+
useEffect(() => {
|
|
1525
|
+
if (!shouldBlockInteractions || !state.isRunning) {
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const handleGlobalClick = (e) => {
|
|
1529
|
+
const target = e.target;
|
|
1530
|
+
// Allow clicks on the tour target element and its children
|
|
1531
|
+
if (targetElement && (targetElement.contains(target) || targetElement === target)) {
|
|
1532
|
+
// If click-to-advance is enabled, let the target click handler deal with it
|
|
1533
|
+
if (shouldClickToAdvance) {
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
// Allow clicks on tour UI elements (popover, buttons, etc.)
|
|
1539
|
+
if (target.closest('[data-tour-popover]') ||
|
|
1540
|
+
target.closest('[data-tour-highlight]') ||
|
|
1541
|
+
target.closest('[data-tour-overlay]')) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
// Block all other clicks
|
|
1545
|
+
e.preventDefault();
|
|
1546
|
+
e.stopPropagation();
|
|
1547
|
+
};
|
|
1548
|
+
const handleGlobalKeydown = (e) => {
|
|
1549
|
+
// Allow tour navigation keys
|
|
1550
|
+
if (['Escape', 'ArrowLeft', 'ArrowRight', 'Enter', 'Space'].includes(e.key)) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
// Block other keyboard interactions
|
|
1554
|
+
e.preventDefault();
|
|
1555
|
+
e.stopPropagation();
|
|
1556
|
+
};
|
|
1557
|
+
// Add event listeners to capture phase to block interactions early
|
|
1558
|
+
document.addEventListener('click', handleGlobalClick, true);
|
|
1559
|
+
document.addEventListener('keydown', handleGlobalKeydown, true);
|
|
1560
|
+
document.addEventListener('mousedown', handleGlobalClick, true);
|
|
1561
|
+
document.addEventListener('touchstart', handleGlobalClick, true);
|
|
1562
|
+
return () => {
|
|
1563
|
+
document.removeEventListener('click', handleGlobalClick, true);
|
|
1564
|
+
document.removeEventListener('keydown', handleGlobalKeydown, true);
|
|
1565
|
+
document.removeEventListener('mousedown', handleGlobalClick, true);
|
|
1566
|
+
document.removeEventListener('touchstart', handleGlobalClick, true);
|
|
1567
|
+
};
|
|
1568
|
+
}, [shouldBlockInteractions, state.isRunning, targetElement, shouldClickToAdvance]);
|
|
1435
1569
|
if (!state.isRunning || !state.currentStep) {
|
|
1436
1570
|
return null;
|
|
1437
1571
|
}
|
|
1438
|
-
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("div", { style: overlayStyle, onClick: handleOverlayClick, className: className, "data-tour-overlay": true }), isVisible && targetElement && (jsxRuntimeExports.jsx("div", { style: highlightStyle, "data-tour-highlight": true })), jsxRuntimeExports.jsx("
|
|
1572
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("div", { style: (shouldBlockInteractions || shouldClickToAdvance) ? cutoutOverlayStyle : overlayStyle, onClick: handleOverlayClick, className: className, "data-tour-overlay": true }), isVisible && targetElement && (jsxRuntimeExports.jsx("div", { style: highlightStyle, "data-tour-highlight": true })), shouldBlockInteractions && (jsxRuntimeExports.jsx("div", { style: {
|
|
1573
|
+
position: 'fixed',
|
|
1574
|
+
top: '20px',
|
|
1575
|
+
right: '20px',
|
|
1576
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
1577
|
+
color: 'white',
|
|
1578
|
+
padding: '8px 12px',
|
|
1579
|
+
borderRadius: '4px',
|
|
1580
|
+
fontSize: '12px',
|
|
1581
|
+
zIndex: (theme.zIndex || 9999) + 1,
|
|
1582
|
+
pointerEvents: 'none',
|
|
1583
|
+
animation: 'tour-fade-in 0.3s ease-out',
|
|
1584
|
+
}, "data-tour-blocking-indicator": true, children: "\uD83D\uDD12 Interactions blocked" })), jsxRuntimeExports.jsx("style", { children: `
|
|
1439
1585
|
@keyframes tour-highlight-pulse {
|
|
1440
1586
|
0%, 100% {
|
|
1441
1587
|
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2), 0 0 20px rgba(59, 130, 246, 0.3);
|
|
@@ -1463,24 +1609,53 @@ const TourOverlay = React.memo(function TourOverlay({ className }) {
|
|
|
1463
1609
|
[data-tour-highlight] {
|
|
1464
1610
|
animation: tour-fade-in 0.3s ease-out;
|
|
1465
1611
|
}
|
|
1612
|
+
|
|
1613
|
+
[data-tour-blocking-indicator] {
|
|
1614
|
+
animation: tour-fade-in 0.3s ease-out;
|
|
1615
|
+
}
|
|
1466
1616
|
` })] }));
|
|
1467
1617
|
});
|
|
1468
1618
|
|
|
1469
1619
|
function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=" "),n+=f);}else for(f in e)e[f]&&(n&&(n+=" "),n+=f);return n}function clsx(){for(var e,t,f=0,n="",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=" "),n+=t);return n}
|
|
1470
1620
|
|
|
1471
1621
|
const TourPopover = React.memo(function TourPopover({ className }) {
|
|
1472
|
-
const { state, theme, next, previous, skip, stop, isFirstStep, isLastStep, canGoNext, canGoPrevious } = useTour();
|
|
1622
|
+
const { state, theme, next, previous, skip, stop, isFirstStep, isLastStep, canGoNext, canGoPrevious, config } = useTour();
|
|
1473
1623
|
const { targetElement } = useTourHighlight(state.currentStep);
|
|
1474
1624
|
const popoverRef = useRef(null);
|
|
1475
1625
|
const [position, setPosition] = useState({ top: 0, left: 0, placement: 'top' });
|
|
1476
1626
|
const updatePosition = useCallback(() => {
|
|
1477
|
-
if (!popoverRef.current || !
|
|
1627
|
+
if (!popoverRef.current || !state.currentStep)
|
|
1628
|
+
return;
|
|
1629
|
+
// For center placement without target element, calculate center position directly
|
|
1630
|
+
if (state.currentStep.placement === 'center' && !targetElement) {
|
|
1631
|
+
const popoverRect = popoverRef.current.getBoundingClientRect();
|
|
1632
|
+
const viewport = {
|
|
1633
|
+
width: window.innerWidth,
|
|
1634
|
+
height: window.innerHeight,
|
|
1635
|
+
scrollTop: window.pageYOffset || document.documentElement.scrollTop,
|
|
1636
|
+
scrollLeft: window.pageXOffset || document.documentElement.scrollLeft,
|
|
1637
|
+
};
|
|
1638
|
+
setPosition({
|
|
1639
|
+
top: viewport.scrollTop + (viewport.height - popoverRect.height) / 2,
|
|
1640
|
+
left: viewport.scrollLeft + (viewport.width - popoverRect.width) / 2,
|
|
1641
|
+
placement: 'center',
|
|
1642
|
+
});
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
// For other placements, require target element
|
|
1646
|
+
if (!targetElement)
|
|
1478
1647
|
return;
|
|
1479
1648
|
const newPosition = calculatePopoverPosition(targetElement, popoverRef.current, state.currentStep.placement || 'top');
|
|
1480
1649
|
setPosition(newPosition);
|
|
1481
1650
|
}, [targetElement, state.currentStep]);
|
|
1482
1651
|
const step = state.currentStep;
|
|
1483
1652
|
const popoverConfig = useMemo(() => step?.popover || {}, [step?.popover]);
|
|
1653
|
+
// Determine if click-to-advance is enabled for this step
|
|
1654
|
+
const shouldClickToAdvance = useMemo(() => {
|
|
1655
|
+
const stepClickToAdvance = step?.clickToAdvance;
|
|
1656
|
+
const globalClickToAdvance = config.clickToAdvance;
|
|
1657
|
+
return stepClickToAdvance !== undefined ? stepClickToAdvance : globalClickToAdvance;
|
|
1658
|
+
}, [step?.clickToAdvance, config.clickToAdvance]);
|
|
1484
1659
|
const popoverStyle = useMemo(() => ({
|
|
1485
1660
|
position: 'absolute',
|
|
1486
1661
|
top: position.top,
|
|
@@ -1513,10 +1688,14 @@ const TourPopover = React.memo(function TourPopover({ className }) {
|
|
|
1513
1688
|
}
|
|
1514
1689
|
}, [canGoNext, isLastStep, next]);
|
|
1515
1690
|
const handlePrevious = useCallback(async () => {
|
|
1691
|
+
if (step?.previousButton?.handler) {
|
|
1692
|
+
await step.previousButton.handler();
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1516
1695
|
if (canGoPrevious) {
|
|
1517
1696
|
await previous();
|
|
1518
1697
|
}
|
|
1519
|
-
}, [canGoPrevious, previous]);
|
|
1698
|
+
}, [canGoPrevious, previous, step?.previousButton]);
|
|
1520
1699
|
const handleSkip = useCallback(async () => {
|
|
1521
1700
|
await skip();
|
|
1522
1701
|
}, [skip]);
|
|
@@ -1524,12 +1703,16 @@ const TourPopover = React.memo(function TourPopover({ className }) {
|
|
|
1524
1703
|
await stop();
|
|
1525
1704
|
}, [stop]);
|
|
1526
1705
|
useEffect(() => {
|
|
1527
|
-
|
|
1706
|
+
// Small delay to ensure popover is rendered and has dimensions
|
|
1707
|
+
const timeoutId = setTimeout(() => {
|
|
1708
|
+
updatePosition();
|
|
1709
|
+
}, 10);
|
|
1528
1710
|
const handleResize = () => updatePosition();
|
|
1529
1711
|
const handleScroll = () => updatePosition();
|
|
1530
1712
|
window.addEventListener('resize', handleResize);
|
|
1531
1713
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
1532
1714
|
return () => {
|
|
1715
|
+
clearTimeout(timeoutId);
|
|
1533
1716
|
window.removeEventListener('resize', handleResize);
|
|
1534
1717
|
window.removeEventListener('scroll', handleScroll);
|
|
1535
1718
|
};
|
|
@@ -1604,7 +1787,7 @@ const TourPopover = React.memo(function TourPopover({ className }) {
|
|
|
1604
1787
|
pointerEvents: 'auto',
|
|
1605
1788
|
position: 'relative',
|
|
1606
1789
|
zIndex: 99999,
|
|
1607
|
-
}, children: popoverConfig.skipLabel || 'Skip Tour' })), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '8px', marginLeft: 'auto' }, children: [!isFirstStep && (jsxRuntimeExports.jsx("button", { onClick: handlePrevious, disabled: !canGoPrevious, style: {
|
|
1790
|
+
}, children: popoverConfig.skipLabel || 'Skip Tour' })), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '8px', marginLeft: 'auto' }, children: [!isFirstStep && (step?.previousButton?.show !== false) && (jsxRuntimeExports.jsx("button", { onClick: handlePrevious, disabled: !canGoPrevious, style: {
|
|
1608
1791
|
backgroundColor: 'transparent',
|
|
1609
1792
|
borderTopWidth: '1px',
|
|
1610
1793
|
borderRightWidth: '1px',
|
|
@@ -1626,7 +1809,7 @@ const TourPopover = React.memo(function TourPopover({ className }) {
|
|
|
1626
1809
|
fontWeight: '500',
|
|
1627
1810
|
opacity: canGoPrevious ? 1 : 0.5,
|
|
1628
1811
|
transition: 'all 0.2s ease',
|
|
1629
|
-
}, children:
|
|
1812
|
+
}, children: step?.previousButton?.label || 'Previous' })), !shouldClickToAdvance && (jsxRuntimeExports.jsx("button", { onClick: (e) => {
|
|
1630
1813
|
e.preventDefault();
|
|
1631
1814
|
e.stopPropagation();
|
|
1632
1815
|
handleNext();
|
|
@@ -1649,7 +1832,18 @@ const TourPopover = React.memo(function TourPopover({ className }) {
|
|
|
1649
1832
|
zIndex: 99999,
|
|
1650
1833
|
}, children: isLastStep
|
|
1651
1834
|
? (popoverConfig.finishLabel || 'Finish')
|
|
1652
|
-
: (popoverConfig.nextLabel || 'Next') })
|
|
1835
|
+
: (popoverConfig.nextLabel || 'Next') })), shouldClickToAdvance && (jsxRuntimeExports.jsx("div", { style: {
|
|
1836
|
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
1837
|
+
border: '1px solid rgba(59, 130, 246, 0.3)',
|
|
1838
|
+
color: theme.primaryColor || '#3b82f6',
|
|
1839
|
+
padding: '8px 16px',
|
|
1840
|
+
borderRadius: '8px',
|
|
1841
|
+
fontSize: '14px',
|
|
1842
|
+
fontWeight: '500',
|
|
1843
|
+
display: 'flex',
|
|
1844
|
+
alignItems: 'center',
|
|
1845
|
+
gap: '6px',
|
|
1846
|
+
}, children: "\uD83D\uDC46 Click the highlighted element to continue" }))] })] }), position.placement !== 'center' && (jsxRuntimeExports.jsx("div", { style: {
|
|
1653
1847
|
position: 'absolute',
|
|
1654
1848
|
width: '12px',
|
|
1655
1849
|
height: '12px',
|