From 1e3c94a37ad80cb8d32dc339758eefc54ae5f108 Mon Sep 17 00:00:00 2001 From: Arnost Pleskot Date: Wed, 31 May 2023 10:22:02 +0200 Subject: [PATCH] feat: recover scrolled position after Library re-opening (#6624) Co-authored-by: dwelle --- src/components/LibraryMenuItems.tsx | 13 +++- src/components/LibraryMenuSection.tsx | 8 ++- src/components/LibraryUnit.scss | 35 +++++++++++ src/components/LibraryUnit.tsx | 1 + src/components/Stack.tsx | 89 +++++++++++++-------------- src/hooks/useScrollPosition.ts | 32 ++++++++++ 6 files changed, 129 insertions(+), 49 deletions(-) create mode 100644 src/hooks/useScrollPosition.ts diff --git a/src/components/LibraryMenuItems.tsx b/src/components/LibraryMenuItems.tsx index 55de2aaf..8dd6b30f 100644 --- a/src/components/LibraryMenuItems.tsx +++ b/src/components/LibraryMenuItems.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { serializeLibraryAsJSON } from "../data/json"; import { t } from "../i18n"; import { @@ -15,6 +15,7 @@ import { duplicateElements } from "../element/newElement"; import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons"; import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent"; import LibraryMenuSection from "./LibraryMenuSection"; +import { useScrollPosition } from "../hooks/useScrollPosition"; import { useLibraryCache } from "../hooks/useLibraryItemSvg"; import "./LibraryMenuItems.scss"; @@ -39,6 +40,15 @@ export default function LibraryMenuItems({ id: string; }) { const [selectedItems, setSelectedItems] = useState([]); + const libraryContainerRef = useRef(null); + const scrollPosition = useScrollPosition(libraryContainerRef); + + // This effect has to be called only on first render, therefore `scrollPosition` isn't in the dependency array + useEffect(() => { + if (scrollPosition > 0) { + libraryContainerRef.current?.scrollTo(0, scrollPosition); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps const { svgCache } = useLibraryCache(); const unpublishedItems = libraryItems.filter( @@ -183,6 +193,7 @@ export default function LibraryMenuItems({ flex: publishedItems.length > 0 ? 1 : "0 1 auto", marginBottom: 0, }} + ref={libraryContainerRef} > <> {!isLibraryEmpty && ( diff --git a/src/components/LibraryMenuSection.tsx b/src/components/LibraryMenuSection.tsx index d13c8959..496f045b 100644 --- a/src/components/LibraryMenuSection.tsx +++ b/src/components/LibraryMenuSection.tsx @@ -58,9 +58,11 @@ function LibraryRow({ const EmptyLibraryRow = () => ( - -
- + {Array.from({ length: ITEMS_PER_ROW }).map((_, index) => ( + +
+ + ))} ); diff --git a/src/components/LibraryUnit.scss b/src/components/LibraryUnit.scss index 85d7505c..1bfc1e6d 100644 --- a/src/components/LibraryUnit.scss +++ b/src/components/LibraryUnit.scss @@ -20,6 +20,27 @@ border-color: var(--color-primary); border-width: 1px; } + + &--skeleton { + opacity: 0.5; + background: linear-gradient( + -45deg, + var(--color-gray-10), + var(--color-gray-20), + var(--color-gray-10) + ); + background-size: 200% 200%; + animation: library-unit__skeleton-opacity-animation 0.3s linear; + } + } + + &.theme--dark .library-unit--skeleton { + background-image: linear-gradient( + -45deg, + var(--color-gray-100), + var(--color-gray-80), + var(--color-gray-100) + ); } .library-unit__dragger { @@ -142,4 +163,18 @@ transform: scale(0.85); } } + + @keyframes library-unit__skeleton-opacity-animation { + 0% { + opacity: 0; + } + + 75% { + opacity: 0; + } + + 100% { + opacity: 0.5; + } + } } diff --git a/src/components/LibraryUnit.tsx b/src/components/LibraryUnit.tsx index 97f73f80..f256b49f 100644 --- a/src/components/LibraryUnit.tsx +++ b/src/components/LibraryUnit.tsx @@ -58,6 +58,7 @@ export const LibraryUnit = ({ "library-unit__active": elements, "library-unit--hover": elements && isHovered, "library-unit--selected": selected, + "library-unit--skeleton": !svg, })} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} diff --git a/src/components/Stack.tsx b/src/components/Stack.tsx index aa18e899..c54a6aef 100644 --- a/src/components/Stack.tsx +++ b/src/components/Stack.tsx @@ -1,6 +1,6 @@ import "./Stack.scss"; -import React from "react"; +import React, { forwardRef } from "react"; import clsx from "clsx"; type StackProps = { @@ -10,53 +10,52 @@ type StackProps = { justifyContent?: "center" | "space-around" | "space-between"; className?: string | boolean; style?: React.CSSProperties; + ref: React.RefObject; }; -const RowStack = ({ - children, - gap, - align, - justifyContent, - className, - style, -}: StackProps) => { - return ( -
- {children} -
- ); -}; +const RowStack = forwardRef( + ( + { children, gap, align, justifyContent, className, style }: StackProps, + ref: React.ForwardedRef, + ) => { + return ( +
+ {children} +
+ ); + }, +); -const ColStack = ({ - children, - gap, - align, - justifyContent, - className, - style, -}: StackProps) => { - return ( -
- {children} -
- ); -}; +const ColStack = forwardRef( + ( + { children, gap, align, justifyContent, className, style }: StackProps, + ref: React.ForwardedRef, + ) => { + return ( +
+ {children} +
+ ); + }, +); export default { Row: RowStack, diff --git a/src/hooks/useScrollPosition.ts b/src/hooks/useScrollPosition.ts new file mode 100644 index 00000000..e4efb460 --- /dev/null +++ b/src/hooks/useScrollPosition.ts @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { atom, useAtom } from "jotai"; +import throttle from "lodash.throttle"; + +const scrollPositionAtom = atom(0); + +export const useScrollPosition = ( + elementRef: React.RefObject, +) => { + const [scrollPosition, setScrollPosition] = useAtom(scrollPositionAtom); + + useEffect(() => { + const { current: element } = elementRef; + if (!element) { + return; + } + + const handleScroll = throttle(() => { + const { scrollTop } = element; + setScrollPosition(scrollTop); + }, 200); + + element.addEventListener("scroll", handleScroll); + + return () => { + handleScroll.cancel(); + element.removeEventListener("scroll", handleScroll); + }; + }, [elementRef, setScrollPosition]); + + return scrollPosition; +};