diff --git a/src/components/VirtualCardGrid.tsx b/src/components/VirtualCardGrid.tsx index 8b6ad7d2..e4231f76 100644 --- a/src/components/VirtualCardGrid.tsx +++ b/src/components/VirtualCardGrid.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll'; import { useElementClientHeightById } from '../hooks/useResizeClientHeight'; @@ -43,11 +43,40 @@ export function VirtualCardGrid({ const mainScrollViewportHeight = useElementClientHeightById(APP_MAIN_SCROLL_VIEWPORT_ID); const overscan = Math.max(2, Math.ceil(mainScrollViewportHeight / Math.max(1, rowHeightEst))); + // Vertical offset between the scroll element's top and this grid's top. + // react-virtual computes row positions relative to the scroll element, so + // without this margin a grid that sits below tall content (e.g. the + // "More by …" rail under a long tracklist) would have its first rows + // placed at negative positions and unmounted as out-of-viewport. + const [scrollMargin, setScrollMargin] = useState(0); + useLayoutEffect(() => { + if (disableVirtualization) return; + const wrap = wrapRef.current; + const scrollEl = document.getElementById(APP_MAIN_SCROLL_VIEWPORT_ID); + if (!wrap || !scrollEl) return; + const measure = () => { + const margin = + wrap.getBoundingClientRect().top + - scrollEl.getBoundingClientRect().top + + scrollEl.scrollTop; + setScrollMargin(prev => (Math.abs(prev - margin) < 1 ? prev : margin)); + }; + measure(); + const ro = new ResizeObserver(measure); + ro.observe(scrollEl); + // Anything above the grid resizing (tracklist growing, hero collapsing) + // shifts the grid down; observe the scroll content too so we re-measure. + const scrollContent = scrollEl.firstElementChild as Element | null; + if (scrollContent) ro.observe(scrollContent); + return () => ro.disconnect(); + }, [disableVirtualization, layoutSignal, virtualRowCount]); + const virtualizer = useVirtualizer({ count: disableVirtualization ? 0 : virtualRowCount, getScrollElement: () => document.getElementById(APP_MAIN_SCROLL_VIEWPORT_ID), estimateSize: () => rowHeightEst, overscan, + scrollMargin, }); useRemeasureGridVirtualizer(virtualizer, { @@ -93,6 +122,9 @@ export function VirtualCardGrid({ {virtualizer.getVirtualItems().map(vRow => { const start = vRow.index * cols; const rowItems = items.slice(start, start + cols); + // `vRow.start` is measured from the scroll element's top; our wrapper + // already sits `scrollMargin` below that, so subtract to land back + // in wrapper-local coordinates. return (
({ top: 0, left: 0, width: '100%', - transform: `translateY(${vRow.start}px)`, + transform: `translateY(${vRow.start - scrollMargin}px)`, display: 'grid', gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`, gap: gridGap,