From 41fb0eb7c6c29180664925b30a9f231aa304558d Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 6 May 2023 00:36:38 -0500 Subject: [PATCH] Virtualize movie select for manual import with react-window --- .../Movie/SelectMovieModalContent.tsx | 125 ++++++++++++++++-- 1 file changed, 112 insertions(+), 13 deletions(-) diff --git a/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.tsx b/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.tsx index 05506fdcb..3208050cf 100644 --- a/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.tsx +++ b/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.tsx @@ -1,5 +1,13 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import { throttle } from 'lodash'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { useSelector } from 'react-redux'; +import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import TextInput from 'Components/Form/TextInput'; import Button from 'Components/Link/Button'; import ModalBody from 'Components/Modal/ModalBody'; @@ -10,20 +18,100 @@ import Scroller from 'Components/Scroller/Scroller'; import { scrollDirections } from 'Helpers/Props'; import Movie from 'Movie/Movie'; import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; +import dimensions from 'Styles/Variables/dimensions'; import SelectMovieRow from './SelectMovieRow'; import styles from './SelectMovieModalContent.css'; +const bodyPadding = parseInt(dimensions.pageContentBodyPadding); + interface SelectMovieModalContentProps { modalTitle: string; onMovieSelect(movie: Movie): void; onModalClose(): void; } +interface RowItemData { + items: Movie[]; + onMovieSelect(movieId: number): void; +} + +const Row: React.FC> = ({ + index, + style, + data, +}) => { + const { items, onMovieSelect } = data; + + if (index >= items.length) { + return null; + } + + const movie = items[index]; + + return ( +
+ +
+ ); +}; + function SelectMovieModalContent(props: SelectMovieModalContentProps) { const { modalTitle, onMovieSelect, onModalClose } = props; + const listRef = useRef>(null); + const scrollerRef = useRef(null); const allMovies: Movie[] = useSelector(createAllMoviesSelector()); const [filter, setFilter] = useState(''); + const [size, setSize] = useState({ width: 0, height: 0 }); + const windowHeight = window.innerHeight; + + useEffect(() => { + const current = scrollerRef?.current as HTMLElement; + + if (current) { + const width = current.clientWidth; + const height = current.clientHeight; + const padding = bodyPadding - 5; + + setSize({ + width: width - padding * 2, + height: height + padding, + }); + } + }, [windowHeight, scrollerRef]); + + useEffect(() => { + const currentScrollerRef = scrollerRef.current as HTMLElement; + const currentScrollListener = currentScrollerRef; + + const handleScroll = throttle(() => { + const { offsetTop = 0 } = currentScrollerRef; + const scrollTop = currentScrollerRef.scrollTop - offsetTop; + + listRef.current?.scrollTo(scrollTop); + }, 10); + + currentScrollListener.addEventListener('scroll', handleScroll); + + return () => { + handleScroll.cancel(); + + if (currentScrollListener) { + currentScrollListener.removeEventListener('scroll', handleScroll); + } + }; + }, [listRef, scrollerRef]); const onFilterChange = useCallback( ({ value }: { value: string }) => { @@ -68,18 +156,29 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) { onChange={onFilterChange} /> - - {items.map((item) => { - return ( - - ); - })} + + + ref={listRef} + style={{ + width: '100%', + height: '100%', + overflow: 'none', + }} + width={size.width} + height={size.height} + itemCount={items.length} + itemSize={38} + itemData={{ + items, + onMovieSelect: onMovieSelectWrapper, + }} + > + {Row} +