From cc1628c13a925a8518eebcd976ac341d3fdc8ae0 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 9 May 2026 11:28:33 -0500 Subject: [PATCH] Fix mobile sidebar section controls Co-Authored-By: Paperclip --- ui/src/components/SidebarSection.test.tsx | 41 +++++++++++++++++++++++ ui/src/components/SidebarSection.tsx | 10 ++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/ui/src/components/SidebarSection.test.tsx b/ui/src/components/SidebarSection.test.tsx index 9cce745b9f3..35c89922b12 100644 --- a/ui/src/components/SidebarSection.test.tsx +++ b/ui/src/components/SidebarSection.test.tsx @@ -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 }) => ( {children} ), })); +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 | 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( + + Projects + , + ); + }); + 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"); + }); }); diff --git a/ui/src/components/SidebarSection.tsx b/ui/src/components/SidebarSection.tsx index 0eb45d5f8cc..e81a59bc808 100644 --- a/ui/src/components/SidebarSection.tsx +++ b/ui/src/components/SidebarSection.tsx @@ -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) { + 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`} >