diff --git a/packages/trpc-panel/src/react-app/components/SideNav.tsx b/packages/trpc-panel/src/react-app/components/SideNav.tsx
index de847be..0edff8a 100644
--- a/packages/trpc-panel/src/react-app/components/SideNav.tsx
+++ b/packages/trpc-panel/src/react-app/components/SideNav.tsx
@@ -1,7 +1,11 @@
-import React from "react";
+import React, { useCallback } from "react";
import type { ParsedRouter } from "../../parse/parseRouter";
import type { ParsedProcedure } from "@src/parse/parseProcedure";
-import { useSiteNavigationContext } from "@src/react-app/components/contexts/SiteNavigationContext";
+import {
+ collapsables,
+ useCollapsableIsShowing,
+ useSiteNavigationContext,
+} from "@src/react-app/components/contexts/SiteNavigationContext";
import { Chevron } from "@src/react-app/components/Chevron";
import { colorSchemeForNode } from "@src/react-app/components/style-utils";
import { ItemTypeIcon } from "@src/react-app/components/ItemTypeIcon";
@@ -31,13 +35,13 @@ function SideNavItem({
node: ParsedRouter | ParsedProcedure;
path: string[];
}) {
- const { togglePath, has, markForScrollTo } = useSiteNavigationContext();
- const shown = has(path) || path.length == 0;
+ const { markForScrollTo } = useSiteNavigationContext();
+ const shown = useCollapsableIsShowing(path) || path.length === 0;
- function onClick() {
- togglePath(path);
+ const onClick = useCallback(function onClick() {
+ collapsables.toggle(path);
markForScrollTo(path);
- }
+ }, []);
return (
<>
@@ -66,9 +70,17 @@ function SideNavItem({
{shown && node.nodeType === "router" && (
{Object.entries(node.children).map(([key, node]) => {
- const newPath = path.concat([key]);
- const k = newPath.join(",");
- return ;
+ return (
+
+ );
})}
)}
diff --git a/packages/trpc-panel/src/react-app/components/contexts/SiteNavigationContext.tsx b/packages/trpc-panel/src/react-app/components/contexts/SiteNavigationContext.tsx
index 023f0e4..7a01cff 100644
--- a/packages/trpc-panel/src/react-app/components/contexts/SiteNavigationContext.tsx
+++ b/packages/trpc-panel/src/react-app/components/contexts/SiteNavigationContext.tsx
@@ -1,16 +1,14 @@
+import { useAllPaths } from "@src/react-app/components/contexts/AllPathsContext";
import React, {
createContext,
ReactNode,
useContext,
+ useMemo,
useRef,
- useState,
} from "react";
+import { create } from "zustand";
const Context = createContext<{
- has: (path: string[]) => boolean;
- hidePath: (path: string[]) => void;
- showPath: (path: string[]) => void;
- togglePath: (path: string[]) => void;
scrollToPathIfMatches: (path: string[], element: Element) => boolean;
markForScrollTo: (path: string[]) => void;
openAndNavigateTo: (path: string[], closeOthers?: boolean) => void;
@@ -25,40 +23,90 @@ function forAllPaths(path: string[], callback: (current: string) => void) {
}
}
-export function SiteNavigationContextProvider({
- children,
-}: {
- children: ReactNode;
-}) {
- const [shownPaths, setShownPaths] = useState>(new Set());
+const collapsablesStore = {
+ current: null as null | ReturnType,
+};
- const scrollToPathRef = useRef(null);
+function initialCollapsableStoreValues(allPaths: string[]) {
+ const vals: Record = {};
+
+ for (const path of allPaths) {
+ vals[path] = false;
+ }
+ return vals;
+}
+
+function initCollapsablesStore(allPaths: string[]) {
+ collapsablesStore.current = create(() => ({
+ ...initialCollapsableStoreValues(allPaths),
+ }));
+}
+
+function useInitCollapsablesStore(allPaths: string[]) {
+ const hasInitted = useRef(false);
+
+ if (!hasInitted.current) {
+ initCollapsablesStore(allPaths);
+ hasInitted.current = true;
+ }
+}
- function hidePath(path: string[]) {
- const newPaths = new Set(shownPaths);
+export const collapsables = (() => {
+ const hide = (path: string[]) => {
const pathJoined = path.join(".");
forAllPaths(path, (current) => {
- if (pathJoined.length <= current.length) newPaths.delete(current);
+ if (pathJoined.length <= current.length) {
+ collapsablesStore.current?.setState({
+ [current]: false,
+ });
+ }
});
- setShownPaths(newPaths);
- }
-
- function showPath(path: string[]) {
- const newPaths = new Set(shownPaths);
+ };
+ const show = (path: string[]) => {
forAllPaths(path, (current) => {
- newPaths.add(current);
+ collapsablesStore.current?.setState({
+ [current]: true,
+ });
});
- setShownPaths(newPaths);
- }
+ };
+ return {
+ hide,
+ show,
+ toggle(path: string[]) {
+ const state = collapsablesStore.current!.getState() as any;
+ if (state[path.join(".")]) {
+ hide(path);
+ } else {
+ show(path);
+ }
+ },
+ hideAll() {
+ const state = collapsablesStore.current! as any;
+ const newValue: Record = {};
+ for (const path in state) {
+ newValue[path] = false;
+ }
+ collapsablesStore.current!.setState(newValue);
+ },
+ };
+})();
- function has(path: string[]) {
- return shownPaths.has(path.join("."));
- }
+export function useCollapsableIsShowing(path: string[]) {
+ const p = useMemo(() => {
+ return path.join(".");
+ }, []);
+ return collapsablesStore.current!((s) => (s as any)[p]);
+}
- function toggle(path: string[]) {
- if (has(path)) hidePath(path);
- else showPath(path);
- }
+export function SiteNavigationContextProvider({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ const allPaths = useAllPaths();
+ useInitCollapsablesStore(allPaths.pathsArray);
+
+ const scrollToPathRef = useRef(null);
function scrollToPathIfMatches(path: string[], element: Element) {
if (
@@ -81,21 +129,16 @@ export function SiteNavigationContextProvider({
}
function openAndNavigateTo(path: string[], hideOthers?: boolean) {
- const newSet = hideOthers ? new Set() : new Set(shownPaths);
- forAllPaths(path, (p) => {
- newSet.add(p);
- });
+ if (hideOthers) {
+ collapsables.hideAll();
+ }
+ collapsables.show(path);
markForScrollTo(path);
- setShownPaths(newSet);
}
return (
- {Object.entries(extraData.parameterDescriptions).map(
- ([key, value]) => (
-
-
- {`${key}: `}
- |
-
- {`${value}`}
- |
-
- )
- )}
+
+ {Object.entries(extraData.parameterDescriptions).map(
+ ([key, value]) => (
+
+
+ {`${key}: `}
+ |
+
+ {`${value}`}
+ |
+
+ )
+ )}
+
)}