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`}
>