import type { VirtualItem } from '@tanstack/react-virtual' import type { GridScrollHeightRecord } from '../context/grid-sync-context' export const isInViewVertical = ( element: VirtualItem | undefined, scroller: HTMLDivElement | null, headerHeight: number, footerHeight: number ): boolean => { if (!element || !scroller) { return false } const scrollTop = scroller.scrollTop const availableHeight = scroller.clientHeight - footerHeight - headerHeight const scrollEnd = scrollTop + availableHeight if (availableHeight > 0) { if (element.start < scrollTop) { return false } else if (element.end > scrollEnd) { return false } } return true } export const isInViewHorizontal = ( element: [start: number, end: number] | undefined, scroller: HTMLDivElement | null, stickyContentLeft: number, stickyContentRight: number ): boolean => { if (!element || !scroller) { return false } const [start, end] = element const scrollLeft = scroller.scrollLeft const availableWidth = scroller.clientWidth - (stickyContentLeft + stickyContentRight) const scrollEnd = scrollLeft + availableWidth const adjustedStart = start - stickyContentLeft const adjustedEnd = end - stickyContentLeft if (availableWidth > 0) { if (adjustedStart < scrollLeft) { return false } else if (adjustedEnd > scrollEnd) { return false } } return true } export const getColumnOffset = ( columnIndex: number, columnWidths: number[] ) => { const columnWidth = columnWidths[columnIndex] const columnOffset = columnWidths .slice(0, columnIndex) .reduce((a, b) => a + b, 0) return [columnOffset, columnOffset + columnWidth] as const } export const hasScrollbar = (el: HTMLElement) => !!(el.offsetHeight - el.clientHeight) export const getScrollbarHeight = (el: HTMLElement) => el.offsetHeight - el.clientHeight /** * If any scrollbar-, head- or tail-heights need to be added as css custom properties so element can adjust. */ export const compensateHeadAndTail = ( el: HTMLElement, heights: GridScrollHeightRecord[], headIncoming: number, tailIncoming: number ) => { const { scrollbar, head, tail } = heights.reduce( (prev, current) => ({ scrollbar: Math.max(prev.scrollbar, current.scrollbar), head: Math.max(prev.head, current.head), tail: Math.max(prev.tail, current.tail), }), { scrollbar: 0, head: 0, tail: 0 } ) const newHead = `${Math.max(head - headIncoming, 0)}px` if (el.style.getPropertyValue('--grid-scroll-head') !== newHead) { el.style.setProperty('--grid-scroll-head', newHead) } const compensatedTail = Math.max(tail - tailIncoming, 0) + Math.max(scrollbar - getScrollbarHeight(el), 0) const newTail = `${compensatedTail}px` if (el.style.getPropertyValue('--grid-scroll-tail') !== newTail) { el.style.setProperty('--grid-scroll-tail', newTail) } } const RETRY_LIMIT = 10 // Number of attempts to verify scroll position const DIFF_THRESHOLD = 2 // Acceptable difference in pixels const shouldRetryScroll = ( desiredScrollTop: number, actualScrollTop: number ): boolean => { const difference = Math.abs(desiredScrollTop - actualScrollTop) return difference > DIFF_THRESHOLD } export const scrollToRowIndex = ( rowIndex: number, rowHeight: number, rowTotalSize: number, scrollContainer: HTMLDivElement | null, headerHeight: number, bottomHeight: number ) => { if (!scrollContainer) return if (rowIndex < 0 || rowHeight <= 0 || rowTotalSize <= 0) { return } let containerHeight = scrollContainer.clientHeight - headerHeight - bottomHeight if (containerHeight <= 0) { return } const performScroll = (attempt = 0): void => { let scrollTop = rowIndex * rowHeight const maxScrollTop = Math.max(0, rowTotalSize - containerHeight) scrollTop = Math.max(0, Math.min(scrollTop, maxScrollTop)) scrollContainer.scrollTo({ top: scrollTop }) // Verify scroll position and retry if necessary if (attempt < RETRY_LIMIT) { requestAnimationFrame(() => { const actualScrollTop = scrollContainer.scrollTop containerHeight = scrollContainer.clientHeight - headerHeight - bottomHeight if (shouldRetryScroll(scrollTop, actualScrollTop)) { performScroll(attempt + 1) } }) } } performScroll() }