import React, { useMemo, useEffect, useState } from "react"; import styled, { css } from "styled-components"; import { hoveredCellContext } from "../context/hoveredCellContext"; import { usePropsRef } from "../context/PropsContext"; import { useScrollerRef } from "../context/ScrollerContext"; import { stickyColumnContext } from "../context/StickyColumnContext"; import { viewportStartContext, viewportSizeContext, } from "../context/ViewportContext"; import { observeResize } from "../utils/observeResize"; const debounce = (f: (...args: TArgs) => void) => { let lastArgs: TArgs; let calling = false; return (...args: TArgs) => { lastArgs = args; if (!calling) { calling = true; requestAnimationFrame(() => { calling = false; f(...lastArgs); }); } }; }; /** * This component tracks the scrolling of the table to enable row * virtualization. When the table changes size or scrolls, we need to change * which rows are rendered * This also adds shadows on the left and right sides of the table when content * is hidden and can be scrolled to */ export const Scroller = ({ children }: { children: React.ReactNode }) => { const setViewPortStart = viewportStartContext.useSetter(); const setViewportSize = viewportSizeContext.useSetter(); const setHoveredCell = hoveredCellContext.useSetter(); const stickyColumnWidths = stickyColumnContext.useState(); const { rowHeight } = usePropsRef().current; const scrollerRef = useScrollerRef(); const [isLeftShadowShowing, setIsLeftShadowShowing] = useState(false); const [isRightShadowShowing, setIsRightShadowShowing] = useState(false); useEffect(() => { if (scrollerRef.current) { setIsLeftShadowShowing(scrollerRef.current.scrollLeft > 0); setIsRightShadowShowing( scrollerRef.current.scrollWidth - scrollerRef.current.clientWidth - scrollerRef.current.scrollLeft > 0 ); } }, []); useEffect(() => { if (!scrollerRef.current) { return; } return observeResize( scrollerRef.current, debounce(() => { if (!scrollerRef.current) { return; } setViewportSize( Math.ceil(scrollerRef.current.clientHeight / rowHeight) ); }) ); }, [rowHeight]); const handleScroll = useMemo( () => debounce(() => { if (scrollerRef.current) { setViewPortStart( Math.floor(scrollerRef.current.scrollTop / rowHeight) ); setIsLeftShadowShowing(scrollerRef.current.scrollLeft > 0); setIsRightShadowShowing( scrollerRef.current.scrollWidth - scrollerRef.current.clientWidth - scrollerRef.current.scrollLeft > 0 ); } }), [] ); return ( setHoveredCell(null)} onScroll={() => { setHoveredCell(null); handleScroll(); }} > {children} acc + width, 0)} isShowing={isLeftShadowShowing} /> ); }; const ScrollerContainer = styled.div` overflow: auto; border-top: 1px solid ${({ theme }) => theme.cells.borderColor}; border-left: 1px solid ${({ theme }) => theme.cells.borderColor}; background: ${({ theme }) => theme.cells.backgroundColor}; `; const ContentWrapper = styled.div` min-width: min-content; min-height: min-content; position: relative; `; const ShadowAbsoluteWrapper = styled.div` position: absolute; pointer-events: none; top: 0; bottom: 0; left: 0; right: 0; display: flex; z-index: 10; justify-content: space-between; `; const ScrollShadowComponent = styled.div<{ left?: number; right?: number; isShowing: boolean; }>` ${({ left, right, theme }) => { if (left !== undefined) { return css` left: ${left}px; background: linear-gradient( to left, transparent, ${theme.scrollerShadow.color} ); `; } if (right !== undefined) { return css` right: ${right}px; background: linear-gradient( to right, transparent, ${theme.scrollerShadow.color} ); `; } return null; }} opacity: ${({ isShowing }) => (isShowing ? 1 : 0)}; position: sticky; bottom: 0; height: 100%; width: ${({ theme }) => theme.scrollerShadow.width}px; transition: opacity 0.2s ease; `;