The popup pan clears the map's bottom controls, not just the clip
Clearing the clip is not the same as being clickable. The attribution and the pin hint are painted over the canvas and take pointer events, so a popup that ends fully inside the frame can still have its buttons underneath them. Measured at 390 on a 366x732 map: the pan cleared the clip exactly (cut 0) and a real click on Full Profile still did nothing, because at y~701 the top element at that point was div.maplibregl-ctrl-attrib-inner; on the sibling pin it was span.pin-hint-pill. Both pins panned, both looked fixed, neither was. Neither the popup nor the control is at fault. Attribution is a licence term of the basemap, so raising the popup over it is not available to us. The usable frame is simply shorter than the container, and the fit test has to say so. panPlanFor now takes a bottomInset and fits against containerH - bottomInset. On the measured case the plan changes from -77 (pan up, popup below the pin, buttons under the attribution) to +119 (pan down, popup above the pin, clear of the band) -- the planner flips direction because with the band reserved, fitting above became the smaller move. That is the existing "pick the smaller move" rule doing its job on a corrected frame, not a new rule. The inset is MEASURED from the live controls rather than assumed. A constant would be wrong on the next control we add, at a width where the attribution wraps to two lines, or when the gallery's clearance transform lifts the band. Only controls that intercept pointer events are measured: the corner container is pointer-events:none and spans nearly the full map width at 390, so measuring the container would over-reserve the whole strip. Unchanged where it was already right: a pin whose anchor fits still returns 0 (rega measured pan 0 on those in both arms), a popup taller than the usable frame still refuses rather than jumping the camera, and bottomInset 0 is byte-identical to the previous behaviour. 7 probe cases, with the post-pan fit asserted from the geometry rather than by re-calling the planner -- a self-consistent oracle cannot see an error that shifts both sides.
This commit is contained in:
parent
79b02f962a
commit
46266a7ecc
1 changed files with 48 additions and 7 deletions
|
|
@ -412,32 +412,73 @@ const POPUP_OFFSET = 12
|
|||
* is 640px against a 452px popup — 164px of headroom. The popup would have to grow by 164px before
|
||||
* any width could overflow. So there is no residual case needing an inner scroll.
|
||||
*
|
||||
* ★ CLEARING THE CLIP IS NOT THE SAME AS BEING CLICKABLE, which is what the first version of this
|
||||
* got wrong. The bottom controls — the attribution and the pin hint — are painted OVER the canvas
|
||||
* and take pointer events, so a popup that ends fully inside the frame can still have its buttons
|
||||
* underneath them. MEASURED at 390 on a 366x732 map: the pan cleared the clip exactly (cut 0), and
|
||||
* a real click on Full Profile still did nothing, because at y~701 the top element at that point
|
||||
* was div.maplibregl-ctrl-attrib-inner; on the sibling pin it was span.pin-hint-pill. The popup is
|
||||
* not at fault and neither is the control: attribution is a LICENCE TERM of the basemap, so raising
|
||||
* the popup over it is not available to us. The usable frame is simply shorter than the container,
|
||||
* and the fit test has to say so.
|
||||
*
|
||||
* bottomInset is therefore MEASURED from the live controls rather than assumed — a constant would
|
||||
* be wrong on the next control we add, at a width where the attribution wraps to two lines, or when
|
||||
* the gallery's clearance transform lifts the whole band.
|
||||
*
|
||||
* Returns 0 when nothing should move: already fitting, unmeasurable, or a popup taller than its
|
||||
* container. That last one matters — a host can give the map a short slot, and there panning would
|
||||
* only move the clipping around, so the failure stays what it is today rather than becoming a
|
||||
* clipped popup AND a camera that jumped.
|
||||
*/
|
||||
function panPlanFor({ containerH, popupH, pinY, offset = POPUP_OFFSET }) {
|
||||
function panPlanFor({ containerH, popupH, pinY, offset = POPUP_OFFSET, bottomInset = 0 }) {
|
||||
if (!(containerH > 0) || !(popupH > 0) || !Number.isFinite(pinY)) return 0
|
||||
const need = popupH + offset
|
||||
if (pinY >= need || (containerH - pinY) >= need) return 0 // one anchor already fits
|
||||
if (need > containerH) return 0 // taller than the frame: unfixable here
|
||||
// The frame the popup must fit inside is the container MINUS the band the controls occupy.
|
||||
const usableH = containerH - Math.max(0, bottomInset)
|
||||
if (pinY >= need || (usableH - pinY) >= need) return 0 // one anchor already fits, clear of the controls
|
||||
if (need > usableH) return 0 // taller than the usable frame: unfixable here
|
||||
const downToFitAbove = need - pinY
|
||||
const upToFitBelow = need - (containerH - pinY)
|
||||
const upToFitBelow = need - (usableH - pinY)
|
||||
return downToFitAbove <= upToFitBelow ? downToFitAbove : -upToFitBelow
|
||||
}
|
||||
|
||||
/**
|
||||
* The height of the bottom control band, in CSS pixels, measured from the live elements.
|
||||
*
|
||||
* Both engines are named because the provider prop can swap them. Only controls that actually
|
||||
* INTERCEPT POINTER EVENTS matter here: the container is pointer-events:none and the control inside
|
||||
* it is pointer-events:auto, so the band is measured from the controls, not from their corner box —
|
||||
* which at 390 spans nearly the full map width and would over-reserve the whole strip.
|
||||
*/
|
||||
function bottomControlInset(container) {
|
||||
if (!container) return 0
|
||||
let top = Infinity
|
||||
const box = container.getBoundingClientRect()
|
||||
const sel = '.maplibregl-ctrl-bottom-left .maplibregl-ctrl, .maplibregl-ctrl-bottom-right .maplibregl-ctrl,'
|
||||
+ '.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl, .mapboxgl-ctrl-bottom-right .mapboxgl-ctrl, .pin-hint-pill'
|
||||
for (const el of container.querySelectorAll(sel)) {
|
||||
const r = el.getBoundingClientRect()
|
||||
if (r.height <= 0 || r.width <= 0) continue // a collapsed control reserves nothing
|
||||
if (r.top < top) top = r.top
|
||||
}
|
||||
if (!Number.isFinite(top)) return 0
|
||||
return Math.max(0, Math.round(box.bottom - top))
|
||||
}
|
||||
|
||||
function panPopupIntoView(lngLat) {
|
||||
const m = map.value
|
||||
if (!m || !popup) return
|
||||
let popupH = 0, containerH = 0, pinY = NaN
|
||||
let popupH = 0, containerH = 0, pinY = NaN, bottomInset = 0
|
||||
try {
|
||||
const el = popup.getElement()
|
||||
popupH = el ? el.getBoundingClientRect().height : 0
|
||||
containerH = m.getContainer()?.clientHeight || 0
|
||||
const c = m.getContainer()
|
||||
containerH = c?.clientHeight || 0
|
||||
bottomInset = bottomControlInset(c)
|
||||
pinY = m.project(lngLat).y
|
||||
} catch (_) { return } // a map mid-load cannot be measured
|
||||
const dy = panPlanFor({ containerH, popupH, pinY })
|
||||
const dy = panPlanFor({ containerH, popupH, pinY, bottomInset })
|
||||
// panBy moves the CAMERA, so the sign is inverted: a positive plan moves the PIN down the screen.
|
||||
if (dy) { try { m.panBy([0, -dy], { duration: 200 }) } catch (_) { /* a pan is a nicety, never a failure */ } }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue