diff --git a/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx index 20e191ba38..adea593d07 100644 --- a/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx +++ b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx @@ -1,9 +1,9 @@ /* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ import React, { useRef, type JSX, useCallback, useEffect, useState, useMemo } from "react"; import { type VirtuosoHandle, type ListRange, Virtuoso, type VirtuosoProps } from "react-virtuoso"; @@ -95,6 +95,19 @@ export interface IVirtualizedListProps extends Omit< * @returns */ onKeyDown?: (e: React.KeyboardEvent) => void; + + /** + * Optional total count of items (for virtualization with partial data loading). + * If provided, this will be used instead of items.length for the total count. + */ + totalCount?: number; + + /** + * Optional callback when the visible range of items changes. + * Useful for loading data on-demand as the user scrolls. + * @param range - The new visible range with startIndex and endIndex + */ + rangeChanged?: (range: ListRange) => void; } /** @@ -113,7 +126,17 @@ export type ScrollIntoViewOnChange = NonNullable< */ export function VirtualizedList(props: IVirtualizedListProps): React.ReactElement { // Extract our custom props to avoid conflicts with Virtuoso props - const { items, getItemComponent, isItemFocusable, getItemKey, context, onKeyDown, ...virtuosoProps } = props; + const { + items, + getItemComponent, + isItemFocusable, + getItemKey, + context, + onKeyDown, + totalCount, + rangeChanged, + ...virtuosoProps + } = props; /** Reference to the Virtuoso component for programmatic scrolling */ const virtuosoHandleRef = useRef(null); /** Reference to the DOM element containing the virtualized list */ @@ -324,6 +347,15 @@ export function VirtualizedList(props: IVirtualizedListProp [tabIndexKey, isFocused, props.context], ); + // Combine internal range tracking with optional external callback + const handleRangeChanged = useCallback( + (range: ListRange) => { + setVisibleRange(range); + rangeChanged?.(range); + }, + [rangeChanged], + ); + return ( (props: IVirtualizedListProp scrollerRef={scrollerRef} onKeyDown={keyDownCallback} context={listContext} - rangeChanged={setVisibleRange} + rangeChanged={handleRangeChanged} // virtuoso errors internally if you pass undefined. overscan={props.overscan || 0} data={props.items} + totalCount={totalCount} onFocus={onFocus} onBlur={onBlur} itemContent={getItemComponentInternal}