import React, { useContext, useId } from 'react' import { GridSyncContext } from './context' import type { GridScrollHeightRecord, GridScrollPosition } from './types' import { useResizeObserver } from '@planview/pv-utilities' import type { GridRowId } from '../../types' import { compensateHeadAndTail, getScrollbarHeight } from '../../utils/scroll' type ScrollBoundsType = { /** margin above scroll items (e.g. header rows) */ head: number /** margin below scroll items (e.g. footer) */ tail: number } const DEFAULT_BOUNDS: ScrollBoundsType = { head: 0, tail: 0 } const useScrollHeight = ( ref: React.RefObject, id: string, bounds: Partial ) => { const { height, enabled } = useContext(GridSyncContext) const { subscribe, update } = height const { head, tail } = React.useMemo( () => ({ ...DEFAULT_BOUNDS, ...bounds }), [bounds] ) const onResize = React.useCallback(() => { if (ref.current) { compensateHeadAndTail( ref.current, update(id, getScrollbarHeight(ref.current), head, tail), head, tail ) } }, [head, id, ref, tail, update]) const onChangeHeight = React.useCallback( (heights: GridScrollHeightRecord[]) => { if (ref.current) { compensateHeadAndTail(ref.current, heights, head, tail) } }, [tail, head, ref] ) useResizeObserver({ ref, onResize }) React.useEffect(() => { if (enabled) { return subscribe(id, onChangeHeight) } }, [id, enabled, subscribe, onChangeHeight]) } export const useScrollSync = ( ref: React.RefObject, bounds: Partial = DEFAULT_BOUNDS ) => { const id = useId() const { enabled, scroll } = useContext(GridSyncContext) const { subscribe, update } = scroll useScrollHeight(ref, id, bounds) const onChange = React.useCallback( (position: Partial) => { if (ref.current) { if (position.scrollTop !== undefined) { ref.current.scrollTop = position.scrollTop } if (position.scrollLeft !== undefined) { ref.current.scrollLeft = position.scrollLeft } } }, [ref] ) React.useEffect(() => { if (enabled) { return subscribe(id, onChange) } }, [id, enabled, subscribe, onChange]) React.useEffect(() => { if (ref.current) { const current = ref.current let animationFrame: number | null = null const onScroll = (e: Event) => { if (animationFrame !== null) { window.cancelAnimationFrame(animationFrame) animationFrame = null } const target = e.target as HTMLElement animationFrame = window.requestAnimationFrame(() => { update(id, { scrollLeft: target.scrollLeft, scrollTop: target.scrollTop, }) }) } current.addEventListener('scroll', onScroll, false) return () => { if (animationFrame) { window.cancelAnimationFrame(animationFrame) animationFrame = null } current.removeEventListener('scroll', onScroll, false) } } }, [ref, id, update]) } export const useHoverSync = (rowId: GridRowId) => { const id = useId() const [isHovered, setIsHovered] = React.useState(false) const { enabled, hover } = useContext(GridSyncContext) const { subscribe, update } = hover const onMouseEnter = React.useCallback(() => { setIsHovered(true) update(id, rowId) }, [update, id, rowId]) const onMouseLeave = React.useCallback(() => { setIsHovered(false) update(id, null) }, [update, id]) const onChange = React.useCallback( (hoveredRowId: GridRowId | null) => { setIsHovered(hoveredRowId === rowId) }, [rowId] ) React.useEffect(() => { if (enabled) { return subscribe(id, onChange) } }, [id, enabled, subscribe, onChange]) return { isHovered, onMouseEnter, onMouseLeave } }