A collapsed legend reopens itself after you close a people-group detail

Collapse the legend, tap a pin, then close the detail: the legend comes back
open, though nothing asked it to. Reported against the leaf research map;
reproduced at 1440 in en, collapsed-first.

selectPeopleGroup saved the pre-detail legend state on every call that carried
a group, not only on the call that entered detail. The second call therefore
saved the 'open' the first call had just forced, on top of the 'collapsed' it
correctly saved on entry, and closing the detail restored that 'open'.

The measured part is why a second call happens at all. The pin click handler is
registered per map layer, and every visible pin has an invisible hitbox twin at
the same point -- same source, larger radius, added alongside it so taps are
easier to land. A layer-scoped click fires once per layer with a feature under
the point, so a single pin click calls selectPeopleGroup exactly twice, 3ms
apart. Measured on two different pins, both reporting both layers at the click
point; never more than twice. So ONE pick was already enough to lose the
collapsed state -- it never needed two, which is why this reproduced every time
rather than intermittently.

The fix is to make the method idempotent: save only when entering detail (no
group was selected before), restore only what was actually saved, then clear it
so a later detail cannot inherit a stale value. The duplicate binding is a real
defect in its own right and is tracked separately; this method should not have
depended on being called exactly once.

Verified two-sided against the shipped source, driving the real method body
rather than a copy: seven scenarios pass on the fix and four fail with the
previous body restored, while the scenarios that must be insensitive stay green
both ways. The poisoned run reproduces the browser measurement value for value
-- saved collapsed, overwritten to open by the second call, restored open.
Trace in _qa-cycle so the next reader can re-run it.
This commit is contained in:
Doxa Maps 2026-09-29 11:43:24 +00:00
parent 9226aa7cff
commit 5520587417

View file

@ -552,15 +552,37 @@ export const useUIStore = defineStore('ui', {
* @param {object|null} peopleGroup - People group data object or null
*/
selectPeopleGroup(peopleGroup) {
// ★ SAVE ONLY ON ENTERING DETAIL, AND ONLY ONCE. This used to save on every call with a
// group, so a SECOND call while a detail was already open stored the 'open' this method
// had just forced, over the 'collapsed' it saved on entry — and the later clear restored
// 'open'. A reader who collapsed the legend, opened a detail and closed it got the
// legend back open, having never asked for it.
//
// ⚠ ONE PICK IS NOT ONE CALL — MEASURED, NOT ASSUMED. The pin click handler is
// registered per map layer, and every visible pin has an invisible hitbox twin at the
// same point (same source, larger radius, added alongside it) so that taps are easier
// to land. A layer-scoped click fires once per layer with a feature under the point,
// so EVERY pin click calls this exactly twice, 3ms apart — measured on the leaf at
// 1440 on 2026-09-29, on two different pins. One pick was therefore already enough to
// corrupt the saved state; it never needed two.
//
// Guarding on `entering` rather than on call count is what makes that harmless: this
// stays idempotent for as long as one detail is open, whatever the caller count. The
// duplicate binding is a real defect and is being fixed separately, but this method
// should not have depended on being called once in the first place.
const entering = peopleGroup && !this.selectedPeopleGroup;
this.selectedPeopleGroup = peopleGroup;
if (peopleGroup) {
this._legendStateBeforeDetail = this.legendState;
if (entering) this._legendStateBeforeDetail = this.legendState;
this.legendState = 'open';
this._customLegendHeight = null;
this.setLegendMode('detail');
} else {
this.legendState = this._legendStateBeforeDetail || 'open';
// Restore only what was actually saved, then forget it: a stale value left here
// would be applied to a later detail that never recorded one.
if (this._legendStateBeforeDetail) this.legendState = this._legendStateBeforeDetail;
this._legendStateBeforeDetail = null;
this._customLegendHeight = null;
this.setLegendMode('prayer');
}