Selaa lähdekoodia

feat: recover scrolled position after Library re-opening (#6624)

Co-authored-by: dwelle <[email protected]>
Arnost Pleskot 2 vuotta sitten
vanhempi
commit
1e3c94a37a

+ 12 - 1
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<LibraryItem["id"][]>([]);
+  const libraryContainerRef = useRef<HTMLDivElement>(null);
+  const scrollPosition = useScrollPosition<HTMLDivElement>(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 && (

+ 5 - 3
src/components/LibraryMenuSection.tsx

@@ -58,9 +58,11 @@ function LibraryRow({
 
 const EmptyLibraryRow = () => (
   <Stack.Row className="library-menu-items-container__row" gap={1}>
-    <Stack.Col>
-      <div className={clsx("library-unit")} />
-    </Stack.Col>
+    {Array.from({ length: ITEMS_PER_ROW }).map((_, index) => (
+      <Stack.Col key={index}>
+        <div className={clsx("library-unit", "library-unit--skeleton")} />
+      </Stack.Col>
+    ))}
   </Stack.Row>
 );
 

+ 35 - 0
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;
+    }
+  }
 }

+ 1 - 0
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)}

+ 44 - 45
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<HTMLDivElement>;
 };
 
-const RowStack = ({
-  children,
-  gap,
-  align,
-  justifyContent,
-  className,
-  style,
-}: StackProps) => {
-  return (
-    <div
-      className={clsx("Stack Stack_horizontal", className)}
-      style={{
-        "--gap": gap,
-        alignItems: align,
-        justifyContent,
-        ...style,
-      }}
-    >
-      {children}
-    </div>
-  );
-};
+const RowStack = forwardRef(
+  (
+    { children, gap, align, justifyContent, className, style }: StackProps,
+    ref: React.ForwardedRef<HTMLDivElement>,
+  ) => {
+    return (
+      <div
+        className={clsx("Stack Stack_horizontal", className)}
+        style={{
+          "--gap": gap,
+          alignItems: align,
+          justifyContent,
+          ...style,
+        }}
+        ref={ref}
+      >
+        {children}
+      </div>
+    );
+  },
+);
 
-const ColStack = ({
-  children,
-  gap,
-  align,
-  justifyContent,
-  className,
-  style,
-}: StackProps) => {
-  return (
-    <div
-      className={clsx("Stack Stack_vertical", className)}
-      style={{
-        "--gap": gap,
-        justifyItems: align,
-        justifyContent,
-        ...style,
-      }}
-    >
-      {children}
-    </div>
-  );
-};
+const ColStack = forwardRef(
+  (
+    { children, gap, align, justifyContent, className, style }: StackProps,
+    ref: React.ForwardedRef<HTMLDivElement>,
+  ) => {
+    return (
+      <div
+        className={clsx("Stack Stack_vertical", className)}
+        style={{
+          "--gap": gap,
+          justifyItems: align,
+          justifyContent,
+          ...style,
+        }}
+        ref={ref}
+      >
+        {children}
+      </div>
+    );
+  },
+);
 
 export default {
   Row: RowStack,

+ 32 - 0
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<number>(0);
+
+export const useScrollPosition = <T extends HTMLElement>(
+  elementRef: React.RefObject<T>,
+) => {
+  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;
+};