mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix(onboarding): prevent invisible overlay blocking clicks and stuck tour
- Tie spotlight/tooltip pointer-events to isVisible so opacity:0 stops trapping taps during step transitions - Render target click-catcher only when overlay is fully visible and clear stale targetRect on step change - Retry target lookup up to 6 times and auto-skip step (or complete tour) when target is missing, instead of leaving overlay stuck invisible-but-interactive - Persist completion to localStorage before unmounting the tour, so the flag survives even if unmount throws
This commit is contained in:
@@ -43,27 +43,52 @@ export default function Onboarding({ steps, onComplete, onSkip }: OnboardingProp
|
|||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const onCompleteRef = useRef(onComplete);
|
||||||
|
useEffect(() => {
|
||||||
|
onCompleteRef.current = onComplete;
|
||||||
|
}, [onComplete]);
|
||||||
|
|
||||||
const step = steps[currentStep];
|
const step = steps[currentStep];
|
||||||
|
|
||||||
// Find and highlight target element
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const findTarget = () => {
|
let cancelled = false;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 6;
|
||||||
|
const isLastStep = currentStep === steps.length - 1;
|
||||||
|
|
||||||
|
setIsVisible(false);
|
||||||
|
setTargetRect(null);
|
||||||
|
|
||||||
|
const tryFind = () => {
|
||||||
|
if (cancelled) return;
|
||||||
const target = document.querySelector(`[data-onboarding="${step.target}"]`);
|
const target = document.querySelector(`[data-onboarding="${step.target}"]`);
|
||||||
if (target) {
|
if (target) {
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = target.getBoundingClientRect();
|
||||||
setTargetRect(rect);
|
setTargetRect(rect);
|
||||||
|
|
||||||
// Scroll element into view if needed
|
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
window.setTimeout(() => {
|
||||||
// Delay visibility for smooth animation
|
if (!cancelled) setIsVisible(true);
|
||||||
setTimeout(() => setIsVisible(true), 100);
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attempts += 1;
|
||||||
|
if (attempts < maxAttempts) {
|
||||||
|
window.setTimeout(tryFind, 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isLastStep) {
|
||||||
|
onCompleteRef.current();
|
||||||
|
} else {
|
||||||
|
setCurrentStep((prev) => Math.min(prev + 1, steps.length - 1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setIsVisible(false);
|
const timer = window.setTimeout(tryFind, 300);
|
||||||
const timer = setTimeout(findTarget, 300);
|
return () => {
|
||||||
return () => clearTimeout(timer);
|
cancelled = true;
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [step.target]);
|
}, [step.target]);
|
||||||
|
|
||||||
// Recalculate position on resize/scroll
|
// Recalculate position on resize/scroll
|
||||||
@@ -170,13 +195,22 @@ export default function Onboarding({ steps, onComplete, onSkip }: OnboardingProp
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<div className="onboarding-overlay" style={{ opacity: isVisible ? 1 : 0 }}>
|
<div className="onboarding-overlay" style={{ opacity: isVisible ? 1 : 0 }}>
|
||||||
{/* Spotlight */}
|
{/* Spotlight */}
|
||||||
<div className="onboarding-spotlight" style={getSpotlightStyle()} />
|
<div
|
||||||
|
className="onboarding-spotlight"
|
||||||
|
style={{
|
||||||
|
...getSpotlightStyle(),
|
||||||
|
pointerEvents: isVisible ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Tooltip */}
|
{/* Tooltip */}
|
||||||
<div
|
<div
|
||||||
ref={tooltipRef}
|
ref={tooltipRef}
|
||||||
className={`onboarding-tooltip tooltip-${step.placement}`}
|
className={`onboarding-tooltip tooltip-${step.placement}`}
|
||||||
style={getTooltipStyle()}
|
style={{
|
||||||
|
...getTooltipStyle(),
|
||||||
|
pointerEvents: isVisible ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Progress indicator */}
|
{/* Progress indicator */}
|
||||||
<div className="mb-4 flex items-center gap-1.5">
|
<div className="mb-4 flex items-center gap-1.5">
|
||||||
@@ -222,8 +256,8 @@ export default function Onboarding({ steps, onComplete, onSkip }: OnboardingProp
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Click handler to advance on target click */}
|
{/* Click handler to advance on target click — only when overlay is fully visible */}
|
||||||
{targetRect && (
|
{targetRect && isVisible && (
|
||||||
<div
|
<div
|
||||||
className="absolute cursor-pointer"
|
className="absolute cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -246,8 +246,8 @@ export default function Dashboard() {
|
|||||||
}, [t, subscription]);
|
}, [t, subscription]);
|
||||||
|
|
||||||
const handleOnboardingComplete = () => {
|
const handleOnboardingComplete = () => {
|
||||||
setShowOnboarding(false);
|
|
||||||
completeOnboarding();
|
completeOnboarding();
|
||||||
|
setShowOnboarding(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user