@@ -21,7 +21,7 @@ export interface ProductTourTooltipProps {
2121 step : ProductTourStep
2222 stepIndex : number
2323 totalSteps : number
24- targetElement : HTMLElement
24+ targetElement : HTMLElement | null
2525 onNext : ( ) => void
2626 onPrevious : ( ) => void
2727 onDismiss : ( reason : ProductTourDismissReason ) => void
@@ -112,11 +112,16 @@ export function ProductTourTooltip({
112112 const isTransitioningRef = useRef ( false )
113113 const isFirstRender = useRef ( true )
114114
115+ const isModalStep = ! targetElement
116+
115117 const updatePosition = useCallback ( ( ) => {
118+ if ( isModalStep ) {
119+ return
120+ }
116121 const rect = targetElement . getBoundingClientRect ( )
117122 setPosition ( calculateTooltipPosition ( rect ) )
118123 setSpotlightStyle ( getSpotlightStyle ( rect ) )
119- } , [ targetElement ] )
124+ } , [ targetElement , isModalStep ] )
120125
121126 useEffect ( ( ) => {
122127 const isStepChange = previousStepRef . current !== stepIndex
@@ -128,6 +133,13 @@ export function ProductTourTooltip({
128133 previousStepRef . current = stepIndex
129134 isTransitioningRef . current = true
130135
136+ if ( isModalStep ) {
137+ // Modal steps are just centered on screen - no positioning needed
138+ setTransitionState ( 'visible' )
139+ isTransitioningRef . current = false
140+ return
141+ }
142+
131143 scrollToElement ( targetElement , ( ) => {
132144 if ( previousStepRef . current !== currentStepIndex ) {
133145 return
@@ -157,6 +169,18 @@ export function ProductTourTooltip({
157169 setDisplayedStepIndex ( stepIndex )
158170 setTransitionState ( 'entering' )
159171
172+ if ( isModalStep ) {
173+ // Modal steps don't need scrolling or position calculation
174+ setTimeout ( ( ) => {
175+ if ( previousStepRef . current !== currentStepIndex ) {
176+ return
177+ }
178+ setTransitionState ( 'visible' )
179+ isTransitioningRef . current = false
180+ } , 50 )
181+ return
182+ }
183+
160184 scrollToElement ( targetElement , ( ) => {
161185 if ( previousStepRef . current !== currentStepIndex ) {
162186 return
@@ -173,10 +197,10 @@ export function ProductTourTooltip({
173197 } )
174198 } , 150 )
175199 }
176- } , [ targetElement , stepIndex , step , updatePosition ] )
200+ } , [ targetElement , stepIndex , step , updatePosition , isModalStep ] )
177201
178202 useEffect ( ( ) => {
179- if ( transitionState !== 'visible' ) {
203+ if ( transitionState !== 'visible' || isModalStep ) {
180204 return
181205 }
182206
@@ -193,7 +217,7 @@ export function ProductTourTooltip({
193217 window ?. removeEventListener ( 'scroll' , handleUpdate , true )
194218 window ?. removeEventListener ( 'resize' , handleUpdate )
195219 }
196- } , [ updatePosition , transitionState ] )
220+ } , [ updatePosition , transitionState , isModalStep ] )
197221
198222 useEffect ( ( ) => {
199223 const handleKeyDown = ( e : KeyboardEvent ) => {
@@ -228,7 +252,7 @@ export function ProductTourTooltip({
228252 '--ph-tour-border-color' : appearance . borderColor ,
229253 } as h . JSX . CSSProperties
230254
231- const isReady = transitionState !== 'initializing' && position && spotlightStyle
255+ const isReady = isModalStep || ( transitionState !== 'initializing' && position && spotlightStyle )
232256 const isVisible = transitionState === 'visible'
233257
234258 if ( ! isReady ) {
@@ -240,14 +264,66 @@ export function ProductTourTooltip({
240264 )
241265 }
242266
267+ // Modal step: centered on screen with overlay dimming, no spotlight/arrow
268+ if ( isModalStep ) {
269+ return (
270+ < div class = "ph-tour-container" style = { containerStyle } >
271+ < div class = "ph-tour-click-overlay" onClick = { handleOverlayClick } />
272+ < div class = "ph-tour-modal-overlay" />
273+ < div class = "ph-tour-tooltip ph-tour-tooltip--modal" onClick = { handleTooltipClick } >
274+ < button
275+ class = "ph-tour-dismiss"
276+ onClick = { ( ) => onDismiss ( 'user_clicked_skip' ) }
277+ aria-label = "Close tour"
278+ >
279+ { cancelSVG }
280+ </ button >
281+
282+ < div
283+ class = "ph-tour-content"
284+ dangerouslySetInnerHTML = { { __html : renderTipTapContent ( displayedStep . content ) } }
285+ />
286+
287+ < div class = "ph-tour-footer" >
288+ < span class = "ph-tour-progress" >
289+ { displayedStepIndex + 1 } of { totalSteps }
290+ </ span >
291+
292+ < div class = "ph-tour-buttons" >
293+ { ! isFirstStep && (
294+ < button class = "ph-tour-button ph-tour-button--secondary" onClick = { onPrevious } >
295+ Back
296+ </ button >
297+ ) }
298+ < button class = "ph-tour-button ph-tour-button--primary" onClick = { onNext } >
299+ { isLastStep ? 'Done' : 'Next' }
300+ </ button >
301+ </ div >
302+ </ div >
303+
304+ { ! appearance . whiteLabel && (
305+ < a
306+ href = "https://posthog.com/product-tours"
307+ target = "_blank"
308+ rel = "noopener noreferrer"
309+ class = "ph-tour-branding"
310+ >
311+ Tour by { IconPosthogLogo }
312+ </ a >
313+ ) }
314+ </ div >
315+ </ div >
316+ )
317+ }
318+
243319 return (
244320 < div class = "ph-tour-container" style = { containerStyle } >
245321 < div class = "ph-tour-click-overlay" onClick = { handleOverlayClick } />
246322
247323 < div
248324 class = "ph-tour-spotlight"
249325 style = {
250- isVisible
326+ isVisible && spotlightStyle
251327 ? spotlightStyle
252328 : {
253329 top : '50%' ,
@@ -261,15 +337,15 @@ export function ProductTourTooltip({
261337 < div
262338 class = { `ph-tour-tooltip ${ isVisible ? 'ph-tour-tooltip--visible' : 'ph-tour-tooltip--hidden' } ` }
263339 style = { {
264- top : `${ position . top } px` ,
265- left : `${ position . left } px` ,
340+ top : `${ position ! . top } px` ,
341+ left : `${ position ! . left } px` ,
266342 opacity : isVisible ? 1 : 0 ,
267343 transform : isVisible ? 'translateY(0)' : 'translateY(10px)' ,
268344 transition : 'opacity 0.15s ease-out, transform 0.15s ease-out' ,
269345 } }
270346 onClick = { handleTooltipClick }
271347 >
272- < div class = { `ph-tour-arrow ph-tour-arrow--${ getOppositePosition ( position . position ) } ` } />
348+ < div class = { `ph-tour-arrow ph-tour-arrow--${ getOppositePosition ( position ! . position ) } ` } />
273349
274350 < button class = "ph-tour-dismiss" onClick = { ( ) => onDismiss ( 'user_clicked_skip' ) } aria-label = "Close tour" >
275351 { cancelSVG }
0 commit comments