diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 7d1c1be9a..ce3abcfd2 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -33,7 +33,7 @@ "@react-pdf-viewer/core": "^3.12.0", "@react-pdf-viewer/toolbar": "^3.12.0", "@tanstack/react-query": "^4.29.19", - "@tanstack/react-virtual": "^3.0.0-beta.68", + "@tanstack/react-virtual": "^3.0.1", "@theme-ui/color": "^0.16.1", "@theme-ui/components": "^0.16.1", "@theme-ui/core": "^0.16.1", @@ -72,6 +72,7 @@ "react-modal": "3.16.1", "react-qrcode-logo": "^2.9.0", "react-scroll-sync": "^0.11.2", + "react-virtuoso": "^4.6.2", "timeago.js": "4.0.2", "tinycolor2": "^1.6.0", "w3c-keyname": "^2.2.6", @@ -37715,6 +37716,8 @@ }, "node_modules/@tanstack/react-virtual": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.1.tgz", + "integrity": "sha512-IFOFuRUTaiM/yibty9qQ9BfycQnYXIDHGP2+cU+0LrFFGNhVxCXSQnaY6wkX8uJVteFEBjUondX0Hmpp7TNcag==", "license": "MIT", "dependencies": { "@tanstack/virtual-core": "3.1.3" @@ -37730,6 +37733,8 @@ }, "node_modules/@tanstack/virtual-core": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz", + "integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==", "license": "MIT", "funding": { "type": "github", @@ -43449,6 +43454,18 @@ "react-dom": "16.x || 17.x || 18.x" } }, + "node_modules/react-virtuoso": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.6.2.tgz", + "integrity": "sha512-vvlqvzPif+MvBrJ09+hJJrVY0xJK9yran+A+/1iwY78k0YCVKsyoNPqoLxOxzYPggspNBNXqUXEcvckN29OxyQ==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16 || >=17 || >= 18", + "react-dom": ">=16 || >=17 || >= 18" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "license": "MIT", diff --git a/apps/web/package.json b/apps/web/package.json index ade6feae8..9d3be65e2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,7 +32,7 @@ "@react-pdf-viewer/core": "^3.12.0", "@react-pdf-viewer/toolbar": "^3.12.0", "@tanstack/react-query": "^4.29.19", - "@tanstack/react-virtual": "^3.0.0-beta.68", + "@tanstack/react-virtual": "^3.0.1", "@theme-ui/color": "^0.16.1", "@theme-ui/components": "^0.16.1", "@theme-ui/core": "^0.16.1", @@ -71,6 +71,7 @@ "react-modal": "3.16.1", "react-qrcode-logo": "^2.9.0", "react-scroll-sync": "^0.11.2", + "react-virtuoso": "^4.6.2", "timeago.js": "4.0.2", "tinycolor2": "^1.6.0", "w3c-keyname": "^2.2.6", diff --git a/apps/web/src/components/list-container/index.tsx b/apps/web/src/components/list-container/index.tsx index 1f9e7b06f..2d949bb88 100644 --- a/apps/web/src/components/list-container/index.tsx +++ b/apps/web/src/components/list-container/index.tsx @@ -17,18 +17,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { useEffect, useRef, useState } from "react"; -import { Flex, Button } from "@theme-ui/components"; +import { forwardRef, useEffect, useRef, useState } from "react"; +import { Flex, Button, Box } from "@theme-ui/components"; import { Plus } from "../icons"; import { useStore as useSelectionStore, store as selectionStore } from "../../stores/selection-store"; import GroupHeader from "../group-header"; -import { ListItemWrapper } from "./list-profiles"; +import { DEFAULT_ITEM_HEIGHT, ListItemWrapper } from "./list-profiles"; import Announcements from "../announcements"; import { ListLoader } from "../loaders/list-loader"; -import { FlexScrollContainer } from "../scroll-container"; +import ScrollContainer from "../scroll-container"; import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation"; import { Context } from "./types"; import { @@ -37,9 +37,29 @@ import { Item, isGroupHeader } from "@notesnook/core"; -import { VirtualizedList } from "../virtualized-list"; -import { ScrollToOptions, Virtualizer } from "@tanstack/react-virtual"; import { useResolvedItem } from "./resolved-item"; +import { + ItemProps, + ScrollerProps, + Virtuoso, + VirtuosoHandle +} from "react-virtuoso"; +import Skeleton from "react-loading-skeleton"; + +export const CustomScrollbarsVirtualList = forwardRef< + HTMLDivElement, + ScrollerProps +>(function CustomScrollbarsVirtualList(props, ref) { + return ( + { + if (typeof ref === "function") ref(sRef); + else if (ref) ref.current = sRef; + }} + /> + ); +}); type ListContainerProps = { group?: GroupingKey; @@ -68,7 +88,8 @@ function ListContainer(props: ListContainerProps) { (store) => store.toggleSelectionMode ); - const listRef = useRef>(); + const listRef = useRef(null); + const listContainerRef = useRef(null); useEffect(() => { return () => { @@ -119,25 +140,24 @@ function ListContainer(props: ListContainerProps) { ) : ( <> - - {header ? header : } - items.getKey(index)} - items={items.ids} - mode="dynamic" - tabIndex={-1} - overscan={10} + items.getKey(index)} + defaultItemHeight={DEFAULT_ITEM_HEIGHT} + totalCount={items.ids.length} onBlur={() => setFocusedGroupIndex(-1)} onKeyDown={(e) => onKeyDown(e.nativeEvent)} - itemWrapperProps={(_, index) => ({ - onFocus: () => onFocus(index), - onMouseDown: (e) => onMouseDown(e.nativeEvent, index) - })} + components={{ + Scroller: CustomScrollbarsVirtualList, + Item: VirtuosoItem, + Header: () => (header ? header : ) + }} + increaseViewportBy={{ top: 10, bottom: 10 }} context={{ items, group, @@ -147,11 +167,15 @@ function ListContainer(props: ListContainerProps) { scrollToIndex: listRef.current?.scrollToIndex, focusGroup: setFocusedGroupIndex, context, - compact + compact, + onMouseDown, + onFocus }} - renderItem={ItemRenderer} + itemContent={(index, _data, context) => ( + + )} /> - + )} {button && ( @@ -194,6 +218,9 @@ type ListContext = { focusGroup: (index: number) => void; context?: Context; compact?: boolean; + + onMouseDown: (e: MouseEvent, itemIndex: number) => void; + onFocus: (itemIndex: number) => void; }; function ItemRenderer({ index, @@ -214,7 +241,37 @@ function ItemRenderer({ compact } = context; const resolvedItem = useResolvedItem({ index, items }); - if (!resolvedItem) return
; + if (!resolvedItem || !resolvedItem.item) + return ( + + + + + + + + + + ); return ( <> @@ -241,7 +298,7 @@ function ItemRenderer({ groups={async () => (items.groups ? items.groups() : [])} onJump={(index) => { scrollToIndex?.(index, { - align: "center", + // align: "center", behavior: "auto" }); focusGroup(index); @@ -260,6 +317,26 @@ function ItemRenderer({ ); } +function VirtuosoItem({ + item: _item, + context, + ...props +}: ItemProps & { + context?: ListContext; +}) { + return ( +
context?.onFocus(props["data-item-index"])} + onMouseDown={(e) => + context?.onMouseDown(e.nativeEvent, props["data-item-index"]) + } + > + {props.children} +
+ ); +} + /** * Scroll the element at the specified index into view and * wait until it renders into the DOM. This function keeps @@ -268,28 +345,29 @@ function ItemRenderer({ * 50ms interval. */ function waitForElement( - list: Virtualizer, + list: VirtuosoHandle, index: number, elementId: string, callback: (element: HTMLElement) => void ) { let waitInterval = 0; let maxAttempts = 3; - list.scrollToIndex(index); - function scrollDone() { - if (!maxAttempts) return; - clearTimeout(waitInterval); + list.scrollIntoView({ + index, + done: function scrollDone() { + if (!maxAttempts) return; + clearTimeout(waitInterval); - const element = document.getElementById(elementId); - if (!element) { - --maxAttempts; - waitInterval = setTimeout(() => { - scrollDone(); - }, 50) as unknown as number; - return; + const element = document.getElementById(elementId); + if (!element) { + --maxAttempts; + waitInterval = setTimeout(() => { + scrollDone(); + }, 50) as unknown as number; + return; + } + + callback(element); } - - callback(element); - } - scrollDone(); + }); }