Skip to content

Commit 09d33b7

Browse files
devvaannshabose
authored andcommitted
fix: raf batching for hover operations
1 parent cf2171d commit 09d33b7

1 file changed

Lines changed: 55 additions & 25 deletions

File tree

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)