Skip to content

Commit 7a6cd22

Browse files
fix: flyTo breaks following mode with untilPanOrZoom (#396)
When using flyTo: true with initialZoomLevel or keepCurrentZoomLevel, the flyTo call triggered zoomstart events which set _userZoomed = true, immediately breaking following mode even though the zoom was triggered by the control itself, not by the user. Root cause: Only the fitBounds path in setView() was setting _ignoreEvent to prevent zoom/pan events from being interpreted as user actions. Fixes #314
1 parent e430c1e commit 7a6cd22

2 files changed

Lines changed: 97 additions & 17 deletions

File tree

spec/L.Control.Locate.spec.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,70 @@ describe("LocateControl", () => {
372372
control.setView();
373373
assert.strictEqual(map.getZoom(), 15);
374374
});
375+
376+
it("should not break following when flyTo is used with untilPanOrZoom", () => {
377+
const control = new LocateControl({
378+
flyTo: true,
379+
setView: "untilPanOrZoom",
380+
initialZoomLevel: 15
381+
});
382+
map.addControl(control);
383+
384+
// Activate to bind event listeners (zoomstart -> _onZoom)
385+
control._activate();
386+
387+
// Simulate first location found (user just clicked)
388+
control._justClicked = true;
389+
control._event = {
390+
latlng: { lat: 51.5, lng: -0.09 },
391+
accuracy: 100,
392+
bounds: { getSouthWest: () => ({ lat: 51.49, lng: -0.1 }), getNorthEast: () => ({ lat: 51.51, lng: -0.08 }) }
393+
};
394+
395+
control.setView();
396+
397+
// flyTo triggers zoomstart internally, but _ignoreEvent should prevent
398+
// _userZoomed from being set to true
399+
assert.strictEqual(control._userZoomed, false, "_userZoomed should remain false after flyTo");
400+
});
401+
402+
it("should set _ignoreEvent when using keepCurrentZoomLevel path", async () => {
403+
const control = new LocateControl({
404+
flyTo: true,
405+
setView: "untilPanOrZoom",
406+
keepCurrentZoomLevel: true
407+
});
408+
map.addControl(control);
409+
control._activate();
410+
411+
control._event = {
412+
latlng: { lat: 51.5, lng: -0.09 },
413+
accuracy: 100,
414+
bounds: { getSouthWest: () => ({ lat: 51.49, lng: -0.1 }), getNorthEast: () => ({ lat: 51.51, lng: -0.08 }) }
415+
};
416+
417+
// Capture _ignoreEvent state during setView
418+
let ignoreEventDuringCall = null;
419+
const originalFlyTo = map.flyTo;
420+
map.flyTo = function (...args) {
421+
ignoreEventDuringCall = control._ignoreEvent;
422+
return originalFlyTo.apply(this, args);
423+
};
424+
425+
try {
426+
control.setView();
427+
428+
// _ignoreEvent should have been true during the flyTo call
429+
assert.strictEqual(ignoreEventDuringCall, true, "_ignoreEvent should be set during flyTo call");
430+
431+
// Wait for requestAnimationFrame to complete
432+
await new Promise((resolve) => requestAnimationFrame(resolve));
433+
assert.strictEqual(control._ignoreEvent, false, "_ignoreEvent should be reset after requestAnimationFrame");
434+
} finally {
435+
// Restore original method
436+
map.flyTo = originalFlyTo;
437+
}
438+
});
375439
});
376440

377441
describe("Popup Binding", () => {

src/L.Control.Locate.js

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,8 @@ const LocateControl = Control.extend({
618618
},
619619

620620
/**
621-
* Zoom (unless we should keep the zoom level) and an to the current view.
621+
* Pan and/or zoom the map to the current location.
622+
* Respects keepCurrentZoomLevel and initialZoomLevel options.
622623
*/
623624
setView() {
624625
this._drawMarker();
@@ -628,27 +629,42 @@ const LocateControl = Control.extend({
628629
return;
629630
}
630631

631-
const latlng = this._event.latlng;
632+
const { latlng } = this._event;
633+
const fly = this.options.flyTo;
634+
let method, args;
632635

633636
if (this._justClicked && this.options.initialZoomLevel !== false) {
634-
const f = this.options.flyTo ? this._map.flyTo : this._map.setView;
635-
f.bind(this._map)(latlng, this.options.initialZoomLevel);
637+
method = fly ? "flyTo" : "setView";
638+
args = [latlng, this.options.initialZoomLevel];
636639
} else if (this._shouldKeepCurrentZoom()) {
637-
const f = this.options.flyTo ? this._map.flyTo : this._map.panTo;
638-
f.bind(this._map)(latlng);
640+
method = fly ? "flyTo" : "panTo";
641+
args = [latlng];
639642
} else {
640-
const f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
641-
// Ignore zoom events while setting the viewport as these would stop following
642-
this._ignoreEvent = true;
643-
f.bind(this._map)(this.options.getLocationBounds(this._event), {
644-
padding: this.options.circlePadding,
645-
maxZoom: this.options.initialZoomLevel || this.options.locateOptions.maxZoom
646-
});
647-
requestAnimationFrame(() => {
648-
// Wait until after the next animFrame because the flyTo can be async
649-
this._ignoreEvent = false;
650-
});
643+
method = fly ? "flyToBounds" : "fitBounds";
644+
args = [
645+
this.options.getLocationBounds(this._event),
646+
{
647+
padding: this.options.circlePadding,
648+
maxZoom: this.options.locateOptions.maxZoom
649+
}
650+
];
651651
}
652+
653+
this._setViewIgnoringEvents(method, args);
654+
},
655+
656+
/**
657+
* Execute a map view method while ignoring zoom/pan events to prevent breaking following mode.
658+
* @param {string} method - The map method name to call ('flyTo', 'setView', 'panTo', 'fitBounds', 'flyToBounds')
659+
* @param {Array} args - Arguments to pass to the method
660+
*/
661+
_setViewIgnoringEvents(method, args) {
662+
this._ignoreEvent = true;
663+
this._map[method](...args);
664+
requestAnimationFrame(() => {
665+
// Wait until after the next animFrame because flyTo/flyToBounds can be async
666+
this._ignoreEvent = false;
667+
});
652668
},
653669

654670
/**

0 commit comments

Comments
 (0)