mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-07-18 19:36:02 +00:00
fix(map): keep the mobile GPS button above the day-detail panel (#1348)
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).
This commit is contained in:
@@ -569,7 +569,12 @@ export const MapView = memo(function MapView({
|
|||||||
// Desktop browsers only get IP-based geolocation (city-level accuracy),
|
// Desktop browsers only get IP-based geolocation (city-level accuracy),
|
||||||
// so the button would be misleading. Mobile, where real GPS lives, keeps it.
|
// so the button would be misleading. Mobile, where real GPS lives, keeps it.
|
||||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -727,7 +727,12 @@ export function MapViewGL({
|
|||||||
// Desktop browsers only get IP-based geolocation (city-level accuracy),
|
// Desktop browsers only get IP-based geolocation (city-level accuracy),
|
||||||
// so the button would be misleading. Mobile, where real GPS lives, keeps it.
|
// so the button would be misleading. Mobile, where real GPS lives, keeps it.
|
||||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
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 (
|
return (
|
||||||
<div className="w-full h-full relative">
|
<div className="w-full h-full relative">
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ describe('DayDetailPanel', () => {
|
|||||||
expect(document.body).toBeInTheDocument();
|
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(<DayDetailPanel {...defaultProps} />);
|
||||||
|
// 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', () => {
|
it('FE-PLANNER-DAYDETAIL-002: returns null when day prop is null', () => {
|
||||||
render(<DayDetailPanel {...defaultProps} day={null as any} />);
|
render(<DayDetailPanel {...defaultProps} day={null as any} />);
|
||||||
expect(document.querySelector('[style*="position: fixed"]')).toBeNull();
|
expect(document.querySelector('[style*="position: fixed"]')).toBeNull();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
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'
|
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,
|
updateAccommodationField, handleRemoveAccommodation,
|
||||||
} = useDayDetail(day, days, tripId, lat, lng, language, onAccommodationChange)
|
} = 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<HTMLDivElement | null>(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
|
if (!day) return null
|
||||||
|
|
||||||
const formattedDate = day.date ? new Date(day.date + 'T00:00:00Z').toLocaleDateString(
|
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 (
|
return (
|
||||||
<div className="fixed z-50" style={{ bottom: 'calc(var(--bottom-nav-h) + 20px)', left: `calc(${leftWidth}px + (100vw - ${leftWidth}px - ${rightWidth}px) / 2)`, transform: 'translateX(-50%)', width: `min(800px, calc(100vw - ${leftWidth}px - ${rightWidth}px - 32px))`, ...(mobile ? { zIndex: 10000 } : null), ...font }}>
|
<div className="fixed z-50" style={{ bottom: 'calc(var(--bottom-nav-h) + 20px)', left: `calc(${leftWidth}px + (100vw - ${leftWidth}px - ${rightWidth}px) / 2)`, transform: 'translateX(-50%)', width: `min(800px, calc(100vw - ${leftWidth}px - ${rightWidth}px - 32px))`, ...(mobile ? { zIndex: 10000 } : null), ...font }}>
|
||||||
<div className="bg-surface-elevated" style={{
|
<div ref={cardRef} className="bg-surface-elevated" style={{
|
||||||
backdropFilter: 'blur(40px) saturate(180%)',
|
backdropFilter: 'blur(40px) saturate(180%)',
|
||||||
WebkitBackdropFilter: 'blur(40px) saturate(180%)',
|
WebkitBackdropFilter: 'blur(40px) saturate(180%)',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
|
|||||||
Reference in New Issue
Block a user