mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
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
This commit is contained in:
@@ -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 {
|
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 { language } = useTranslation()
|
||||||
|
const statsContentRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [statsWidth, setStatsWidth] = useState<number | undefined>(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 bg = (o) => dark ? `rgba(255,255,255,${o})` : `rgba(0,0,0,${o})`
|
||||||
const tp = dark ? '#f1f5f9' : '#0f172a'
|
const tp = dark ? '#f1f5f9' : '#0f172a'
|
||||||
const tm = dark ? '#94a3b8' : '#64748b'
|
const tm = dark ? '#94a3b8' : '#64748b'
|
||||||
@@ -1290,7 +1299,7 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail
|
|||||||
// Bucket list content
|
// Bucket list content
|
||||||
const bucketContent = (
|
const bucketContent = (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-stretch" style={{ overflowX: 'auto', padding: '0 8px' }}>
|
<div className="flex items-stretch" style={{ overflowX: 'auto', padding: '0 8px', maxWidth: statsWidth, width: '100%' }}>
|
||||||
{bucketList.map(item => (
|
{bucketList.map(item => (
|
||||||
<div key={item.id} className="group flex flex-col items-center justify-center shrink-0" style={{ padding: '8px 14px', position: 'relative', minWidth: 80 }}>
|
<div key={item.id} className="group flex flex-col items-center justify-center shrink-0" style={{ padding: '8px 14px', position: 'relative', minWidth: 80 }}>
|
||||||
{(() => {
|
{(() => {
|
||||||
@@ -1400,7 +1409,7 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail
|
|||||||
{/* Both tabs always rendered so the wider one sets the panel width */}
|
{/* Both tabs always rendered so the wider one sets the panel width */}
|
||||||
<div style={{ display: 'grid' }}>
|
<div style={{ display: 'grid' }}>
|
||||||
<div style={bucketTab === 'bucket' ? { visibility: 'hidden' as const, gridArea: '1/1' } : { gridArea: '1/1' }}>
|
<div style={bucketTab === 'bucket' ? { visibility: 'hidden' as const, gridArea: '1/1' } : { gridArea: '1/1' }}>
|
||||||
<div className="flex items-stretch justify-center">
|
<div ref={statsContentRef} className="flex items-stretch justify-center">
|
||||||
|
|
||||||
{/* ═══ SECTION 1: Numbers ═══ */}
|
{/* ═══ SECTION 1: Numbers ═══ */}
|
||||||
{/* Countries hero */}
|
{/* Countries hero */}
|
||||||
|
|||||||
@@ -64,11 +64,13 @@ class _MockIntersectionObserver {
|
|||||||
globalThis.IntersectionObserver = _MockIntersectionObserver as unknown as typeof IntersectionObserver;
|
globalThis.IntersectionObserver = _MockIntersectionObserver as unknown as typeof IntersectionObserver;
|
||||||
|
|
||||||
// ResizeObserver — used by resizable panels
|
// ResizeObserver — used by resizable panels
|
||||||
globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({
|
class _MockResizeObserver {
|
||||||
observe: vi.fn(),
|
observe = vi.fn()
|
||||||
unobserve: vi.fn(),
|
unobserve = vi.fn()
|
||||||
disconnect: vi.fn(),
|
disconnect = vi.fn()
|
||||||
})) as unknown as typeof ResizeObserver;
|
constructor(_callback: ResizeObserverCallback) {}
|
||||||
|
}
|
||||||
|
globalThis.ResizeObserver = _MockResizeObserver as unknown as typeof ResizeObserver;
|
||||||
|
|
||||||
// URL.createObjectURL / revokeObjectURL — Node 22 URL.createObjectURL requires
|
// URL.createObjectURL / revokeObjectURL — Node 22 URL.createObjectURL requires
|
||||||
// a native node:buffer Blob; passing a jsdom Blob throws ERR_INVALID_ARG_TYPE.
|
// a native node:buffer Blob; passing a jsdom Blob throws ERR_INVALID_ARG_TYPE.
|
||||||
|
|||||||
Reference in New Issue
Block a user