web: replace react-virtual with react-virtuouso

This commit is contained in:
Abdullah Atta
2023-12-13 15:33:15 +05:00
parent baa2d1a389
commit cfd53a8b2c
3 changed files with 141 additions and 45 deletions

View File

@@ -33,7 +33,7 @@
"@react-pdf-viewer/core": "^3.12.0", "@react-pdf-viewer/core": "^3.12.0",
"@react-pdf-viewer/toolbar": "^3.12.0", "@react-pdf-viewer/toolbar": "^3.12.0",
"@tanstack/react-query": "^4.29.19", "@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/color": "^0.16.1",
"@theme-ui/components": "^0.16.1", "@theme-ui/components": "^0.16.1",
"@theme-ui/core": "^0.16.1", "@theme-ui/core": "^0.16.1",
@@ -72,6 +72,7 @@
"react-modal": "3.16.1", "react-modal": "3.16.1",
"react-qrcode-logo": "^2.9.0", "react-qrcode-logo": "^2.9.0",
"react-scroll-sync": "^0.11.2", "react-scroll-sync": "^0.11.2",
"react-virtuoso": "^4.6.2",
"timeago.js": "4.0.2", "timeago.js": "4.0.2",
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"w3c-keyname": "^2.2.6", "w3c-keyname": "^2.2.6",
@@ -37715,6 +37716,8 @@
}, },
"node_modules/@tanstack/react-virtual": { "node_modules/@tanstack/react-virtual": {
"version": "3.1.3", "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", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/virtual-core": "3.1.3" "@tanstack/virtual-core": "3.1.3"
@@ -37730,6 +37733,8 @@
}, },
"node_modules/@tanstack/virtual-core": { "node_modules/@tanstack/virtual-core": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz",
"integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@@ -43449,6 +43454,18 @@
"react-dom": "16.x || 17.x || 18.x" "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": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"license": "MIT", "license": "MIT",

View File

@@ -32,7 +32,7 @@
"@react-pdf-viewer/core": "^3.12.0", "@react-pdf-viewer/core": "^3.12.0",
"@react-pdf-viewer/toolbar": "^3.12.0", "@react-pdf-viewer/toolbar": "^3.12.0",
"@tanstack/react-query": "^4.29.19", "@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/color": "^0.16.1",
"@theme-ui/components": "^0.16.1", "@theme-ui/components": "^0.16.1",
"@theme-ui/core": "^0.16.1", "@theme-ui/core": "^0.16.1",
@@ -71,6 +71,7 @@
"react-modal": "3.16.1", "react-modal": "3.16.1",
"react-qrcode-logo": "^2.9.0", "react-qrcode-logo": "^2.9.0",
"react-scroll-sync": "^0.11.2", "react-scroll-sync": "^0.11.2",
"react-virtuoso": "^4.6.2",
"timeago.js": "4.0.2", "timeago.js": "4.0.2",
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"w3c-keyname": "^2.2.6", "w3c-keyname": "^2.2.6",

View File

@@ -17,18 +17,18 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { useEffect, useRef, useState } from "react"; import { forwardRef, useEffect, useRef, useState } from "react";
import { Flex, Button } from "@theme-ui/components"; import { Flex, Button, Box } from "@theme-ui/components";
import { Plus } from "../icons"; import { Plus } from "../icons";
import { import {
useStore as useSelectionStore, useStore as useSelectionStore,
store as selectionStore store as selectionStore
} from "../../stores/selection-store"; } from "../../stores/selection-store";
import GroupHeader from "../group-header"; import GroupHeader from "../group-header";
import { ListItemWrapper } from "./list-profiles"; import { DEFAULT_ITEM_HEIGHT, ListItemWrapper } from "./list-profiles";
import Announcements from "../announcements"; import Announcements from "../announcements";
import { ListLoader } from "../loaders/list-loader"; 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 { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
import { Context } from "./types"; import { Context } from "./types";
import { import {
@@ -37,9 +37,29 @@ import {
Item, Item,
isGroupHeader isGroupHeader
} from "@notesnook/core"; } from "@notesnook/core";
import { VirtualizedList } from "../virtualized-list";
import { ScrollToOptions, Virtualizer } from "@tanstack/react-virtual";
import { useResolvedItem } from "./resolved-item"; 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 (
<ScrollContainer
{...props}
forwardedRef={(sRef) => {
if (typeof ref === "function") ref(sRef);
else if (ref) ref.current = sRef;
}}
/>
);
});
type ListContainerProps = { type ListContainerProps = {
group?: GroupingKey; group?: GroupingKey;
@@ -68,7 +88,8 @@ function ListContainer(props: ListContainerProps) {
(store) => store.toggleSelectionMode (store) => store.toggleSelectionMode
); );
const listRef = useRef<Virtualizer<Element, Element>>(); const listRef = useRef<VirtuosoHandle>(null);
const listContainerRef = useRef(null);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -119,25 +140,24 @@ function ListContainer(props: ListContainerProps) {
</> </>
) : ( ) : (
<> <>
<FlexScrollContainer <Flex
style={{ display: "flex", flexDirection: "column", flex: 1 }} ref={listContainerRef}
variant="columnFill"
data-test-id={`${group}-list`} data-test-id={`${group}-list`}
> >
{header ? header : <Announcements />} <Virtuoso
<VirtualizedList ref={listRef}
virtualizerRef={listRef} computeItemKey={(index) => items.getKey(index)}
estimatedSize={50} defaultItemHeight={DEFAULT_ITEM_HEIGHT}
getItemKey={(index) => items.getKey(index)} totalCount={items.ids.length}
items={items.ids}
mode="dynamic"
tabIndex={-1}
overscan={10}
onBlur={() => setFocusedGroupIndex(-1)} onBlur={() => setFocusedGroupIndex(-1)}
onKeyDown={(e) => onKeyDown(e.nativeEvent)} onKeyDown={(e) => onKeyDown(e.nativeEvent)}
itemWrapperProps={(_, index) => ({ components={{
onFocus: () => onFocus(index), Scroller: CustomScrollbarsVirtualList,
onMouseDown: (e) => onMouseDown(e.nativeEvent, index) Item: VirtuosoItem,
})} Header: () => (header ? header : <Announcements />)
}}
increaseViewportBy={{ top: 10, bottom: 10 }}
context={{ context={{
items, items,
group, group,
@@ -147,11 +167,15 @@ function ListContainer(props: ListContainerProps) {
scrollToIndex: listRef.current?.scrollToIndex, scrollToIndex: listRef.current?.scrollToIndex,
focusGroup: setFocusedGroupIndex, focusGroup: setFocusedGroupIndex,
context, context,
compact compact,
onMouseDown,
onFocus
}} }}
renderItem={ItemRenderer} itemContent={(index, _data, context) => (
<ItemRenderer context={context} index={index} />
)}
/> />
</FlexScrollContainer> </Flex>
</> </>
)} )}
{button && ( {button && (
@@ -194,6 +218,9 @@ type ListContext = {
focusGroup: (index: number) => void; focusGroup: (index: number) => void;
context?: Context; context?: Context;
compact?: boolean; compact?: boolean;
onMouseDown: (e: MouseEvent, itemIndex: number) => void;
onFocus: (itemIndex: number) => void;
}; };
function ItemRenderer({ function ItemRenderer({
index, index,
@@ -214,7 +241,37 @@ function ItemRenderer({
compact compact
} = context; } = context;
const resolvedItem = useResolvedItem({ index, items }); const resolvedItem = useResolvedItem({ index, items });
if (!resolvedItem) return <div style={{ height: 50, width: "100%" }}></div>; if (!resolvedItem || !resolvedItem.item)
return (
<Box key="list-item-skeleton" sx={{ py: 2, px: 1 }}>
<Skeleton
enableAnimation={false}
height={16}
width={`50%`}
style={{ marginBottom: 5 }}
/>
<Skeleton height={12} count={2} />
<Flex>
<Skeleton enableAnimation={false} height={10} inline width={50} />
<Skeleton
enableAnimation={false}
height={10}
inline
width={10}
circle
style={{ marginLeft: 5 }}
/>
<Skeleton
enableAnimation={false}
height={10}
inline
width={10}
circle
style={{ marginLeft: 5 }}
/>
</Flex>
</Box>
);
return ( return (
<> <>
@@ -241,7 +298,7 @@ function ItemRenderer({
groups={async () => (items.groups ? items.groups() : [])} groups={async () => (items.groups ? items.groups() : [])}
onJump={(index) => { onJump={(index) => {
scrollToIndex?.(index, { scrollToIndex?.(index, {
align: "center", // align: "center",
behavior: "auto" behavior: "auto"
}); });
focusGroup(index); focusGroup(index);
@@ -260,6 +317,26 @@ function ItemRenderer({
); );
} }
function VirtuosoItem({
item: _item,
context,
...props
}: ItemProps<string> & {
context?: ListContext;
}) {
return (
<div
{...props}
onFocus={() => context?.onFocus(props["data-item-index"])}
onMouseDown={(e) =>
context?.onMouseDown(e.nativeEvent, props["data-item-index"])
}
>
{props.children}
</div>
);
}
/** /**
* Scroll the element at the specified index into view and * Scroll the element at the specified index into view and
* wait until it renders into the DOM. This function keeps * wait until it renders into the DOM. This function keeps
@@ -268,28 +345,29 @@ function ItemRenderer({
* 50ms interval. * 50ms interval.
*/ */
function waitForElement( function waitForElement(
list: Virtualizer<Element, Element>, list: VirtuosoHandle,
index: number, index: number,
elementId: string, elementId: string,
callback: (element: HTMLElement) => void callback: (element: HTMLElement) => void
) { ) {
let waitInterval = 0; let waitInterval = 0;
let maxAttempts = 3; let maxAttempts = 3;
list.scrollToIndex(index); list.scrollIntoView({
function scrollDone() { index,
if (!maxAttempts) return; done: function scrollDone() {
clearTimeout(waitInterval); if (!maxAttempts) return;
clearTimeout(waitInterval);
const element = document.getElementById(elementId); const element = document.getElementById(elementId);
if (!element) { if (!element) {
--maxAttempts; --maxAttempts;
waitInterval = setTimeout(() => { waitInterval = setTimeout(() => {
scrollDone(); scrollDone();
}, 50) as unknown as number; }, 50) as unknown as number;
return; return;
}
callback(element);
} }
});
callback(element);
}
scrollDone();
} }