@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/dist/index.js CHANGED
@@ -1416,8 +1416,21 @@ function useTourHighlight(step) {
1416
1416
  }
1417
1417
 
1418
1418
  const TourOverlay = React.memo(function TourOverlay({ className }) {
1419
- const { state, theme, stop } = useTour();
1419
+ const { state, theme, stop, config, next } = useTour();
1420
1420
  const { targetElement, highlightStyle, isVisible } = useTourHighlight(state.currentStep);
1421
+ const [scrollTrigger, setScrollTrigger] = React.useState(0);
1422
+ // Determine if interactions should be blocked
1423
+ const shouldBlockInteractions = React.useMemo(() => {
1424
+ const stepBlocking = state.currentStep?.blockInteractions;
1425
+ const globalBlocking = config.blockInteractions;
1426
+ return stepBlocking !== undefined ? stepBlocking : globalBlocking;
1427
+ }, [state.currentStep?.blockInteractions, config.blockInteractions]);
1428
+ // Determine if click-to-advance is enabled
1429
+ const shouldClickToAdvance = React.useMemo(() => {
1430
+ const stepClickToAdvance = state.currentStep?.clickToAdvance;
1431
+ const globalClickToAdvance = config.clickToAdvance;
1432
+ return stepClickToAdvance !== undefined ? stepClickToAdvance : globalClickToAdvance;
1433
+ }, [state.currentStep?.clickToAdvance, config.clickToAdvance]);
1421
1434
  const overlayStyle = React.useMemo(() => ({
1422
1435
  position: 'fixed',
1423
1436
  top: 0,
@@ -1429,15 +1442,148 @@ const TourOverlay = React.memo(function TourOverlay({ className }) {
1429
1442
  zIndex: (theme.zIndex || 9999) - 1,
1430
1443
  pointerEvents: 'auto',
1431
1444
  }), [theme.overlay?.backgroundColor, theme.overlay?.opacity, theme.zIndex]);
1432
- const handleOverlayClick = React.useCallback(() => {
1433
- if (state.currentStep?.canSkip !== false) {
1445
+ // Create cutout style for the highlighted element
1446
+ const cutoutOverlayStyle = React.useMemo(() => {
1447
+ if ((!shouldBlockInteractions && !shouldClickToAdvance) || !targetElement || !isVisible) {
1448
+ return overlayStyle;
1449
+ }
1450
+ // Use getBoundingClientRect for viewport-relative coordinates (perfect for fixed overlay)
1451
+ const rect = targetElement.getBoundingClientRect();
1452
+ const padding = state.currentStep?.highlight?.padding || 8;
1453
+ // Ensure coordinates are within viewport bounds
1454
+ const left = Math.max(0, rect.left - padding);
1455
+ const top = Math.max(0, rect.top - padding);
1456
+ const right = Math.min(window.innerWidth, rect.right + padding);
1457
+ const bottom = Math.min(window.innerHeight, rect.bottom + padding);
1458
+ return {
1459
+ ...overlayStyle,
1460
+ clipPath: `polygon(
1461
+ 0% 0%,
1462
+ 0% 100%,
1463
+ ${left}px 100%,
1464
+ ${left}px ${top}px,
1465
+ ${right}px ${top}px,
1466
+ ${right}px ${bottom}px,
1467
+ ${left}px ${bottom}px,
1468
+ ${left}px 100%,
1469
+ 100% 100%,
1470
+ 100% 0%
1471
+ )`,
1472
+ };
1473
+ }, [overlayStyle, shouldBlockInteractions, shouldClickToAdvance, targetElement, isVisible, state.currentStep?.highlight?.padding, scrollTrigger]); // eslint-disable-line react-hooks/exhaustive-deps
1474
+ // Force re-render of cutout overlay on scroll to maintain proper positioning
1475
+ React.useEffect(() => {
1476
+ if (!shouldBlockInteractions && !shouldClickToAdvance) {
1477
+ return;
1478
+ }
1479
+ const handleScroll = () => {
1480
+ // Force recalculation by updating scroll trigger
1481
+ setScrollTrigger(prev => prev + 1);
1482
+ };
1483
+ window.addEventListener('scroll', handleScroll, { passive: true });
1484
+ window.addEventListener('resize', handleScroll, { passive: true });
1485
+ return () => {
1486
+ window.removeEventListener('scroll', handleScroll);
1487
+ window.removeEventListener('resize', handleScroll);
1488
+ };
1489
+ }, [shouldBlockInteractions, shouldClickToAdvance]);
1490
+ const handleOverlayClick = React.useCallback((e) => {
1491
+ // If interactions are blocked, prevent the click from propagating
1492
+ if (shouldBlockInteractions) {
1493
+ e.preventDefault();
1494
+ e.stopPropagation();
1495
+ return;
1496
+ }
1497
+ // Allow clicking outside to close tour if not blocking interactions
1498
+ if (config.allowClickOutside !== false && state.currentStep?.canSkip !== false) {
1434
1499
  stop();
1435
1500
  }
1436
- }, [state.currentStep?.canSkip, stop]);
1501
+ }, [shouldBlockInteractions, config.allowClickOutside, state.currentStep?.canSkip, stop]);
1502
+ // Handle click-to-advance functionality
1503
+ React.useEffect(() => {
1504
+ if (!shouldClickToAdvance || !targetElement || !state.isRunning) {
1505
+ return;
1506
+ }
1507
+ // Ensure target element is above the overlay when click-to-advance is enabled
1508
+ const originalZIndex = targetElement.style.zIndex;
1509
+ const originalPosition = targetElement.style.position;
1510
+ targetElement.style.position = targetElement.style.position || 'relative';
1511
+ targetElement.style.zIndex = String((theme.zIndex || 9999) + 1);
1512
+ const handleTargetClick = (_e) => {
1513
+ setTimeout(() => {
1514
+ next();
1515
+ }, 100);
1516
+ };
1517
+ targetElement.addEventListener('click', handleTargetClick, true);
1518
+ return () => {
1519
+ targetElement.removeEventListener('click', handleTargetClick, true);
1520
+ // Restore original styles
1521
+ targetElement.style.zIndex = originalZIndex;
1522
+ targetElement.style.position = originalPosition;
1523
+ };
1524
+ }, [shouldClickToAdvance, targetElement, state.isRunning, next, theme.zIndex]);
1525
+ // Block interactions on the entire page when blocking is enabled
1526
+ React.useEffect(() => {
1527
+ if (!shouldBlockInteractions || !state.isRunning) {
1528
+ return;
1529
+ }
1530
+ const handleGlobalClick = (e) => {
1531
+ const target = e.target;
1532
+ // Allow clicks on the tour target element and its children
1533
+ if (targetElement && (targetElement.contains(target) || targetElement === target)) {
1534
+ // If click-to-advance is enabled, let the target click handler deal with it
1535
+ if (shouldClickToAdvance) {
1536
+ return;
1537
+ }
1538
+ return;
1539
+ }
1540
+ // Allow clicks on tour UI elements (popover, buttons, etc.)
1541
+ if (target.closest('[data-tour-popover]') ||
1542
+ target.closest('[data-tour-highlight]') ||
1543
+ target.closest('[data-tour-overlay]')) {
1544
+ return;
1545
+ }
1546
+ // Block all other clicks
1547
+ e.preventDefault();
1548
+ e.stopPropagation();
1549
+ };
1550
+ const handleGlobalKeydown = (e) => {
1551
+ // Allow tour navigation keys
1552
+ if (['Escape', 'ArrowLeft', 'ArrowRight', 'Enter', 'Space'].includes(e.key)) {
1553
+ return;
1554
+ }
1555
+ // Block other keyboard interactions
1556
+ e.preventDefault();
1557
+ e.stopPropagation();
1558
+ };
1559
+ // Add event listeners to capture phase to block interactions early
1560
+ document.addEventListener('click', handleGlobalClick, true);
1561
+ document.addEventListener('keydown', handleGlobalKeydown, true);
1562
+ document.addEventListener('mousedown', handleGlobalClick, true);
1563
+ document.addEventListener('touchstart', handleGlobalClick, true);
1564
+ return () => {
1565
+ document.removeEventListener('click', handleGlobalClick, true);
1566
+ document.removeEventListener('keydown', handleGlobalKeydown, true);
1567
+ document.removeEventListener('mousedown', handleGlobalClick, true);
1568
+ document.removeEventListener('touchstart', handleGlobalClick, true);
1569
+ };
1570
+ }, [shouldBlockInteractions, state.isRunning, targetElement, shouldClickToAdvance]);
1437
1571
  if (!state.isRunning || !state.currentStep) {
1438
1572
  return null;
1439
1573
  }
1440
- 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("style", { children: `
1574
+ 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: {
1575
+ position: 'fixed',
1576
+ top: '20px',
1577
+ right: '20px',
1578
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
1579
+ color: 'white',
1580
+ padding: '8px 12px',
1581
+ borderRadius: '4px',
1582
+ fontSize: '12px',
1583
+ zIndex: (theme.zIndex || 9999) + 1,
1584
+ pointerEvents: 'none',
1585
+ animation: 'tour-fade-in 0.3s ease-out',
1586
+ }, "data-tour-blocking-indicator": true, children: "\uD83D\uDD12 Interactions blocked" })), jsxRuntimeExports.jsx("style", { children: `
1441
1587
  @keyframes tour-highlight-pulse {
1442
1588
  0%, 100% {
1443
1589
  box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2), 0 0 20px rgba(59, 130, 246, 0.3);
@@ -1465,24 +1611,53 @@ const TourOverlay = React.memo(function TourOverlay({ className }) {
1465
1611
  [data-tour-highlight] {
1466
1612
  animation: tour-fade-in 0.3s ease-out;
1467
1613
  }
1614
+
1615
+ [data-tour-blocking-indicator] {
1616
+ animation: tour-fade-in 0.3s ease-out;
1617
+ }
1468
1618
  ` })] }));
1469
1619
  });
1470
1620
 
1471
1621
  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}
1472
1622
 
1473
1623
  const TourPopover = React.memo(function TourPopover({ className }) {
1474
- const { state, theme, next, previous, skip, stop, isFirstStep, isLastStep, canGoNext, canGoPrevious } = useTour();
1624
+ const { state, theme, next, previous, skip, stop, isFirstStep, isLastStep, canGoNext, canGoPrevious, config } = useTour();
1475
1625
  const { targetElement } = useTourHighlight(state.currentStep);
1476
1626
  const popoverRef = React.useRef(null);
1477
1627
  const [position, setPosition] = React.useState({ top: 0, left: 0, placement: 'top' });
1478
1628
  const updatePosition = React.useCallback(() => {
1479
- if (!popoverRef.current || !targetElement || !state.currentStep)
1629
+ if (!popoverRef.current || !state.currentStep)
1630
+ return;
1631
+ // For center placement without target element, calculate center position directly
1632
+ if (state.currentStep.placement === 'center' && !targetElement) {
1633
+ const popoverRect = popoverRef.current.getBoundingClientRect();
1634
+ const viewport = {
1635
+ width: window.innerWidth,
1636
+ height: window.innerHeight,
1637
+ scrollTop: window.pageYOffset || document.documentElement.scrollTop,
1638
+ scrollLeft: window.pageXOffset || document.documentElement.scrollLeft,
1639
+ };
1640
+ setPosition({
1641
+ top: viewport.scrollTop + (viewport.height - popoverRect.height) / 2,
1642
+ left: viewport.scrollLeft + (viewport.width - popoverRect.width) / 2,
1643
+ placement: 'center',
1644
+ });
1645
+ return;
1646
+ }
1647
+ // For other placements, require target element
1648
+ if (!targetElement)
1480
1649
  return;
1481
1650
  const newPosition = calculatePopoverPosition(targetElement, popoverRef.current, state.currentStep.placement || 'top');
1482
1651
  setPosition(newPosition);
1483
1652
  }, [targetElement, state.currentStep]);
1484
1653
  const step = state.currentStep;
1485
1654
  const popoverConfig = React.useMemo(() => step?.popover || {}, [step?.popover]);
1655
+ // Determine if click-to-advance is enabled for this step
1656
+ const shouldClickToAdvance = React.useMemo(() => {
1657
+ const stepClickToAdvance = step?.clickToAdvance;
1658
+ const globalClickToAdvance = config.clickToAdvance;
1659
+ return stepClickToAdvance !== undefined ? stepClickToAdvance : globalClickToAdvance;
1660
+ }, [step?.clickToAdvance, config.clickToAdvance]);
1486
1661
  const popoverStyle = React.useMemo(() => ({
1487
1662
  position: 'absolute',
1488
1663
  top: position.top,
@@ -1515,10 +1690,14 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1515
1690
  }
1516
1691
  }, [canGoNext, isLastStep, next]);
1517
1692
  const handlePrevious = React.useCallback(async () => {
1693
+ if (step?.previousButton?.handler) {
1694
+ await step.previousButton.handler();
1695
+ return;
1696
+ }
1518
1697
  if (canGoPrevious) {
1519
1698
  await previous();
1520
1699
  }
1521
- }, [canGoPrevious, previous]);
1700
+ }, [canGoPrevious, previous, step?.previousButton]);
1522
1701
  const handleSkip = React.useCallback(async () => {
1523
1702
  await skip();
1524
1703
  }, [skip]);
@@ -1526,12 +1705,16 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1526
1705
  await stop();
1527
1706
  }, [stop]);
1528
1707
  React.useEffect(() => {
1529
- updatePosition();
1708
+ // Small delay to ensure popover is rendered and has dimensions
1709
+ const timeoutId = setTimeout(() => {
1710
+ updatePosition();
1711
+ }, 10);
1530
1712
  const handleResize = () => updatePosition();
1531
1713
  const handleScroll = () => updatePosition();
1532
1714
  window.addEventListener('resize', handleResize);
1533
1715
  window.addEventListener('scroll', handleScroll, { passive: true });
1534
1716
  return () => {
1717
+ clearTimeout(timeoutId);
1535
1718
  window.removeEventListener('resize', handleResize);
1536
1719
  window.removeEventListener('scroll', handleScroll);
1537
1720
  };
@@ -1606,7 +1789,7 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1606
1789
  pointerEvents: 'auto',
1607
1790
  position: 'relative',
1608
1791
  zIndex: 99999,
1609
- }, 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: {
1792
+ }, 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: {
1610
1793
  backgroundColor: 'transparent',
1611
1794
  borderTopWidth: '1px',
1612
1795
  borderRightWidth: '1px',
@@ -1628,7 +1811,7 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1628
1811
  fontWeight: '500',
1629
1812
  opacity: canGoPrevious ? 1 : 0.5,
1630
1813
  transition: 'all 0.2s ease',
1631
- }, children: "Previous" })), jsxRuntimeExports.jsx("button", { onClick: (e) => {
1814
+ }, children: step?.previousButton?.label || 'Previous' })), !shouldClickToAdvance && (jsxRuntimeExports.jsx("button", { onClick: (e) => {
1632
1815
  e.preventDefault();
1633
1816
  e.stopPropagation();
1634
1817
  handleNext();
@@ -1651,7 +1834,18 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1651
1834
  zIndex: 99999,
1652
1835
  }, children: isLastStep
1653
1836
  ? (popoverConfig.finishLabel || 'Finish')
1654
- : (popoverConfig.nextLabel || 'Next') })] })] }), position.placement !== 'center' && (jsxRuntimeExports.jsx("div", { style: {
1837
+ : (popoverConfig.nextLabel || 'Next') })), shouldClickToAdvance && (jsxRuntimeExports.jsx("div", { style: {
1838
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
1839
+ border: '1px solid rgba(59, 130, 246, 0.3)',
1840
+ color: theme.primaryColor || '#3b82f6',
1841
+ padding: '8px 16px',
1842
+ borderRadius: '8px',
1843
+ fontSize: '14px',
1844
+ fontWeight: '500',
1845
+ display: 'flex',
1846
+ alignItems: 'center',
1847
+ gap: '6px',
1848
+ }, children: "\uD83D\uDC46 Click the highlighted element to continue" }))] })] }), position.placement !== 'center' && (jsxRuntimeExports.jsx("div", { style: {
1655
1849
  position: 'absolute',
1656
1850
  width: '12px',
1657
1851
  height: '12px',