fix(geo): AOI map blank — RAF poll до появления div + shrink-0 защита

DevTools на пользовательском скрине показал:
- Map div СУЩЕСТВУЕТ в DOM (display:block, height:520 inline)
- Реальный размер 974×254 — flex-shrink порезал до ~50% запрошенного
  потому что parent (Modal body) overflow-hidden + max-h:calc(100vh-2rem)
  на узком viewport (497px) не вмещал 520+ контента.
- Класса 'leaflet-container' на div НЕТ — L.map() так и не вызвался.

Корень: Modal от @nstart/ui рендерит children только после
useAnimatedPresence фликает isMounted=true (на след. рендер после
isOpen=true). Мой useEffect фаерится РАНЬШЕ — div ещё не в DOM,
mapDivRef.current=null. Старый guard 'if (!div) return' выходил
БЕЗ schedule RAF — больше никогда не пробовал.

Фикс в tryInit: schedule RAF и для null div, и для 0-size. Cap 100
кадров (~1.6s). Это finally доводит до L.map() когда div появится
и получит размер.

Plus: shrink-0 + min-height: 520 на map div чтобы flex-shrink не
ужимал до 254px на узких viewport. Visual quality 520px stable.
This commit is contained in:
Zimin A.N.
2026-05-06 22:12:17 +03:00
parent 325838cd12
commit 9aa3d842d2
@@ -62,14 +62,21 @@ export const AoiPickerDialog = ({ isOpen, onClose, onApply, initial }: Props) =>
const tryInit = () => {
if (cancelled) return
if (mapRef.current) return // уже init'нут
const div = mapDivRef.current
if (!div || mapRef.current) return
// Defer пока container не имеет реальный размер — RAF до 100 кадров
// (~1.6s @ 60fps), потом сдаёмся и init'им как есть (fallback).
if ((div.clientWidth === 0 || div.clientHeight === 0) && rafId < 100) {
// Modal от @nstart/ui рендерит children только после useAnimatedPresence
// флипает isMounted=true (на след. рендер после isOpen=true). Наш useEffect
// фаерится РАНЬШЕ — div ещё не в DOM. Plus modal transform animation
// изначально даёт 0×0 размер. Poll'им RAF до тех пор пока:
// - div существует (ref populated на render когда modal mounted'нул его)
// - clientWidth и clientHeight > 0 (после layout & transform settled)
// Cap на 100 кадров (~1.6s) — fallback если что-то совсем странное.
if (!div || div.clientWidth === 0 || div.clientHeight === 0) {
if (rafId < 100) {
rafId += 1
requestAnimationFrame(tryInit)
}
return
}
@@ -180,12 +187,12 @@ export const AoiPickerDialog = ({ isOpen, onClose, onApply, initial }: Props) =>
<div
ref={mapDivRef}
className="rounded-sm border border-regolith"
// Hardcoded pixel height + block display — full decoupling от flex/vh.
// Leaflet требует non-zero containerSize в момент L.map() create().
// 60vh раньше иногда читался как 0 во время Modal transform animation;
// 520px стабильно.
style={{ display: 'block', width: '100%', height: 520 }}
// shrink-0 защищает от flex-shrink в overflow-hidden parent body Modal'а:
// на узком viewport (height < 700px) panel max-h сжимал body, flex-shrink
// обрезал map с 520px до ~250px (DevTools confirmed). shrink-0 + min-h
// фиксируют 520 даже когда panel переполнен.
className="rounded-sm border border-regolith shrink-0"
style={{ display: 'block', width: '100%', height: 520, minHeight: 520 }}
aria-label={t('aoi.dialog.mapAriaLabel')}
/>