diff --git a/src/components/MangaGrid.tsx b/src/components/MangaGrid.tsx index 675127c1..2342e49c 100644 --- a/src/components/MangaGrid.tsx +++ b/src/components/MangaGrid.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { ForwardedRef, forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import Grid, { GridTypeMap } from '@mui/material/Grid'; import { Box, Typography } from '@mui/material'; import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso'; @@ -73,126 +73,139 @@ type DefaultGridProps = Pick & { handleSelection?: SelectableCollectionReturnType['handleSelection']; }; -const HorizontalGrid = ({ - isLoading, - mangas, - inLibraryIndicator, - GridItemContainer, - gridLayout, - isSelectModeActive, - selectedMangaIds, - handleSelection, - mode, -}: DefaultGridProps) => ( - - {isLoading ? ( - - ) : ( - mangas.map((manga) => ( - - {createMangaCard( - manga, - gridLayout, - inLibraryIndicator, - isSelectModeActive, - selectedMangaIds, - handleSelection, - mode, - )} - - )) - )} - +const HorizontalGrid = forwardRef( + ( + { + isLoading, + mangas, + inLibraryIndicator, + GridItemContainer, + gridLayout, + isSelectModeActive, + selectedMangaIds, + handleSelection, + mode, + }: DefaultGridProps, + ref: ForwardedRef, + ) => ( + + {isLoading ? ( + + ) : ( + mangas.map((manga) => ( + + {createMangaCard( + manga, + gridLayout, + inLibraryIndicator, + isSelectModeActive, + selectedMangaIds, + handleSelection, + mode, + )} + + )) + )} + + ), ); -const VerticalGrid = ({ - isLoading, - mangas, - inLibraryIndicator, - GridItemContainer, - gridLayout, - hasNextPage, - loadMore, - isSelectModeActive, - selectedMangaIds, - handleSelection, - mode, -}: DefaultGridProps & { - hasNextPage: boolean; - loadMore: () => void; -}) => { - const location = useLocation<{ snapshot?: GridStateSnapshot }>(); - const navigate = useNavigate(); - const { snapshot } = location.state ?? {}; +const VerticalGrid = forwardRef( + ( + { + isLoading, + mangas, + inLibraryIndicator, + GridItemContainer, + gridLayout, + hasNextPage, + loadMore, + isSelectModeActive, + selectedMangaIds, + handleSelection, + mode, + }: DefaultGridProps & { + hasNextPage: boolean; + loadMore: () => void; + }, + ref: ForwardedRef, + ) => { + const location = useLocation<{ snapshot?: GridStateSnapshot }>(); + const navigate = useNavigate(); + const { snapshot } = location.state ?? {}; - const persistGridStateTimeout = useRef(); - const persistGridState = (gridState: GridStateSnapshot) => { - const currentUrl = window.location.href; + const persistGridStateTimeout = useRef(); + const persistGridState = (gridState: GridStateSnapshot) => { + const currentUrl = window.location.href; - clearTimeout(persistGridStateTimeout.current); - persistGridStateTimeout.current = setTimeout(() => { - const didLocationChange = currentUrl !== window.location.href; - if (didLocationChange) { - return; - } - - navigate( - { pathname: '', search: location.search }, - { replace: true, state: { ...location.state, snapshot: gridState } }, - ); - }, 250); - }; - useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]); - - return ( - <> - loadMore()} - itemContent={(index) => - createMangaCard( - mangas[index], - gridLayout, - inLibraryIndicator, - isSelectModeActive, - selectedMangaIds, - handleSelection, - mode, - ) + clearTimeout(persistGridStateTimeout.current); + persistGridStateTimeout.current = setTimeout(() => { + const didLocationChange = currentUrl !== window.location.href; + if (didLocationChange) { + return; } - /> - {/* render div to prevent UI jumping around when showing/hiding loading placeholder */ - /* eslint-disable-next-line no-nested-ternary */} - {isSelectModeActive && gridLayout === GridLayout.List ? ( - - ) : // eslint-disable-next-line no-nested-ternary - isLoading ? ( - - ) : hasNextPage ? ( -
- ) : null} - - ); -}; + + navigate( + { pathname: '', search: location.search }, + { replace: true, state: { ...location.state, snapshot: gridState } }, + ); + }, 250); + }; + useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]); + + return ( + <> + + loadMore()} + itemContent={(index) => + createMangaCard( + mangas[index], + gridLayout, + inLibraryIndicator, + isSelectModeActive, + selectedMangaIds, + handleSelection, + mode, + ) + } + /> + + {/* render div to prevent UI jumping around when showing/hiding loading placeholder */ + /* eslint-disable-next-line no-nested-ternary */} + {isSelectModeActive && gridLayout === GridLayout.List ? ( + + ) : // eslint-disable-next-line no-nested-ternary + isLoading ? ( + + ) : hasNextPage ? ( +
+ ) : null} + + ); + }, +); export interface IMangaGridProps extends Omit { message?: string; @@ -221,9 +234,11 @@ export const MangaGrid: React.FC = (props) => { mode, } = props; + const gridRef = useRef(null); + const [dimensions, setDimensions] = useState(document.documentElement.offsetWidth); const [gridItemWidth] = useLocalStorage('ItemWidth', 300); - const gridRef = useRef(null); + const gridWrapperRef = useRef(null); const GridItemContainer = useMemo( () => GridItemContainerWithDimension(dimensions, gridItemWidth, gridLayout), [dimensions, gridItemWidth, gridLayout], @@ -231,7 +246,7 @@ export const MangaGrid: React.FC = (props) => { const updateGridWidth = () => { const getDimensions = () => { - const gridWidth = gridRef.current?.offsetWidth; + const gridWidth = gridWrapperRef.current?.offsetWidth; if (!gridWidth) { return document.documentElement.offsetWidth; @@ -258,6 +273,36 @@ export const MangaGrid: React.FC = (props) => { return () => window.removeEventListener('resize', onResize); }, []); + useEffect(() => { + if (!gridRef.current) { + return () => {}; + } + + if (gridRef.current.offsetHeight > document.documentElement.clientHeight) { + return () => {}; + } + + const resizeObserver = new ResizeObserver(() => { + const gridHeight = gridRef.current!.offsetHeight; + const isScrollbarVisible = gridHeight > document.documentElement.clientHeight; + + if (!gridHeight) { + return; + } + + if (isScrollbarVisible) { + resizeObserver.disconnect(); + return; + } + + loadMore(); + resizeObserver.disconnect(); + }); + resizeObserver.observe(gridRef.current); + + return () => resizeObserver.disconnect(); + }, [loadMore]); + const hasNoItems = !isLoading && mangas.length === 0; if (hasNoItems) { if (noFaces) { @@ -278,7 +323,7 @@ export const MangaGrid: React.FC = (props) => { return (
= (props) => { > {horizontal ? ( = (props) => { /> ) : ( (); const navigate = useNavigate(); @@ -232,7 +229,7 @@ export function SourceMangas() { contentType, searchTerm, filtersToApply, - isLargeScreen ? 2 : 1, + 1, ); const mangas = data?.fetchSourceManga.mangas ?? []; const hasNextPage = data?.fetchSourceManga.hasNextPage ?? false;