import { useCallback, useContext, useLayoutEffect, useRef } from 'react' import { TerminalSizeContext } from '../components/TerminalSizeContext.js' import type { DOMElement } from '../dom.js' type ViewportEntry = { /** * Whether the element is currently within the terminal viewport */ isVisible: boolean } /** * Hook to detect if a component is within the terminal viewport. * * Returns a callback ref and a viewport entry object. * Attach the ref to the component you want to track. */ export function useTerminalViewport(): [ ref: (element: DOMElement | null) => void, entry: ViewportEntry, ] { const terminalSize = useContext(TerminalSizeContext) const elementRef = useRef(null) const entryRef = useRef({ isVisible: true }) const setElement = useCallback((el: DOMElement | null) => { elementRef.current = el }, []) useLayoutEffect(() => { const element = elementRef.current if (!element?.yogaNode || !terminalSize) { return } const height = element.yogaNode.getComputedHeight() const rows = terminalSize.rows let absoluteTop = element.yogaNode.getComputedTop() let parent: DOMElement | undefined = element.parentNode let root = element.yogaNode while (parent) { if (parent.yogaNode) { absoluteTop += parent.yogaNode.getComputedTop() root = parent.yogaNode } if (parent.scrollTop) absoluteTop -= parent.scrollTop parent = parent.parentNode } const screenHeight = root.getComputedHeight() const bottom = absoluteTop + height const cursorRestoreScroll = screenHeight > rows ? 1 : 0 const viewportY = Math.max(0, screenHeight - rows) + cursorRestoreScroll const viewportBottom = viewportY + rows const visible = bottom > viewportY && absoluteTop < viewportBottom if (visible !== entryRef.current.isVisible) { entryRef.current = { isVisible: visible } } }) return [setElement, entryRef.current] }