@@ -34,6 +34,8 @@ function RemoteFunctions(config = {}) {
3434 let _cssSelectorHighlight ; // temporary highlight for CSS selector matches in edit mode
3535 let _hoverLockTimer = null ;
3636 let _cssSelectorHighlightTimer = null ;
37+ let _lastHoverTarget = null ; // tracks the element currently under the mouse (for same-element skip)
38+ let _pendingHoverRAF = null ; // pending requestAnimationFrame ID for hover updates
3739
3840 // this will store the element that was clicked previously (before the new click)
3941 // we need this so that we can remove click styling from the previous element when a new element is clicked
@@ -445,6 +447,44 @@ function RemoteFunctions(config = {}) {
445447 return getHighlightMode ( ) !== "click" ;
446448 }
447449
450+ /**
451+ * Applies the current hover state in a single batched DOM update.
452+ * Called once per animation frame via requestAnimationFrame.
453+ * _lastHoverTarget holds the element to highlight (or null to clear).
454+ */
455+ function _applyHoverState ( ) {
456+ _pendingHoverRAF = null ;
457+
458+ if ( ! _hoverHighlight || ! shouldShowHighlightOnHover ( ) ) {
459+ return ;
460+ }
461+
462+ _hoverHighlight . clear ( ) ;
463+ const hoverBoxHandler = LivePreviewView . getToolHandler ( "HoverBox" ) ;
464+ if ( hoverBoxHandler ) {
465+ hoverBoxHandler . dismiss ( ) ;
466+ }
467+
468+ const element = _lastHoverTarget ;
469+ // Show hover overlay + hover box only for non-selected elements
470+ if ( element && element !== previouslySelectedElement ) {
471+ _hoverHighlight . add ( element ) ;
472+ if ( hoverBoxHandler ) {
473+ hoverBoxHandler . createHoverBox ( element ) ;
474+ }
475+ }
476+ }
477+
478+ /**
479+ * Schedules a hover state update for the next animation frame.
480+ * Multiple calls within one frame collapse into a single DOM update.
481+ */
482+ function _scheduleHoverUpdate ( ) {
483+ if ( ! _pendingHoverRAF ) {
484+ _pendingHoverRAF = requestAnimationFrame ( _applyHoverState ) ;
485+ }
486+ }
487+
448488 function onElementHover ( event ) {
449489 // don't want highlighting and stuff when auto scrolling or when dragging (svgs)
450490 // for dragging normal html elements its already taken care of...so we just add svg drag checking
@@ -460,30 +500,19 @@ function RemoteFunctions(config = {}) {
460500 return false ;
461501 }
462502
503+ // Same element as last hover — nothing changed, skip entirely
504+ if ( element === _lastHoverTarget ) {
505+ return ;
506+ }
507+ _lastHoverTarget = element ;
508+
463509 // if _hoverHighlight is uninitialized, initialize it
464510 if ( ! _hoverHighlight && shouldShowHighlightOnHover ( ) ) {
465511 _hoverHighlight = new Highlight ( true ) ;
466512 }
467513
468- // this is to check the user's settings, if they want to show the elements highlights on hover or click
469514 if ( _hoverHighlight && shouldShowHighlightOnHover ( ) ) {
470- _hoverHighlight . clear ( ) ;
471-
472- // Skip hover overlay for the currently click-selected element.
473- // It already has its own overlay from the click/selection flow,
474- // and adding hover state on top would stack duplicate overlays.
475- if ( element !== previouslySelectedElement ) {
476- _hoverHighlight . add ( element ) ;
477- }
478-
479- // Show minimal hover tooltip (tag + dimensions)
480- const hoverBoxHandler = LivePreviewView . getToolHandler ( "HoverBox" ) ;
481- if ( hoverBoxHandler ) {
482- hoverBoxHandler . dismiss ( ) ;
483- if ( element !== previouslySelectedElement ) {
484- hoverBoxHandler . createHoverBox ( element ) ;
485- }
486- }
515+ _scheduleHoverUpdate ( ) ;
487516 }
488517 }
489518
@@ -495,14 +524,9 @@ function RemoteFunctions(config = {}) {
495524 // Use isElementInspectable (not isElementEditable) so that JS-rendered
496525 // elements also get their hover highlight and hover box properly dismissed.
497526 if ( LivePreviewView . isElementInspectable ( element ) && element . nodeType === Node . ELEMENT_NODE ) {
498- // this is to check the user's settings, if they want to show the elements highlights on hover or click
499527 if ( _hoverHighlight && shouldShowHighlightOnHover ( ) ) {
500- _hoverHighlight . clear ( ) ;
501- // dismiss the hover box
502- const hoverBoxHandler = LivePreviewView . getToolHandler ( "HoverBox" ) ;
503- if ( hoverBoxHandler ) {
504- hoverBoxHandler . dismiss ( ) ;
505- }
528+ _lastHoverTarget = null ;
529+ _scheduleHoverUpdate ( ) ;
506530 }
507531 }
508532 }
@@ -574,6 +598,12 @@ function RemoteFunctions(config = {}) {
574598 function disableHoverListeners ( ) {
575599 window . document . removeEventListener ( "mouseover" , onElementHover ) ;
576600 window . document . removeEventListener ( "mouseout" , onElementHoverOut ) ;
601+ // Cancel any pending rAF hover update so stale callbacks don't fire
602+ if ( _pendingHoverRAF ) {
603+ cancelAnimationFrame ( _pendingHoverRAF ) ;
604+ _pendingHoverRAF = null ;
605+ }
606+ _lastHoverTarget = null ;
577607 }
578608
579609 function enableHoverListeners ( ) {
0 commit comments