From 00be3e940a6c0ed693e6746ccea4c0d6b5331d37 Mon Sep 17 00:00:00 2001 From: Smittix Date: Sat, 21 Feb 2026 14:22:59 +0000 Subject: [PATCH] Fix proximity radar hover jitter without breaking device rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace capture-phase mouseenter/mouseleave with bubbling mouseover/mouseout for tracking hover state in the ProximityRadar component. The capture-phase approach caused two problems: 1. Moving between sibling child elements (hit-area → dot circle) fired mouseleave, prematurely clearing isHovered and triggering a DOM rebuild that caused visible jitter. 2. When renderDevices() rebuilt innerHTML, the browser fired mouseleave for the destroyed element with relatedTarget pointing at the newly created element at the same position, leaving isHovered permanently stuck at true and suppressing all future renders. The fix uses mouseover/mouseout (which bubble) with devicesGroup.contains() to reliably detect whether the cursor genuinely left the device group, immune to innerHTML rebuilds. Fixes both WiFi and Bluetooth proximity radars as they share this component. Closes #143. Co-Authored-By: Claude Sonnet 4.6 --- static/js/components/proximity-radar.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/static/js/components/proximity-radar.js b/static/js/components/proximity-radar.js index 9cd43d1..eaf7a94 100644 --- a/static/js/components/proximity-radar.js +++ b/static/js/components/proximity-radar.js @@ -134,21 +134,28 @@ const ProximityRadar = (function() { } }); - devicesGroup.addEventListener('mouseenter', (e) => { + // mouseover/mouseout bubble, so we get events from all descendants. + // Use devicesGroup.contains(relatedTarget) to detect true entry/exit + // rather than capture-phase mouseenter/mouseleave, which can leave + // isHovered stuck when innerHTML is rebuilt under the cursor. + devicesGroup.addEventListener('mouseover', (e) => { if (e.target.closest('.radar-device')) { isHovered = true; } - }, true); // capture phase so we catch enter on child elements + }); - devicesGroup.addEventListener('mouseleave', (e) => { - if (e.target.closest('.radar-device')) { + devicesGroup.addEventListener('mouseout', (e) => { + if (!e.target.closest('.radar-device')) return; + // Only clear hover when the mouse leaves the group entirely — + // moving between sibling children keeps relatedTarget inside the group. + if (!devicesGroup.contains(e.relatedTarget)) { isHovered = false; if (renderPending) { renderPending = false; renderDevices(); } } - }, true); + }); // Add sweep animation animateSweep();