From 737401ce600bea4f70f19b2ecaf9d433e8395f9c Mon Sep 17 00:00:00 2001 From: Sawyer Hood Date: Tue, 8 Sep 2026 08:24:39 -0700 Subject: [PATCH] Persist sidebar footer disclosure across route remounts --- ...ginSidebarFooterItems.persistence.test.tsx | 103 ++++++++++++++++++ .../plugin/PluginSidebarFooterItems.tsx | 22 +++- 2 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 apps/app/src/components/plugin/PluginSidebarFooterItems.persistence.test.tsx diff --git a/apps/app/src/components/plugin/PluginSidebarFooterItems.persistence.test.tsx b/apps/app/src/components/plugin/PluginSidebarFooterItems.persistence.test.tsx new file mode 100644 index 0000000000..52b3d69ad5 --- /dev/null +++ b/apps/app/src/components/plugin/PluginSidebarFooterItems.persistence.test.tsx @@ -0,0 +1,103 @@ +// @vitest-environment jsdom +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, expect, it } from "vitest"; +import { MemoryRouter } from "react-router-dom"; +import { SidebarMenu, SidebarProvider } from "@/components/ui/sidebar.js"; +import { + resetPluginSlotStoreForTest, + setPluginSlotRegistrations, +} from "@/lib/plugin-slots"; +import { + collectPluginAppRegistrations, + definePluginApp, +} from "@/lib/plugin-app-definition"; +import { + PluginSidebarFooterDisclosure, + PluginSidebarFooterItems, + usePluginSidebarFooterDisclosure, +} from "./PluginSidebarFooterItems"; + +function DisclosureHarness() { + const disclosure = usePluginSidebarFooterDisclosure(); + return ( + <> + + + + + + ); +} + +function renderDisclosure() { + return render( + + + + + , + ); +} + +afterEach(() => { + cleanup(); + resetPluginSlotStoreForTest(); +}); + +it("keeps the active footer disclosure across a host remount", () => { + const definition = definePluginApp((app) => { + app.experimental_sidebarFooter.register({ + kind: "disclosure", + id: "status", + label: "Provider status", + icon: "ChartColumn", + component: ({ dismiss }) => ( +
+

Provider status content

+ +
+ ), + }); + }); + setPluginSlotRegistrations( + "status-plugin", + collectPluginAppRegistrations(definition), + ); + + const firstHost = renderDisclosure(); + fireEvent.click(screen.getByRole("button", { name: "Provider status" })); + expect(screen.getByText("Provider status content")).toBeTruthy(); + firstHost.unmount(); + + const secondHost = renderDisclosure(); + expect(screen.getByText("Provider status content")).toBeTruthy(); + expect( + screen + .getByRole("button", { name: "Provider status" }) + .getAttribute("aria-expanded"), + ).toBe("true"); + + fireEvent.click( + screen.getByRole("button", { name: "Close provider status" }), + ); + expect(screen.queryByText("Provider status content")).toBeNull(); + secondHost.unmount(); + + renderDisclosure(); + expect( + screen + .getByRole("button", { name: "Provider status" }) + .getAttribute("aria-expanded"), + ).toBe("false"); + expect(screen.queryByText("Provider status content")).toBeNull(); +}); diff --git a/apps/app/src/components/plugin/PluginSidebarFooterItems.tsx b/apps/app/src/components/plugin/PluginSidebarFooterItems.tsx index be817c8950..16ff8e3302 100644 --- a/apps/app/src/components/plugin/PluginSidebarFooterItems.tsx +++ b/apps/app/src/components/plugin/PluginSidebarFooterItems.tsx @@ -25,11 +25,22 @@ const SIDEBAR_FOOTER_ACTION_CLASS = cn( COARSE_POINTER_CHILD_ICON_BUTTON_CLASS, "text-muted-foreground hover:text-sidebar-foreground [&>svg]:opacity-80", ); +let rememberedActiveDisclosureKey: string | null = null; function footerItemKey(item: PluginSidebarFooterItemSlot): string { return `${item.pluginId}/${item.id}/${item.generation}`; } +function nextActiveDisclosureKey( + current: string | null, + itemKey: string, + command: ExperimentalSidebarFooterCommandKind, +): string | null { + if (command === "open") return itemKey; + if (command === "close") return current === itemKey ? null : current; + return current === itemKey ? null : itemKey; +} + function footerDisclosureId(item: PluginSidebarFooterItemSlot): string { return `plugin-sidebar-footer-disclosure-${item.pluginId}-${item.id}-${item.generation}`; } @@ -44,7 +55,9 @@ export function usePluginSidebarFooterDisclosure() { () => sidebarFooterItems.filter((item) => item.kind === "disclosure"), [sidebarFooterItems], ); - const [activeKey, setActiveKey] = useState(null); + const [activeKey, setActiveKey] = useState( + rememberedActiveDisclosureKey, + ); const [suppressedTooltipKey, setSuppressedTooltipKey] = useState< string | null >(null); @@ -76,9 +89,9 @@ export function usePluginSidebarFooterDisclosure() { (command === "toggle" && activeKey === itemKey); setSuppressedTooltipKey(isClosing ? itemKey : null); setActiveKey((current) => { - if (command === "open") return itemKey; - if (command === "close") return current === itemKey ? null : current; - return current === itemKey ? null : itemKey; + const next = nextActiveDisclosureKey(current, itemKey, command); + rememberedActiveDisclosureKey = next; + return next; }); }, [activeKey], @@ -88,6 +101,7 @@ export function usePluginSidebarFooterDisclosure() { if (activeItem !== null) { setSuppressedTooltipKey(footerItemKey(activeItem)); } + rememberedActiveDisclosureKey = null; setActiveKey(null); }, [activeItem]);