mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
fixed lab infinite scroll
This commit is contained in:
parent
de4207ab63
commit
f7cd6316f5
4 changed files with 103 additions and 23 deletions
|
@ -1,7 +1,9 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import Masonry from 'react-masonry-css';
|
import Masonry from 'react-masonry-css';
|
||||||
|
|
||||||
|
import { useScrollEnd } from '~/hooks/dom/useScrollEnd';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
const defaultColumns = {
|
const defaultColumns = {
|
||||||
|
@ -11,12 +13,42 @@ const defaultColumns = {
|
||||||
|
|
||||||
interface ColumnsProps {
|
interface ColumnsProps {
|
||||||
cols?: Record<number, number>;
|
cols?: Record<number, number>;
|
||||||
|
onScrollEnd?: () => void;
|
||||||
|
hasMore?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Columns: FC<ColumnsProps> = ({ children, cols = defaultColumns }) => (
|
const Columns: FC<ColumnsProps> = ({
|
||||||
<Masonry className={styles.wrap} breakpointCols={cols} columnClassName={styles.column}>
|
children,
|
||||||
|
cols = defaultColumns,
|
||||||
|
onScrollEnd,
|
||||||
|
hasMore,
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [columns, setColumns] = useState<Element[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const childs = ref.current?.querySelectorAll(`.${styles.column}`);
|
||||||
|
|
||||||
|
if (!childs) return;
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => setColumns([...childs]), 150);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [ref.current]);
|
||||||
|
|
||||||
|
useScrollEnd(columns, onScrollEnd, { active: hasMore, threshold: 2 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<Masonry
|
||||||
|
className={styles.wrap}
|
||||||
|
breakpointCols={cols}
|
||||||
|
columnClassName={styles.column}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Masonry>
|
</Masonry>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export { Columns };
|
export { Columns };
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
@import "src/styles/variables";
|
@import 'src/styles/variables';
|
||||||
@import "src/styles/mixins";
|
@import 'src/styles/mixins';
|
||||||
|
|
||||||
div.wrap {
|
div.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
padding: $gap $gap * 0.5;
|
padding: $gap $gap * 0.5;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
@include tablet {
|
@include tablet {
|
||||||
padding: 0 $gap * 0.5;
|
padding: 0 $gap * 0.5;
|
||||||
|
|
|
@ -11,16 +11,16 @@ import styles from './styles.module.scss';
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
const LabGrid: FC<IProps> = memo(() => {
|
const LabGrid: FC<IProps> = memo(() => {
|
||||||
const { nodes, hasMore, loadMore, search, setSearch } = useLabContext();
|
const { nodes, hasMore, loadMore, search, setSearch, isLoading } =
|
||||||
|
useLabContext();
|
||||||
|
|
||||||
if (search && !nodes.length) {
|
if (search && !nodes.length) {
|
||||||
return <LabNoResults resetSearch={() => setSearch('')} />;
|
return <LabNoResults resetSearch={() => setSearch('')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteScroll hasMore={hasMore} loadMore={loadMore}>
|
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<Columns>
|
<Columns hasMore={hasMore && !isLoading} onScrollEnd={loadMore}>
|
||||||
{nodes.map((node) => (
|
{nodes.map((node) => (
|
||||||
<LabNode
|
<LabNode
|
||||||
node={node.node}
|
node={node.node}
|
||||||
|
@ -31,7 +31,6 @@ const LabGrid: FC<IProps> = memo(() => {
|
||||||
))}
|
))}
|
||||||
</Columns>
|
</Columns>
|
||||||
</div>
|
</div>
|
||||||
</InfiniteScroll>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
48
src/hooks/dom/useScrollEnd.ts
Normal file
48
src/hooks/dom/useScrollEnd.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { throttle } from 'throttle-debounce';
|
||||||
|
|
||||||
|
import { useWindowSize } from './useWindowSize';
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
active?: boolean;
|
||||||
|
threshold?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useScrollEnd = (
|
||||||
|
item: Element | Element[] | undefined | null,
|
||||||
|
callback?: () => void,
|
||||||
|
{ active = true, threshold = 1.5 }: Options = {},
|
||||||
|
) => {
|
||||||
|
const debouncedCallback = useMemo(
|
||||||
|
() => callback && throttle(1000, callback),
|
||||||
|
[callback],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { innerHeight } = useWindowSize();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!item || !active || !debouncedCallback) return;
|
||||||
|
|
||||||
|
const items = Array.isArray(item) ? item : [item];
|
||||||
|
const positions = items.map((it) => {
|
||||||
|
const { top, height } = it.getBoundingClientRect();
|
||||||
|
return top + window.scrollY + height;
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventHandler = () => {
|
||||||
|
if (
|
||||||
|
!positions.some(
|
||||||
|
(it) => it < window.scrollY + window.innerHeight * threshold,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
debouncedCallback();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', eventHandler);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('scroll', eventHandler);
|
||||||
|
}, [item, active, innerHeight, debouncedCallback]);
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue