iconoir/iconoir.com/components/ReactWindowScroller.tsx
2023-10-29 00:33:17 +02:00

120 lines
3.5 KiB
TypeScript

// From: https://github.com/FedericoDiRosa/react-window-scroller/blob/master/src/index.jsx
// Modified to remove scrollTo callback to support momentum scroll on iOS. We don't need it
// in this implementation anyway.
import { throttle } from 'lodash';
import React, { useEffect, useRef } from 'react';
import { GridProps, ListProps } from 'react-window';
function isHtmlElement(
element: HTMLElement | typeof window,
): element is HTMLElement {
return (element as HTMLElement).scrollTop !== undefined;
}
interface PositionKey {
x: string;
y: string;
}
const windowScrollPositionKey: PositionKey = {
y: 'pageYOffset',
x: 'pageXOffset',
};
const documentScrollPositionKey: PositionKey = {
y: 'scrollTop',
x: 'scrollLeft',
};
const getScrollPosition = (
axis: keyof PositionKey,
element?: HTMLElement | null,
): number =>
// @ts-ignore indexing as string
element?.[documentScrollPositionKey[axis] as any] ||
// @ts-ignore indexing as string
window[windowScrollPositionKey[axis] as any] ||
// @ts-ignore indexing as string
document.documentElement[documentScrollPositionKey[axis] as any] ||
// @ts-ignore indexing as string
document.body[documentScrollPositionKey[axis] as any] ||
0;
interface ChildOpts<Props extends ListProps | GridProps> {
ref: React.MutableRefObject<any>;
outerRef: React.MutableRefObject<any>;
style: object;
onScroll: Props['onScroll'];
}
interface ReactWindowScrollerProps<Props extends ListProps | GridProps> {
// eslint-disable-next-line no-unused-vars
children: (opts: ChildOpts<Props>) => React.ReactElement;
throttleTime?: number;
isGrid?: boolean;
}
export function ReactWindowScroller<
Props extends ListProps | GridProps = ListProps,
>({
children,
throttleTime = 10,
isGrid = false,
}: ReactWindowScrollerProps<Props>) {
const ref = useRef<any>();
const outerRef = useRef<HTMLElement>();
const targetElement =
typeof window === 'undefined' ? (undefined as any) : window;
useEffect(() => {
const handleWindowScroll = throttle(() => {
const rect = outerRef.current?.parentElement?.getBoundingClientRect();
const offsetTop =
(rect?.top || 0) +
(isHtmlElement(targetElement)
? targetElement.scrollTop
: targetElement.scrollY);
const offsetLeft =
(rect?.left || 0) +
(isHtmlElement(targetElement)
? targetElement.scrollLeft
: targetElement.scrollX);
const scrollTop = getScrollPosition('y') - offsetTop;
const scrollLeft = getScrollPosition('x') - offsetLeft;
if (isGrid)
ref.current && ref.current!.scrollTo({ scrollLeft, scrollTop });
if (!isGrid) ref.current && ref.current!.scrollTo(scrollTop);
}, throttleTime);
targetElement.addEventListener('scroll', handleWindowScroll);
return () => {
handleWindowScroll.cancel();
targetElement.removeEventListener('scroll', handleWindowScroll);
};
}, [throttleTime, isGrid, targetElement]);
// We are purposefully doing nothing here in order to support momentum scroll on iOS.
const onScroll = () => {};
React.useEffect(() => {
// We have to get rid of the scroll handlers here, because they will cause the list
// to go blank whenever adjusting the number of items.
ref.current._onScrollVertical = () => {};
ref.current._onScrollHorizontal = () => {};
}, [outerRef]);
return children({
ref,
outerRef,
style: {
width: isGrid ? 'auto' : '100%',
height: '100%',
display: 'inline-block',
overflow: 'hidden',
zIndex: 3,
},
onScroll,
});
}