diff --git a/static/app/views/preprod/snapshots/main/snapshotListView.tsx b/static/app/views/preprod/snapshots/main/snapshotListView.tsx index c3f9cf6413e5b9..8ec13102f0948a 100644 --- a/static/app/views/preprod/snapshots/main/snapshotListView.tsx +++ b/static/app/views/preprod/snapshots/main/snapshotListView.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useImperativeHandle, + useLayoutEffect, useMemo, useRef, useState, @@ -86,17 +87,16 @@ const CARD_CHROME_HEIGHT = 120; const CARD_GAP = 0; const GROUP_PADDING = 0; const ROW_PADDING_BOTTOM = 16; -const LIST_CONTENT_WIDTH_ASSUMPTION = 900; +const DEFAULT_CONTENT_WIDTH = 900; const SNAPSHOT_FRAME_BORDER_WIDTH = 1; const STICKY_HEADER_BOTTOM_OVERLAP = SNAPSHOT_FRAME_BORDER_WIDTH * 2; -function estimateCardHeight(image: SnapshotImage, splitColumns: boolean) { - const columnWidth = splitColumns - ? LIST_CONTENT_WIDTH_ASSUMPTION / 2 - : LIST_CONTENT_WIDTH_ASSUMPTION; - // The uses width: auto + max-width: 100%, so it never scales up past - // its natural size. Mirror that here: only scale down when natural width - // exceeds the column. +function estimateCardHeight( + image: SnapshotImage, + splitColumns: boolean, + contentWidth: number +) { + const columnWidth = splitColumns ? contentWidth / 2 : contentWidth; const aspectHeight = image.width > 0 && image.height > 0 ? image.width <= columnWidth @@ -114,7 +114,7 @@ export function isItemUngrouped(item: SidebarItem): boolean { return !item.images[0]?.group; } -function buildGroups(items: SidebarItem[]): GroupRow[] { +function buildGroups(items: SidebarItem[], contentWidth: number): GroupRow[] { const groups: GroupRow[] = []; for (const item of items) { const cards: GroupCard[] = []; @@ -122,11 +122,11 @@ function buildGroups(items: SidebarItem[]): GroupRow[] { for (const pair of item.pairs) { cards.push({ type: 'pair-card', - id: `c:${item.key}:${pair.head_image.key}`, + id: `c:${item.key}:${pair.head_image.image_file_name}`, pair, estimatedHeight: Math.max( - estimateCardHeight(pair.head_image, true), - estimateCardHeight(pair.base_image, true) + estimateCardHeight(pair.head_image, true, contentWidth), + estimateCardHeight(pair.base_image, true, contentWidth) ), }); } @@ -134,21 +134,21 @@ function buildGroups(items: SidebarItem[]): GroupRow[] { for (const pair of item.pairs) { cards.push({ type: 'image-card', - id: `c:${item.key}:${pair.head_image.key}`, + id: `c:${item.key}:${pair.head_image.image_file_name}`, image: pair.head_image, copyData: pair, cardType: item.type, - estimatedHeight: estimateCardHeight(pair.head_image, false), + estimatedHeight: estimateCardHeight(pair.head_image, false, contentWidth), }); } } else { for (const image of item.images) { cards.push({ type: 'image-card', - id: `c:${item.key}:${image.key}`, + id: `c:${item.key}:${image.image_file_name}`, image, cardType: item.type, - estimatedHeight: estimateCardHeight(image, false), + estimatedHeight: estimateCardHeight(image, false, contentWidth), }); } } @@ -192,10 +192,40 @@ export const SnapshotListView = memo(function SnapshotListView({ onVisibleGroupChange, }: SnapshotListViewProps) { const theme = useTheme(); - const groups = useMemo(() => buildGroups(items), [items]); const scrollRef = useRef(null); const [scrollTop, setScrollTop] = useState(0); + const [contentWidth, setContentWidth] = useState(DEFAULT_CONTENT_WIDTH); + + useLayoutEffect(() => { + const el = scrollRef.current; + if (!el) { + return; + } + const style = getComputedStyle(el); + setContentWidth( + el.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) + ); + }, []); + + useEffect(() => { + const el = scrollRef.current; + if (!el) { + return; + } + const observer = new ResizeObserver(entries => { + for (const entry of entries) { + if (entry.contentRect.width > 0) { + setContentWidth(entry.contentRect.width); + } + } + }); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + const groups = useMemo(() => buildGroups(items, contentWidth), [items, contentWidth]); + const virtualizer = useVirtualizer({ count: groups.length, getScrollElement: () => scrollRef.current, @@ -204,7 +234,6 @@ export const SnapshotListView = memo(function SnapshotListView({ overscan: 5, scrollPaddingEnd: 8, }); - virtualizer.shouldAdjustScrollPositionOnItemSizeChange = () => false; // Flat (snapshotKey -> groupIdx / position) index for keyboard nav and scroll; O(1) lookups const flatIndex = useMemo(() => { @@ -339,22 +368,27 @@ export const SnapshotListView = memo(function SnapshotListView({ } didInitialScroll.current = true; virtualizer.scrollToIndex(targetIdx, {align: 'start'}); - requestAnimationFrame(() => { + + const isUngrouped = + groups[flatIndex.groupIdxByKey.get(initialSnapshotKey) ?? -1]?.isUngrouped ?? true; + + let retries = 3; + const adjustScroll = () => { const el = scrollRef.current?.querySelector( `[data-snapshot-key="${CSS.escape(initialSnapshotKey)}"]` ); - if (el) { - el.scrollIntoView({block: 'start'}); - const groupIdx = flatIndex.groupIdxByKey.get(initialSnapshotKey); - if ( - groupIdx !== undefined && - !groups[groupIdx]?.isUngrouped && - scrollRef.current - ) { - scrollRef.current.scrollTop -= SNAPSHOT_GROUP_HEADER_HEIGHT; + if (!el || !scrollRef.current) { + if (retries-- > 0) { + requestAnimationFrame(adjustScroll); } + return; } - }); + el.scrollIntoView({block: 'start'}); + if (!isUngrouped) { + scrollRef.current.scrollTop -= SNAPSHOT_GROUP_HEADER_HEIGHT; + } + }; + requestAnimationFrame(adjustScroll); }, [groups, initialSnapshotKey, flatIndex, virtualizer]); const keyNavRef = useRef({