@@ -371,8 +371,13 @@ interface BreadcrumbLocationPopoverProps {
371371 veilBoundaryRef : React . RefObject < HTMLDivElement | null >
372372}
373373
374- /** How long the reopen latch is held after a click-to-navigate, covering the route swap. */
375- const NAVIGATE_LATCH_MS = 250
374+ /**
375+ * Grace period before a hover-out dismisses the path popover. Covers the gap
376+ * the pointer crosses between the trigger and the popover content (and brief
377+ * jitter at their edges); re-entering either within this window cancels the
378+ * close. Standard hover-intent close delay — not tied to any navigation timing.
379+ */
380+ const POPOVER_CLOSE_DELAY_MS = 120
376381
377382function BreadcrumbLocationPopover ( {
378383 icon : Icon ,
@@ -382,61 +387,51 @@ function BreadcrumbLocationPopover({
382387} : BreadcrumbLocationPopoverProps ) {
383388 const [ open , setOpen ] = useState ( false )
384389 const closeTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
385- /**
386- * Suppresses reopen for the brief window between a click-to-navigate and the
387- * route swap. Navigating away tears this popover down (the list and detail
388- * views render different subtrees), so if `open` were still true the dimming
389- * veil and popover content would snap away instead of fading — a visible
390- * flash. {@link navigateAndClose} closes the popover before running the
391- * crumb's handler and latches this so the pointer still resting on the
392- * trigger can't re-fire `openPopover` mid-navigation. The latch is held by a
393- * short timer (not cleared on the next pointer exit, which fires immediately
394- * and would let the popover flash back open before the route swaps); the
395- * timer releases it so the popover keeps working when the handler does not
396- * actually navigate (e.g. an unsaved-changes guard that opens a modal).
397- */
398- const navigatingRef = useRef ( false )
399- const navigateLatchRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
400390 const rootBreadcrumb = breadcrumbs [ 0 ]
401391
402- const clearCloseTimeout = ( ) => {
392+ const cancelScheduledClose = ( ) => {
403393 if ( closeTimeoutRef . current ) {
404394 clearTimeout ( closeTimeoutRef . current )
405395 closeTimeoutRef . current = null
406396 }
407397 }
408398
399+ /**
400+ * Hover-intent open. Driven only by pointer-/keyboard-enter — never by
401+ * pointer movement. This is what makes the popover dismiss cleanly on a
402+ * click-to-navigate: a stationary click fires no enter event, so once
403+ * {@link navigateAndClose} sets `open` false nothing re-opens it before the
404+ * route swaps. (A move-driven open would re-fire under the resting cursor and
405+ * flash the popover/veil back in mid-navigation.)
406+ */
409407 const openPopover = ( ) => {
410- if ( navigatingRef . current ) return
411- clearCloseTimeout ( )
408+ cancelScheduledClose ( )
412409 setOpen ( true )
413410 }
414411
415412 const scheduleClose = ( ) => {
416- clearCloseTimeout ( )
413+ cancelScheduledClose ( )
417414 closeTimeoutRef . current = setTimeout ( ( ) => {
418415 setOpen ( false )
419416 closeTimeoutRef . current = null
420- } , 120 )
417+ } , POPOVER_CLOSE_DELAY_MS )
421418 }
422419
420+ /**
421+ * Closes the popover up front, then runs the crumb's handler. Closing first
422+ * lets the veil fade and the popover play its exit animation instead of
423+ * snapping away when navigation unmounts the header.
424+ */
423425 const navigateAndClose = ( onClick ?: ( ) => void ) => {
424426 if ( ! onClick ) return
425- navigatingRef . current = true
426- clearCloseTimeout ( )
427+ cancelScheduledClose ( )
427428 setOpen ( false )
428429 onClick ( )
429- if ( navigateLatchRef . current ) clearTimeout ( navigateLatchRef . current )
430- navigateLatchRef . current = setTimeout ( ( ) => {
431- navigatingRef . current = false
432- navigateLatchRef . current = null
433- } , NAVIGATE_LATCH_MS )
434430 }
435431
436432 useEffect ( ( ) => {
437433 return ( ) => {
438434 if ( closeTimeoutRef . current ) clearTimeout ( closeTimeoutRef . current )
439- if ( navigateLatchRef . current ) clearTimeout ( navigateLatchRef . current )
440435 }
441436 } , [ ] )
442437
@@ -453,10 +448,6 @@ function BreadcrumbLocationPopover({
453448 onBlur = { scheduleClose }
454449 onMouseEnter = { openPopover }
455450 onMouseLeave = { scheduleClose }
456- onMouseMove = { openPopover }
457- onPointerEnter = { openPopover }
458- onPointerLeave = { scheduleClose }
459- onPointerMove = { openPopover }
460451 className = { cn (
461452 chipVariants ( { flush : true } ) ,
462453 'max-w-none gap-1.5 px-2 transition-colors' ,
@@ -492,10 +483,6 @@ function BreadcrumbLocationPopover({
492483 ) }
493484 onMouseEnter = { openPopover }
494485 onMouseLeave = { scheduleClose }
495- onMouseMove = { openPopover }
496- onPointerEnter = { openPopover }
497- onPointerLeave = { scheduleClose }
498- onPointerMove = { openPopover }
499486 >
500487 < PopoverSection className = 'px-1.5 py-0.5 text-[var(--text-muted)] text-xs' >
501488 < span className = 'inline-flex items-center gap-1' >
0 commit comments