Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 (
<>
<PluginSidebarFooterDisclosure
item={disclosure.activeItem}
onDismiss={disclosure.dismiss}
/>
<SidebarMenu>
<PluginSidebarFooterItems
activeDisclosureKey={disclosure.activeKey}
suppressedTooltipKey={disclosure.suppressedTooltipKey}
onTooltipSuppressionEnd={disclosure.clearTooltipSuppression}
onDisclosureCommand={disclosure.handleCommand}
/>
</SidebarMenu>
</>
);
}

function renderDisclosure() {
return render(
<MemoryRouter>
<SidebarProvider>
<DisclosureHarness />
</SidebarProvider>
</MemoryRouter>,
);
}

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 }) => (
<div>
<p>Provider status content</p>
<button type="button" onClick={dismiss}>
Close provider status
</button>
</div>
),
});
});
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();
});
22 changes: 18 additions & 4 deletions apps/app/src/components/plugin/PluginSidebarFooterItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
}
Expand All @@ -44,7 +55,9 @@ export function usePluginSidebarFooterDisclosure() {
() => sidebarFooterItems.filter((item) => item.kind === "disclosure"),
[sidebarFooterItems],
);
const [activeKey, setActiveKey] = useState<string | null>(null);
const [activeKey, setActiveKey] = useState<string | null>(
rememberedActiveDisclosureKey,
);
const [suppressedTooltipKey, setSuppressedTooltipKey] = useState<
string | null
>(null);
Expand Down Expand Up @@ -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],
Expand All @@ -88,6 +101,7 @@ export function usePluginSidebarFooterDisclosure() {
if (activeItem !== null) {
setSuppressedTooltipKey(footerItemKey(activeItem));
}
rememberedActiveDisclosureKey = null;
setActiveKey(null);
}, [activeItem]);

Expand Down
Loading