From 9012bffabcf2c2f7ded324416e436d9f241b0dde Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 21 Apr 2026 20:21:40 +0200 Subject: [PATCH] fix(atlas): constrain bucket list width to prevent panel overflow With 30+ bucket list entries the panel expanded to near-full viewport width, elongating the Stats tab, hiding overflow entries, and covering the Leaflet zoom controls. Measure the stats content width via ResizeObserver and use it as maxWidth on the horizontal bucket row so scroll activates exactly when entries exceed the stats panel width. Also fixes the ResizeObserver test mock to use a class (matching the IntersectionObserver pattern) so the instance methods are accessible. Closes #787 --- client/src/pages/AtlasPage.tsx | 13 +++++++++++-- client/tests/setup.ts | 12 +++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/client/src/pages/AtlasPage.tsx b/client/src/pages/AtlasPage.tsx index 1d4535f3..97286e88 100644 --- a/client/src/pages/AtlasPage.tsx +++ b/client/src/pages/AtlasPage.tsx @@ -1240,6 +1240,15 @@ interface SidebarContentProps { function SidebarContent({ data, stats, countries, selectedCountry, countryDetail, resolveName, onTripClick, onUnmarkCountry, bucketList, bucketTab, setBucketTab, showBucketAdd, setShowBucketAdd, bucketForm, setBucketForm, onAddBucket, onDeleteBucket, onSearchBucket, onSelectBucketPoi, bucketSearchResults, setBucketSearchResults, bucketPoiMonth, setBucketPoiMonth, bucketPoiYear, setBucketPoiYear, bucketSearching, bucketSearch, setBucketSearch, t, dark }: SidebarContentProps): React.ReactElement { const { language } = useTranslation() + const statsContentRef = useRef(null) + const [statsWidth, setStatsWidth] = useState(undefined) + useEffect(() => { + const el = statsContentRef.current + if (!el || typeof ResizeObserver === 'undefined') return + const ro = new ResizeObserver(() => setStatsWidth(el.offsetWidth)) + ro.observe(el) + return () => ro.disconnect() + }, []) const bg = (o) => dark ? `rgba(255,255,255,${o})` : `rgba(0,0,0,${o})` const tp = dark ? '#f1f5f9' : '#0f172a' const tm = dark ? '#94a3b8' : '#64748b' @@ -1290,7 +1299,7 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail // Bucket list content const bucketContent = ( <> -
+
{bucketList.map(item => (
{(() => { @@ -1400,7 +1409,7 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail {/* Both tabs always rendered so the wider one sets the panel width */}
-
+
{/* ═══ SECTION 1: Numbers ═══ */} {/* Countries hero */} diff --git a/client/tests/setup.ts b/client/tests/setup.ts index b8b2bcd9..0c0fc012 100644 --- a/client/tests/setup.ts +++ b/client/tests/setup.ts @@ -64,11 +64,13 @@ class _MockIntersectionObserver { globalThis.IntersectionObserver = _MockIntersectionObserver as unknown as typeof IntersectionObserver; // ResizeObserver — used by resizable panels -globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})) as unknown as typeof ResizeObserver; +class _MockResizeObserver { + observe = vi.fn() + unobserve = vi.fn() + disconnect = vi.fn() + constructor(_callback: ResizeObserverCallback) {} +} +globalThis.ResizeObserver = _MockResizeObserver as unknown as typeof ResizeObserver; // URL.createObjectURL / revokeObjectURL — Node 22 URL.createObjectURL requires // a native node:buffer Blob; passing a jsdom Blob throws ERR_INVALID_ARG_TYPE.