fix(guidance): Stop button drops taps because render() rebuilds it on every GPS fix (#179)#180
Conversation
… mid-tap
updateGuidance() is called on every GPS fix (~1 Hz on a moving
phone) and ends with render(), which does panelEl.innerHTML = ''
and recreates the Stop button. iOS Safari tracks the specific DOM
element that received touchstart — if it's removed before touchend,
no click is synthesized and the tap is silently dropped. With GPS
fixes coming faster than typical tap timing, almost every tap lands
in the rebuild window.
Switch to event delegation: a single persistent click listener on
panelEl that checks .closest('.guidance-btn') and calls onStop.
The listener survives every render; buttons can come and go without
breaking the touch path.
Also adds type="button" to the buttons defensively.
Closes #179
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code ReviewOverviewThis PR fixes a genuine iOS Safari bug: What's Good
Suggestions / Minor Notes1. Optional: guard L.DomEvent.on(c, 'click', (e: Event) => {
if ((e.target as HTMLElement | null)?.closest('.guidance-btn')) onStop();
});In practice, a 2. Only Both the Stop button (guiding state) and the Cancel button (routing state) have class 3. No new automated tests The project's test philosophy (pure functions only, no DOM/Leaflet integration) means this is expected and acceptable. The manual test plan in the PR description covers it appropriately. VerdictLGTM. The fix is correct, the approach is idiomatic, and it's the right size for the problem. The iOS tap-drop bug is a real class of issue in heavily re-rendering UIs and event delegation is the canonical solution. Ready to merge pending the manual test plan items. |
Summary
updateGuidance()is called fromonLocationFoundon every accepted GPS fix (~1 Hz on a moving phone). Its last action isrender(), which doespanelEl.innerHTML = ''and re-creates the Stop button (viaappendButton).touchstart. If that element is destroyed beforetouchendarrives, noclickis fired. With GPS fixes faster than typical tap-and-release timing, almost every tap on the Stop button lands in the rebuild window and is silently dropped.Fix
Switch to event delegation: a single persistent
clicklistener attached once topanelElat control creation. The listener checksevent.target.closest('.guidance-btn')and callsonStopwhen matched. BecausepanelElitself never gets destroyed (only its inner HTML), the listener survives every render and the button can come and go freely.Also adds
type="button"to the buttons defensively.Closes #179.
Why event delegation is the right shape
addGuidanceControlonce, and can never desynchronise.Test plan
npm run type-checkcleannpm run lintcleannpm testclean (633 tests)🤖 Generated with Claude Code