diff --git a/components/Table/Table.tsx b/components/Table/Table.tsx index 13916d67..0f425bb1 100644 --- a/components/Table/Table.tsx +++ b/components/Table/Table.tsx @@ -1,4 +1,7 @@ import type { CSSProperties } from 'react'; +import { useEffect } from 'react'; + +import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import styles from './table.module.scss'; import { useFloatingTableHeader } from './useFloatingTableHeader'; @@ -28,16 +31,24 @@ const TableHeaderRow = ({ ); }; +const intersectionObserverOptions = {}; + export const Table = ({ data, columns }: TableProps) => { const { tableRef, floatingHeaderRef } = useFloatingTableHeader(); + const { setRef, entry } = useIntersectionObserver( + intersectionObserverOptions, + ); + + console.log({ entry }); + return (
- + diff --git a/hooks/useIntersectionObserver.tsx b/hooks/useIntersectionObserver.tsx new file mode 100644 index 00000000..628d69e6 --- /dev/null +++ b/hooks/useIntersectionObserver.tsx @@ -0,0 +1,100 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useWillUnmount } from './useWillUnmount'; + +type UseIntersectionObserverArgs = Pick; +type ObserverCallback = (entry: IntersectionObserverEntry) => void; +type Observer = { + readonly key: string; + readonly intersectionObserver: IntersectionObserver; + // eslint-disable-next-line + readonly elementToCallback: Map; +}; + +export const useIntersectionObserver = ( + options: UseIntersectionObserverArgs, +) => { + const unobserve = useRef<() => void>(); + + const [entry, setEntry] = useState(null); + + const setRef = useCallback( + (el: T) => { + console.log({ el }); + + if (unobserve.current) { + unobserve.current(); + unobserve.current = undefined; + } + + if (el && el.tagName) { + unobserve.current = observe(el, setEntry, options); + } + }, + [options], + ); + + useWillUnmount(() => { + if (unobserve.current) { + unobserve.current(); + unobserve.current = undefined; + } + }); + + if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') { + return { setRef: () => {}, entry: null } as const; + } + + return { setRef, entry } as const; +}; + +const observe = (() => { + const observers = new Map(); + + const createObserver = (options: UseIntersectionObserverArgs) => { + const key = JSON.stringify(options); + if (observers.has(key)) { + return observers.get(key)!; + } + + const elementToCallback = new Map(); + const intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + const callback = elementToCallback.get(entry.target); + if (callback) { + callback(entry); + } + }); + }, options); + + const observer: Observer = { + key, + elementToCallback, + intersectionObserver, + }; + observers.set(key, observer); + + return observer; + }; + + return ( + el: T, + callback: ObserverCallback, + options: UseIntersectionObserverArgs, + ) => { + const { key, elementToCallback, intersectionObserver } = createObserver(options); + elementToCallback.set(el, callback); + intersectionObserver.observe(el); + + const unobserve = () => { + intersectionObserver.unobserve(el); + elementToCallback.delete(el); + + if (elementToCallback.size === 0) { + intersectionObserver.disconnect(); + observers.delete(key); + } + }; + return unobserve; + }; +})(); diff --git a/hooks/useWillUnmount.tsx b/hooks/useWillUnmount.tsx new file mode 100644 index 00000000..a3161a2a --- /dev/null +++ b/hooks/useWillUnmount.tsx @@ -0,0 +1,7 @@ +import type { EffectCallback } from 'react'; +import { useEffect } from 'react'; + +/* eslint-disable react-hooks/exhaustive-deps */ +export const useWillUnmount = (cb: ReturnType) => { + useEffect(() => cb, []); +};