Fix mobile sidebar section controls

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-09 18:27:03 -05:00
1 parent 0891c1c72f
commit cc1628c13a
2 files changed
+49 -2

No files matched your search

+41
View File
@@ -6,12 +6,20 @@ import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SidebarSection } from "./SidebarSection";
const sidebarState = vi.hoisted(() => ({
isMobile: false,
}));
vi.mock("@/lib/router", () => ({
Link: ({ children, to, ...props }: { children: ReactNode; to: string }) => (
<a href={to} {...props}>{children}</a>
),
}));
vi.mock("../context/SidebarContext", () => ({
useSidebar: () => sidebarState,
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
@@ -43,6 +51,7 @@ describe("SidebarSection", () => {
let root: ReturnType<typeof createRoot> | null;
beforeEach(() => {
sidebarState.isMobile = false;
container = document.createElement("div");
document.body.appendChild(container);
root = null;
@@ -146,4 +155,36 @@ describe("SidebarSection", () => {
});
expect(onAction).toHaveBeenCalledTimes(1);
});
it("keeps section header controls visible on mobile", async () => {
sidebarState.isMobile = true;
const currentRoot = createRoot(container);
root = currentRoot;
await act(async () => {
currentRoot.render(
<SidebarSection
label="Projects"
collapsible={{ open: false, onOpenChange: vi.fn() }}
menu={{
ariaLabel: "Projects section actions",
actions: [{ type: "item", label: "New project" }],
}}
>
<a href="/projects">Projects</a>
</SidebarSection>,
);
});
await flushReact();
const projectsLabel = Array.from(container.querySelectorAll("span"))
.find((element) => element.textContent === "Projects");
const caret = projectsLabel?.previousElementSibling;
const trigger = container.querySelector('button[aria-label="Projects section actions"]');
expect(caret?.getAttribute("class")).toContain("opacity-100");
expect(caret?.getAttribute("class")).not.toContain("opacity-0");
expect(trigger?.getAttribute("class")).toContain("opacity-100");
expect(trigger?.getAttribute("class")).not.toContain("opacity-0");
});
});
+8 -2
View File
@@ -2,6 +2,7 @@ import { useState, type ComponentType, type ReactNode } from "react";
import { Link } from "@/lib/router";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useSidebar } from "../context/SidebarContext";
import {
Collapsible,
CollapsibleContent,
@@ -59,11 +60,16 @@ function SidebarSectionHeader({
label,
menu,
}: Pick<SidebarSectionProps, "collapsible" | "label" | "menu">) {
const { isMobile } = useSidebar();
const [menuOpen, setMenuOpen] = useState(false);
const hasMenu = Boolean(menu && ((menu.actions?.length ?? 0) > 0 || (menu.radioChoices?.length ?? 0) > 0));
const labelClassName = "text-[10px] font-medium uppercase tracking-widest font-mono text-muted-foreground/60";
const headerControlVisibilityClassName = isMobile
? "opacity-100"
: "opacity-0 group-hover/sidebar-section:opacity-100 group-focus-within/sidebar-section:opacity-100";
const caretClassName = cn(
"h-3 w-3 shrink-0 text-muted-foreground/60 opacity-0 transition-all group-hover/sidebar-section:opacity-100 group-focus-within/sidebar-section:opacity-100",
"h-3 w-3 shrink-0 text-muted-foreground/60 transition-all",
headerControlVisibilityClassName,
collapsible?.open && "rotate-90",
menuOpen && "opacity-100",
);
@@ -97,7 +103,7 @@ function SidebarSectionHeader({
size="icon-xs"
className={cn(
"h-5 w-5 text-muted-foreground/60 transition-opacity hover:text-foreground data-[state=open]:opacity-100",
"opacity-0 group-hover/sidebar-section:opacity-100 group-focus-within/sidebar-section:opacity-100",
headerControlVisibilityClassName,
)}
aria-label={menu?.ariaLabel ?? `${label} actions`}
>