import React, { useMemo } from 'react' import { GridSyncContext } from './context' import type { GridHoverCallback, GridScrollCallback, GridScrollHeightCallback, GridScrollHeightRecord, GridScrollPosition, GridSyncState, SyncContext, } from './types' import type { GridRowId } from '../../types' export type GridSyncProviderProps = { orientation?: 'vertical' | 'horizontal' children: React.ReactNode } function useScrollState() { const ref = React.useRef(undefined) if (!ref.current) { ref.current = { scroll: { listeners: new Map(), }, height: { listeners: new Map(), }, hover: { listeners: new Map(), }, } } return ref.current } export const GridSyncProvider = ({ orientation = 'vertical', children, }: GridSyncProviderProps) => { const provider = useScrollState() const lastScrollTop = React.useRef(-1) const lastScrollLeft = React.useRef(-1) const updateScroll = React.useCallback( (id: string, position: GridScrollPosition) => { const { scrollTop, scrollLeft } = position /* Prevent reporting the same scroll multiple times */ if ( (orientation === 'vertical' && lastScrollTop.current === scrollTop) || (orientation === 'horizontal' && lastScrollLeft.current === scrollLeft) ) { return } lastScrollTop.current = scrollTop lastScrollLeft.current = scrollLeft const val = orientation === 'vertical' ? { scrollTop } : { scrollLeft } for (const [listenerId, listener] of provider.scroll.listeners) { if (listenerId !== id) { listener(val) } } }, [provider, orientation] ) const scrollSubscribe = React.useCallback( (id: string, callback: GridScrollCallback) => { provider.scroll.listeners.set(id, callback) return () => { provider.scroll.listeners.delete(id) } }, [provider] ) const heights = React.useRef([]) const updateHeight = React.useCallback( (id: string, scrollbar: number, head: number, tail: number) => { heights.current = [ ...heights.current.filter((record) => record.id !== id), { id, scrollbar, head, tail }, ] for (const [listenerId, listener] of provider.height.listeners) { if (listenerId !== id) { listener( heights.current.filter( (record) => record.id !== listenerId ) ) } } return heights.current.filter((record) => record.id !== id) }, [provider] ) const heightSubscribe = React.useCallback( (id: string, callback: GridScrollHeightCallback) => { provider.height.listeners.set(id, callback) return () => { /** * Clean up listeners */ provider.height.listeners.delete(id) /** * Clean up state */ heights.current = heights.current.filter( (record) => record.id !== id ) } }, [provider] ) const lastHoverId = React.useRef(null) const updateHover = React.useCallback( (id: string, rowId: GridRowId | null) => { if (lastHoverId.current === rowId) { return } lastHoverId.current = rowId for (const [listenerId, listener] of provider.hover.listeners) { if (listenerId !== id) { listener(rowId) } } }, [provider] ) const hoverSubscribe = React.useCallback( (id: string, callback: GridHoverCallback) => { provider.hover.listeners.set(id, callback) return () => { provider.hover.listeners.delete(id) } }, [provider] ) const state = useMemo( () => ({ enabled: true, scroll: { subscribe: scrollSubscribe, update: updateScroll, }, height: { subscribe: heightSubscribe, update: updateHeight, }, hover: { subscribe: hoverSubscribe, update: updateHover, }, }), [ scrollSubscribe, updateScroll, heightSubscribe, updateHeight, hoverSubscribe, updateHover, ] ) return ( {children} ) }