/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { useRef, forwardRef, useImperativeHandle, useCallback, useMemo, } from 'react'; import type React from 'react'; import { VirtualizedList, type VirtualizedListRef } from './VirtualizedList.js'; import { useScrollable } from '../../contexts/ScrollProvider.js'; import { Box, type DOMElement } from 'ink'; import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js'; import { useKeypress, type Key } from '../../hooks/useKeypress.js'; type VirtualizedListProps = { data: T[]; renderItem: (info: { item: T; index: number }) => React.ReactElement; estimatedItemHeight: (index: number) => number; keyExtractor: (item: T, index: number) => string; initialScrollIndex?: number; initialScrollOffsetInIndex?: number; }; interface ScrollableListProps extends VirtualizedListProps { hasFocus: boolean; } export type ScrollableListRef = VirtualizedListRef; function ScrollableList( props: ScrollableListProps, ref: React.Ref>, ) { const { hasFocus } = props; const virtualizedListRef = useRef>(null); const containerRef = useRef(null); useImperativeHandle( ref, () => ({ scrollBy: (delta) => virtualizedListRef.current?.scrollBy(delta), scrollTo: (offset) => virtualizedListRef.current?.scrollTo(offset), scrollToEnd: () => virtualizedListRef.current?.scrollToEnd(), scrollToIndex: (params) => virtualizedListRef.current?.scrollToIndex(params), scrollToItem: (params) => virtualizedListRef.current?.scrollToItem(params), getScrollIndex: () => virtualizedListRef.current?.getScrollIndex() ?? 0, getScrollState: () => virtualizedListRef.current?.getScrollState() ?? { scrollTop: 0, scrollHeight: 0, innerHeight: 0, }, }), [], ); const getScrollState = useCallback( () => virtualizedListRef.current?.getScrollState() ?? { scrollTop: 0, scrollHeight: 0, innerHeight: 0, }, [], ); const scrollBy = useCallback((delta: number) => { virtualizedListRef.current?.scrollBy(delta); }, []); const { scrollbarColor, flashScrollbar, scrollByWithAnimation } = useAnimatedScrollbar(hasFocus, scrollBy); useKeypress( (key: Key) => { if (key.shift) { if (key.name === 'up') { scrollByWithAnimation(-1); } if (key.name === 'down') { scrollByWithAnimation(1); } } }, { isActive: hasFocus }, ); const hasFocusCallback = useCallback(() => hasFocus, [hasFocus]); const scrollableEntry = useMemo( () => ({ ref: containerRef as React.RefObject, getScrollState, scrollBy: scrollByWithAnimation, hasFocus: hasFocusCallback, flashScrollbar, }), [getScrollState, scrollByWithAnimation, hasFocusCallback, flashScrollbar], ); useScrollable(scrollableEntry, hasFocus); return ( ); } const ScrollableListWithForwardRef = forwardRef(ScrollableList) as ( props: ScrollableListProps & { ref?: React.Ref> }, ) => React.ReactElement; export { ScrollableListWithForwardRef as ScrollableList };