Fix proximity radar hover jitter without breaking device rendering

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 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-21 14:22:59 +00:00
parent fb2a12773a
commit 00be3e940a

View File

@@ -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();