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