fix: Tour-Tooltip auf feste Breite begrenzen
Entfernt left+right-Stretching in CSS, positioniert das Tooltip horizontal am Spotlight und misst Ziele nach Navigation mit verzögerten Retries. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+6
-4
@@ -4276,10 +4276,12 @@ body.app-tour-active .app-tour-target-active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-tour-tooltip:not(.centered) {
|
.app-tour-tooltip:not(.centered) {
|
||||||
left: max(16px, env(safe-area-inset-left, 0px));
|
left: 50%;
|
||||||
right: max(16px, env(safe-area-inset-right, 0px));
|
transform: translateX(-50%);
|
||||||
width: auto;
|
}
|
||||||
max-width: none;
|
|
||||||
|
.app-tour-tooltip:not(.centered).app-tour-tooltip--anchored {
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-tour-tooltip.centered {
|
.app-tour-tooltip.centered {
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ interface SpotlightRect {
|
|||||||
|
|
||||||
const TOOLTIP_EDGE_MARGIN = 16
|
const TOOLTIP_EDGE_MARGIN = 16
|
||||||
const TOOLTIP_ESTIMATED_HEIGHT = 240
|
const TOOLTIP_ESTIMATED_HEIGHT = 240
|
||||||
|
const TOOLTIP_WIDTH = 420
|
||||||
|
|
||||||
|
function computeTooltipLeft(spotlight: SpotlightRect): number {
|
||||||
|
const tooltipWidth = Math.min(TOOLTIP_WIDTH, window.innerWidth - TOOLTIP_EDGE_MARGIN * 2)
|
||||||
|
const ideal = spotlight.left + spotlight.width / 2 - tooltipWidth / 2
|
||||||
|
const maxLeft = window.innerWidth - TOOLTIP_EDGE_MARGIN - tooltipWidth
|
||||||
|
return Math.max(TOOLTIP_EDGE_MARGIN, Math.min(ideal, maxLeft))
|
||||||
|
}
|
||||||
|
|
||||||
function buildCutoutClipPath(rect: SpotlightRect): string {
|
function buildCutoutClipPath(rect: SpotlightRect): string {
|
||||||
const right = rect.left + rect.width
|
const right = rect.left + rect.width
|
||||||
@@ -51,6 +59,7 @@ export default function AppTourOverlay() {
|
|||||||
currentStepId,
|
currentStepId,
|
||||||
currentStepIndex,
|
currentStepIndex,
|
||||||
totalSteps,
|
totalSteps,
|
||||||
|
layoutTick,
|
||||||
nextStep,
|
nextStep,
|
||||||
prevStep,
|
prevStep,
|
||||||
skipTour
|
skipTour
|
||||||
@@ -66,7 +75,10 @@ export default function AppTourOverlay() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
const updateSpotlight = () => {
|
const updateSpotlight = () => {
|
||||||
|
if (cancelled) return
|
||||||
const selector = getTourTargetSelector(currentStepId)
|
const selector = getTourTargetSelector(currentStepId)
|
||||||
if (!selector) {
|
if (!selector) {
|
||||||
setSpotlight(null)
|
setSpotlight(null)
|
||||||
@@ -78,6 +90,10 @@ export default function AppTourOverlay() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const rect = el.getBoundingClientRect()
|
const rect = el.getBoundingClientRect()
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) {
|
||||||
|
setSpotlight(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
const padding = 8
|
const padding = 8
|
||||||
setSpotlight({
|
setSpotlight({
|
||||||
top: Math.max(8, rect.top - padding),
|
top: Math.max(8, rect.top - padding),
|
||||||
@@ -90,19 +106,17 @@ export default function AppTourOverlay() {
|
|||||||
updateSpotlight()
|
updateSpotlight()
|
||||||
window.addEventListener('resize', updateSpotlight)
|
window.addEventListener('resize', updateSpotlight)
|
||||||
window.addEventListener('scroll', updateSpotlight, true)
|
window.addEventListener('scroll', updateSpotlight, true)
|
||||||
const retryDelay = getTourTargetRetryDelay(currentStepId)
|
|
||||||
const timer = window.setTimeout(updateSpotlight, retryDelay)
|
const retryDelays = [getTourTargetRetryDelay(currentStepId), 120, 280, 480]
|
||||||
const retryTimer = retryDelay > 120
|
const timers = retryDelays.map((delay) => window.setTimeout(updateSpotlight, delay))
|
||||||
? window.setTimeout(updateSpotlight, retryDelay + 180)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.clearTimeout(timer)
|
cancelled = true
|
||||||
if (retryTimer !== undefined) window.clearTimeout(retryTimer)
|
for (const timer of timers) window.clearTimeout(timer)
|
||||||
window.removeEventListener('resize', updateSpotlight)
|
window.removeEventListener('resize', updateSpotlight)
|
||||||
window.removeEventListener('scroll', updateSpotlight, true)
|
window.removeEventListener('scroll', updateSpotlight, true)
|
||||||
}
|
}
|
||||||
}, [currentStepId, isActive])
|
}, [currentStepId, isActive, layoutTick])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive) return
|
if (!isActive) return
|
||||||
@@ -138,9 +152,17 @@ export default function AppTourOverlay() {
|
|||||||
const tooltipStyle = centered
|
const tooltipStyle = centered
|
||||||
? undefined
|
? undefined
|
||||||
: spotlight
|
: spotlight
|
||||||
? { top: computeTooltipTop(spotlight) }
|
? { top: computeTooltipTop(spotlight), left: computeTooltipLeft(spotlight) }
|
||||||
: { top: '20%' }
|
: { top: '20%' }
|
||||||
|
|
||||||
|
const tooltipClassName = [
|
||||||
|
'app-tour-tooltip',
|
||||||
|
centered ? 'centered' : '',
|
||||||
|
!centered && spotlight ? 'app-tour-tooltip--anchored' : ''
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
const backdropStyle = spotlight && !centered
|
const backdropStyle = spotlight && !centered
|
||||||
? { clipPath: buildCutoutClipPath(spotlight) }
|
? { clipPath: buildCutoutClipPath(spotlight) }
|
||||||
: undefined
|
: undefined
|
||||||
@@ -165,7 +187,7 @@ export default function AppTourOverlay() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`app-tour-tooltip${centered ? ' centered' : ''}`} style={tooltipStyle}>
|
<div className={tooltipClassName} style={tooltipStyle}>
|
||||||
<button type="button" className="app-tour-close" onClick={skipTour} aria-label={t('tour.skip')}>
|
<button type="button" className="app-tour-close" onClick={skipTour} aria-label={t('tour.skip')}>
|
||||||
<X size={18} />
|
<X size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ interface AppTourContextValue {
|
|||||||
currentStepId: TourStepId | null
|
currentStepId: TourStepId | null
|
||||||
currentStepIndex: number
|
currentStepIndex: number
|
||||||
totalSteps: number
|
totalSteps: number
|
||||||
|
layoutTick: number
|
||||||
startTour: (options?: { force?: boolean; demoMode?: boolean }) => void
|
startTour: (options?: { force?: boolean; demoMode?: boolean }) => void
|
||||||
stopTour: () => void
|
stopTour: () => void
|
||||||
restartTour: () => void
|
restartTour: () => void
|
||||||
@@ -135,6 +136,7 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
const [stepIndex, setStepIndex] = useState(0)
|
const [stepIndex, setStepIndex] = useState(0)
|
||||||
const [pendingAfterLogin, setPendingAfterLogin] = useState(false)
|
const [pendingAfterLogin, setPendingAfterLogin] = useState(false)
|
||||||
const [isDemoTour, setIsDemoTour] = useState(false)
|
const [isDemoTour, setIsDemoTour] = useState(false)
|
||||||
|
const [layoutTick, setLayoutTick] = useState(0)
|
||||||
const navigationRef = useRef<TourNavigation | null>(null)
|
const navigationRef = useRef<TourNavigation | null>(null)
|
||||||
const demoContextRef = useRef<DemoTourContext | null>(null)
|
const demoContextRef = useRef<DemoTourContext | null>(null)
|
||||||
const tourModeRef = useRef<{ demoMode: boolean }>({ demoMode: false })
|
const tourModeRef = useRef<{ demoMode: boolean }>({ demoMode: false })
|
||||||
@@ -276,6 +278,8 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
if (!stepId) return
|
if (!stepId) return
|
||||||
applyStepSideEffects(stepId)
|
applyStepSideEffects(stepId)
|
||||||
scrollToCurrentTarget(stepId)
|
scrollToCurrentTarget(stepId)
|
||||||
|
const timer = window.setTimeout(() => setLayoutTick((tick) => tick + 1), 0)
|
||||||
|
return () => window.clearTimeout(timer)
|
||||||
}, [isActive, isDemoTour, stepIndex, applyStepSideEffects, scrollToCurrentTarget])
|
}, [isActive, isDemoTour, stepIndex, applyStepSideEffects, scrollToCurrentTarget])
|
||||||
|
|
||||||
const restartTour = useCallback(() => {
|
const restartTour = useCallback(() => {
|
||||||
@@ -318,6 +322,7 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
currentStepId,
|
currentStepId,
|
||||||
currentStepIndex: stepIndex,
|
currentStepIndex: stepIndex,
|
||||||
totalSteps: stepOrder.length,
|
totalSteps: stepOrder.length,
|
||||||
|
layoutTick,
|
||||||
startTour,
|
startTour,
|
||||||
stopTour,
|
stopTour,
|
||||||
restartTour,
|
restartTour,
|
||||||
@@ -342,6 +347,7 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
|
|||||||
startTour,
|
startTour,
|
||||||
stepIndex,
|
stepIndex,
|
||||||
stepOrder.length,
|
stepOrder.length,
|
||||||
|
layoutTick,
|
||||||
stopTour
|
stopTour
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user