From c5cd71c8c3d82d33ec1cf3ef59478a32b9f5f16c Mon Sep 17 00:00:00 2001 From: jeffvli Date: Fri, 14 Nov 2025 01:26:56 -0800 Subject: [PATCH] add scrollToIndex alignment to lists --- .../item-grid-list/item-grid-list.tsx | 22 ++++- .../item-table-list/item-table-list.tsx | 87 +++++++++++++++++-- src/renderer/components/item-list/types.ts | 5 +- 3 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx index f6df65b57..145e4322d 100644 --- a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx +++ b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx @@ -360,10 +360,24 @@ export const ItemGridList = ({ const controls = useDefaultItemListControls(); const scrollToIndex = useCallback( - (index: number) => { + ( + index: number, + options?: { align?: 'bottom' | 'center' | 'top'; behavior?: 'auto' | 'smooth' }, + ) => { if (!listRef.current || !tableMeta) return; const row = Math.floor(index / tableMeta.columnCount); - listRef.current.scrollToItem(row, 'smart'); + + // Map alignment options to react-window's alignment + let alignment: 'auto' | 'center' | 'end' | 'smart' | 'start' = 'smart'; + if (options?.align === 'top') { + alignment = 'start'; + } else if (options?.align === 'center') { + alignment = 'center'; + } else if (options?.align === 'bottom') { + alignment = 'end'; + } + + listRef.current.scrollToItem(row, alignment); }, [tableMeta], ); @@ -580,8 +594,8 @@ export const ItemGridList = ({ const imperativeHandle: ItemListHandle = useMemo(() => { return { internalState, - scrollToIndex: (index: number) => { - scrollToIndex(index); + scrollToIndex: (index: number, options?: { align?: 'bottom' | 'center' | 'top' }) => { + scrollToIndex(index, options); }, scrollToOffset: (offset: number) => { scrollToOffset(offset); diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.tsx b/src/renderer/components/item-list/item-table-list/item-table-list.tsx index 3e81f48b6..9b05b79f8 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list.tsx @@ -703,14 +703,16 @@ export const ItemTableList = ({ | HTMLDivElement | undefined; + const behavior = 'instant'; + if (mainContainer) { - mainContainer.scrollTo({ behavior: 'instant', top: offset }); + mainContainer.scrollTo({ behavior, top: offset }); } if (pinnedLeftContainer) { - pinnedLeftContainer.scrollTo({ behavior: 'instant', top: offset }); + pinnedLeftContainer.scrollTo({ behavior, top: offset }); } if (pinnedRightContainer) { - pinnedRightContainer.scrollTo({ behavior: 'instant', top: offset }); + pinnedRightContainer.scrollTo({ behavior, top: offset }); } }, []); @@ -781,11 +783,80 @@ export const ItemTableList = ({ ); const scrollToTableIndex = useCallback( - (index: number) => { - const offset = calculateScrollTopForIndex(index); + (index: number, options?: { align?: 'bottom' | 'center' | 'top' }) => { + const mainContainer = rowRef.current?.childNodes[0] as HTMLDivElement | undefined; + if (!mainContainer) return; + + const viewportHeight = mainContainer.clientHeight; + const align = options?.align || 'top'; + + // Calculate the base scroll offset (top of the row) + let offset = calculateScrollTopForIndex(index); + + // Calculate row height for the target index + const adjustedIndex = enableHeader ? Math.max(0, index - 1) : index; + const mockCellProps: TableItemProps = { + cellPadding, + columns: parsedColumns, + controls: {} as ItemControls, + data: enableHeader ? [null, ...data] : data, + enableAlternateRowColors, + enableExpansion, + enableHeader, + enableHorizontalBorders, + enableRowHoverHighlight, + enableSelection, + enableVerticalBorders, + getRowHeight: () => DEFAULT_ROW_HEIGHT, + internalState: {} as ItemListStateActions, + itemType, + playerContext, + size, + tableId, + }; + + let targetRowHeight: number; + if (typeof rowHeight === 'number') { + targetRowHeight = rowHeight; + } else if (typeof rowHeight === 'function') { + targetRowHeight = rowHeight(adjustedIndex, mockCellProps); + } else { + targetRowHeight = DEFAULT_ROW_HEIGHT; + } + + // Adjust offset based on alignment + if (align === 'center') { + offset = offset - viewportHeight / 2 + targetRowHeight / 2; + } else if (align === 'bottom') { + offset = offset - viewportHeight + targetRowHeight; + } + // 'top' uses the base offset + + // Ensure offset is not negative + offset = Math.max(0, offset); + scrollToTableOffset(offset); }, - [calculateScrollTopForIndex, scrollToTableOffset], + [ + calculateScrollTopForIndex, + scrollToTableOffset, + enableHeader, + cellPadding, + parsedColumns, + data, + enableAlternateRowColors, + enableExpansion, + enableHorizontalBorders, + enableRowHoverHighlight, + enableSelection, + enableVerticalBorders, + itemType, + playerContext, + size, + tableId, + DEFAULT_ROW_HEIGHT, + rowHeight, + ], ); const [initialize] = useOverlayScrollbars({ @@ -1280,8 +1351,8 @@ export const ItemTableList = ({ const imperativeHandle: ItemListHandle = useMemo(() => { return { internalState, - scrollToIndex: (index: number) => { - scrollToTableIndex(enableHeader ? index + 1 : index); + scrollToIndex: (index: number, options?: { align?: 'bottom' | 'center' | 'top' }) => { + scrollToTableIndex(enableHeader ? index + 1 : index, options); }, scrollToOffset: (offset: number) => { scrollToTableOffset(offset); diff --git a/src/renderer/components/item-list/types.ts b/src/renderer/components/item-list/types.ts index 26a3c2a0f..77a35287e 100644 --- a/src/renderer/components/item-list/types.ts +++ b/src/renderer/components/item-list/types.ts @@ -54,7 +54,10 @@ export interface ItemListGridComponentProps extends ItemListComponentPro export interface ItemListHandle { internalState: ItemListStateActions; - scrollToIndex: (index: number, options?: { behavior?: 'auto' | 'smooth' }) => void; + scrollToIndex: ( + index: number, + options?: { align?: 'top' | 'bottom' | 'center'; behavior?: 'auto' | 'smooth' }, + ) => void; scrollToOffset: (offset: number, options?: { behavior?: 'auto' | 'smooth' }) => void; }