From c10b9cc202da4510396d5742f0ad27be1ebc5798 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 28 Jun 2026 13:03:17 +0200 Subject: [PATCH] fix(map): keep the mobile GPS button above the day-detail panel (#1348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On mobile the location (GPS) FAB sat at bottom: calc(var(--bottom-nav-h) + 12px), which only clears the bottom nav. When a day is selected, DayDetailPanel slides up over the map from bottom: navh+20 and spans nearly full width at z-index 10000, covering the button's band — so the button was hidden behind it. DayDetailPanel now publishes its live measured height to a root CSS var --day-panel-h (ResizeObserver, reset to 0 on unmount), and both map renderers lift the button above the panel when it's open, reusing the hasDayDetail prop they already receive: hasDayDetail ? calc(var(--bottom-nav-h) + 20px + var(--day-panel-h) + 12px) : calc(var(--bottom-nav-h) + 12px) Applied to both the Leaflet (MapView) and GL (MapViewGL) renderers. When the panel closes, hasDayDetail is false and the offset falls back to the bottom-nav value. Desktop is unaffected — the button is mobile-only. Tests: new DayDetailPanel case asserting --day-panel-h is published and reset on unmount; client tsc clean, full client suite green (2851). --- client/src/components/Map/MapView.tsx | 7 +++++- client/src/components/Map/MapViewGL.tsx | 7 +++++- .../Planner/DayDetailPanel.test.tsx | 10 ++++++++ .../src/components/Planner/DayDetailPanel.tsx | 25 +++++++++++++++++-- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/client/src/components/Map/MapView.tsx b/client/src/components/Map/MapView.tsx index 607df1f1..57d59202 100644 --- a/client/src/components/Map/MapView.tsx +++ b/client/src/components/Map/MapView.tsx @@ -569,7 +569,12 @@ export const MapView = memo(function MapView({ // Desktop browsers only get IP-based geolocation (city-level accuracy), // so the button would be misleading. Mobile, where real GPS lives, keeps it. const isMobile = typeof window !== 'undefined' && window.innerWidth < 768 - const locationButtonBottom = 'calc(var(--bottom-nav-h, 84px) + 12px)' + // When the day-detail panel is open it slides up over the map (bottom: navh+20, + // height var(--day-panel-h)) and covers the button's band, so lift the button + // above it; otherwise keep the plain bottom-nav offset. #1348 + const locationButtonBottom = hasDayDetail + ? 'calc(var(--bottom-nav-h, 84px) + 20px + var(--day-panel-h, 0px) + 12px)' + : 'calc(var(--bottom-nav-h, 84px) + 12px)' return ( <> diff --git a/client/src/components/Map/MapViewGL.tsx b/client/src/components/Map/MapViewGL.tsx index 65e790a3..9d6f3526 100644 --- a/client/src/components/Map/MapViewGL.tsx +++ b/client/src/components/Map/MapViewGL.tsx @@ -727,7 +727,12 @@ export function MapViewGL({ // Desktop browsers only get IP-based geolocation (city-level accuracy), // so the button would be misleading. Mobile, where real GPS lives, keeps it. const isMobile = typeof window !== 'undefined' && window.innerWidth < 768 - const buttonBottom = 'calc(var(--bottom-nav-h, 84px) + 12px)' + // When the day-detail panel is open it slides up over the map (bottom: navh+20, + // height var(--day-panel-h)) and covers the button's band, so lift the button + // above it; otherwise keep the plain bottom-nav offset. #1348 + const buttonBottom = hasDayDetail + ? 'calc(var(--bottom-nav-h, 84px) + 20px + var(--day-panel-h, 0px) + 12px)' + : 'calc(var(--bottom-nav-h, 84px) + 12px)' return (
diff --git a/client/src/components/Planner/DayDetailPanel.test.tsx b/client/src/components/Planner/DayDetailPanel.test.tsx index a70fd9d5..cf9642a1 100644 --- a/client/src/components/Planner/DayDetailPanel.test.tsx +++ b/client/src/components/Planner/DayDetailPanel.test.tsx @@ -51,6 +51,16 @@ describe('DayDetailPanel', () => { expect(document.body).toBeInTheDocument(); }); + it('FE-PLANNER-DAYDETAIL-063: publishes its height to --day-panel-h and resets it on unmount (#1348)', () => { + document.documentElement.style.removeProperty('--day-panel-h'); + const { unmount } = render(); + // The panel publishes its measured height so the map's mobile GPS button can + // sit above it instead of being hidden behind it. + expect(document.documentElement.style.getPropertyValue('--day-panel-h')).not.toBe(''); + unmount(); + expect(document.documentElement.style.getPropertyValue('--day-panel-h')).toBe('0px'); + }); + it('FE-PLANNER-DAYDETAIL-002: returns null when day prop is null', () => { render(); expect(document.querySelector('[style*="position: fixed"]')).toBeNull(); diff --git a/client/src/components/Planner/DayDetailPanel.tsx b/client/src/components/Planner/DayDetailPanel.tsx index c7dad60d..71d5c93c 100644 --- a/client/src/components/Planner/DayDetailPanel.tsx +++ b/client/src/components/Planner/DayDetailPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import ReactDOM from 'react-dom' import { X, Sun, Cloud, CloudRain, CloudSnow, CloudDrizzle, CloudLightning, Wind, Droplets, Sunrise, Sunset, Hotel, Calendar, MapPin, LogIn, LogOut, Hash, Pencil, Plane, Utensils, Train, Car, Ship, Ticket, FileText, Users, ChevronsDown, ChevronsUp } from 'lucide-react' @@ -86,6 +86,27 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri updateAccommodationField, handleRemoveAccommodation, } = useDayDetail(day, days, tripId, lat, lng, language, onAccommodationChange) + // Publish the panel's live height as a root CSS var so the map's mobile GPS + // button can sit just above the panel instead of being hidden behind it (#1348). + // The card grows/shrinks (collapse, content, ≤60vh), so track it live. + const cardRef = useRef(null) + useEffect(() => { + const el = cardRef.current + if (!el) return + const root = document.documentElement + const publish = () => root.style.setProperty('--day-panel-h', `${el.offsetHeight}px`) + publish() + let ro: ResizeObserver | undefined + if (typeof ResizeObserver !== 'undefined') { + ro = new ResizeObserver(publish) + ro.observe(el) + } + return () => { + ro?.disconnect() + root.style.setProperty('--day-panel-h', '0px') + } + }, []) + if (!day) return null const formattedDate = day.date ? new Date(day.date + 'T00:00:00Z').toLocaleDateString( @@ -98,7 +119,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri return (
-