mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add list scroll persistence
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||||
|
|
||||||
|
interface UseItemListScrollPersistProps {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useItemListScrollPersist = ({ enabled }: UseItemListScrollPersistProps) => {
|
||||||
|
const [scrollOffset, setScrollOffset] = useQueryState('scrollOffset', parseAsInteger);
|
||||||
|
|
||||||
|
const handleOnScrollEnd = (offset: number) => {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
setScrollOffset(offset);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleOnScrollEnd, scrollOffset };
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useElementSize, useMergedRef } from '@mantine/hooks';
|
import { useElementSize, useMergedRef } from '@mantine/hooks';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { throttle } from 'lodash';
|
import debounce from 'lodash/debounce';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
import { AnimatePresence } from 'motion/react';
|
import { AnimatePresence } from 'motion/react';
|
||||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||||
import {
|
import {
|
||||||
@@ -9,12 +10,13 @@ import {
|
|||||||
UIEvent,
|
UIEvent,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { List, ListImperativeAPI, RowComponentProps, useListRef } from 'react-window-v2';
|
import { List, RowComponentProps, useListRef } from 'react-window-v2';
|
||||||
|
|
||||||
import { ExpandedListContainer } from '../expanded-list-container';
|
import { ExpandedListContainer } from '../expanded-list-container';
|
||||||
import styles from './item-grid-list.module.css';
|
import styles from './item-grid-list.module.css';
|
||||||
@@ -56,7 +58,7 @@ export interface ItemGridListProps {
|
|||||||
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
|
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
|
||||||
onScrollEnd?: (offset: number, handle: ItemListHandle) => void;
|
onScrollEnd?: (offset: number, handle: ItemListHandle) => void;
|
||||||
onStartReached?: (index: number, handle: ItemListHandle) => void;
|
onStartReached?: (index: number, handle: ItemListHandle) => void;
|
||||||
ref: Ref<ListImperativeAPI>;
|
ref?: Ref<ItemListHandle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ItemGridList = ({
|
export const ItemGridList = ({
|
||||||
@@ -64,12 +66,15 @@ export const ItemGridList = ({
|
|||||||
enableExpansion = true,
|
enableExpansion = true,
|
||||||
enableSelection = true,
|
enableSelection = true,
|
||||||
gap = 'sm',
|
gap = 'sm',
|
||||||
|
initialTop,
|
||||||
itemsPerRow,
|
itemsPerRow,
|
||||||
itemType,
|
itemType,
|
||||||
onEndReached,
|
onEndReached,
|
||||||
onRangeChanged,
|
onRangeChanged,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
onScrollEnd,
|
||||||
onStartReached,
|
onStartReached,
|
||||||
|
ref,
|
||||||
}: ItemGridListProps) => {
|
}: ItemGridListProps) => {
|
||||||
const itemGridRef = useListRef(null);
|
const itemGridRef = useListRef(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -110,15 +115,40 @@ export const ItemGridList = ({
|
|||||||
}
|
}
|
||||||
}, [itemGridRef, initialize]);
|
}, [itemGridRef, initialize]);
|
||||||
|
|
||||||
|
const isInitialScrollPositionSet = useRef<boolean>(false);
|
||||||
|
|
||||||
const hasExpanded = internalState.hasExpanded();
|
const hasExpanded = internalState.hasExpanded();
|
||||||
|
|
||||||
|
const handleOnScrollEnd = useCallback(
|
||||||
|
(scrollTop: number, handle: ItemListHandle) => {
|
||||||
|
onScrollEnd?.(scrollTop, handle);
|
||||||
|
},
|
||||||
|
[onScrollEnd],
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedOnScrollEnd = debounce(handleOnScrollEnd, 150);
|
||||||
|
|
||||||
const handleScroll = useCallback(
|
const handleScroll = useCallback(
|
||||||
(e: UIEvent<HTMLDivElement>) => {
|
(e: UIEvent<HTMLDivElement>) => {
|
||||||
onScroll?.(e);
|
onScroll?.(e);
|
||||||
|
debouncedOnScrollEnd(
|
||||||
|
e.currentTarget.scrollTop,
|
||||||
|
itemGridRef.current ?? (undefined as any),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[onScroll],
|
[onScroll, debouncedOnScrollEnd, itemGridRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const scrollToGridOffset = useCallback((offset: number) => {
|
||||||
|
const scrollContainer = scrollContainerRef.current?.firstElementChild as
|
||||||
|
| HTMLElement
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.scrollTo({ behavior: 'instant', top: offset });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [tableMeta, setTableMeta] = useState<null | {
|
const [tableMeta, setTableMeta] = useState<null | {
|
||||||
columnCount: number;
|
columnCount: number;
|
||||||
itemHeight: number;
|
itemHeight: number;
|
||||||
@@ -242,6 +272,47 @@ export const ItemGridList = ({
|
|||||||
itemType,
|
itemType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialTop || isInitialScrollPositionSet.current || !tableMeta?.itemHeight) return;
|
||||||
|
isInitialScrollPositionSet.current = true;
|
||||||
|
|
||||||
|
if (initialTop.type === 'offset') {
|
||||||
|
scrollToGridOffset(initialTop.to);
|
||||||
|
} else {
|
||||||
|
itemGridRef.current?.scrollToRow({
|
||||||
|
behavior: initialTop.behavior,
|
||||||
|
index: initialTop.to,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [initialTop, itemGridRef, scrollToGridOffset, tableMeta?.itemHeight]);
|
||||||
|
|
||||||
|
const imperativeHandle: ItemListHandle = useMemo(() => {
|
||||||
|
return {
|
||||||
|
clearExpanded: () => {
|
||||||
|
internalState.clearExpanded();
|
||||||
|
},
|
||||||
|
clearSelected: () => {
|
||||||
|
internalState.clearSelected();
|
||||||
|
},
|
||||||
|
getItem: (index: number) => data[index],
|
||||||
|
getItemCount: () => data.length,
|
||||||
|
getItems: () => data,
|
||||||
|
internalState,
|
||||||
|
scrollToIndex: (index: number) => {
|
||||||
|
itemGridRef.current?.scrollToRow({
|
||||||
|
align: 'smart',
|
||||||
|
behavior: 'auto',
|
||||||
|
index: Math.floor(index / (tableMeta?.columnCount || 1)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
scrollToOffset: (offset: number) => {
|
||||||
|
scrollToGridOffset(offset);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [data, internalState, scrollToGridOffset, tableMeta?.columnCount, itemGridRef]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => imperativeHandle);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.itemGridContainer}
|
className={styles.itemGridContainer}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface ItemControls {
|
|||||||
export interface ItemListComponentProps<TQuery> {
|
export interface ItemListComponentProps<TQuery> {
|
||||||
itemsPerPage?: number;
|
itemsPerPage?: number;
|
||||||
query: Omit<TQuery, 'limit' | 'startIndex'>;
|
query: Omit<TQuery, 'limit' | 'startIndex'>;
|
||||||
|
saveScrollOffset?: boolean;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user