fix: show chat sidebar trigger

This commit is contained in:
Jesse_Chen
2026-07-18 21:45:38 +08:00
parent ff040fa1b5
commit 9efe55ea49
3 changed files with 19 additions and 13 deletions
-2
View File
@@ -25,7 +25,6 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
import {
@@ -113,7 +112,6 @@ export function AppSidebar({
<div className="brand-row">
<span className="brand-mark" aria-hidden="true" />
{showExpandedContent ? <strong>Jyotisha</strong> : null}
<SidebarTrigger placement="sidebar" />
</div>
<SidebarMenu>
<SidebarMenuItem>
+13 -9
View File
@@ -1,6 +1,7 @@
"use client";
import { Tooltip } from "@base-ui/react/tooltip";
import { PanelLeft } from "lucide-react";
import {
createContext,
forwardRef,
@@ -37,7 +38,7 @@ export type SidebarContextValue = {
type SidebarProviderContextValue = SidebarContextValue & {
readonly setInsetTrigger: (element: HTMLButtonElement | null) => void;
readonly setSidebarTrigger: (element: HTMLButtonElement | null) => void;
readonly setSidebarSurface: (element: HTMLElement | null) => void;
};
export type SidebarProviderProps = ComponentProps<"div"> & {
@@ -84,7 +85,7 @@ export function SidebarProvider({
const openMobileRef = useRef(false);
const userChangedDesktopState = useRef(false);
const insetTriggerRef = useRef<HTMLButtonElement | null>(null);
const sidebarTriggerRef = useRef<HTMLButtonElement | null>(null);
const sidebarSurfaceRef = useRef<HTMLElement | null>(null);
const wasMobileOpen = useRef(false);
const isControlled = controlledOpen !== undefined;
const open = controlledOpen ?? uncontrolledOpen;
@@ -134,7 +135,7 @@ export function SidebarProvider({
useEffect(() => {
if (openMobile && !wasMobileOpen.current) {
const focusDrawer = window.requestAnimationFrame(() => sidebarTriggerRef.current?.focus());
const focusDrawer = window.requestAnimationFrame(() => sidebarSurfaceRef.current?.focus());
wasMobileOpen.current = true;
return () => window.cancelAnimationFrame(focusDrawer);
}
@@ -158,7 +159,7 @@ export function SidebarProvider({
isMobile,
toggleSidebar,
setInsetTrigger: (element) => { insetTriggerRef.current = element; },
setSidebarTrigger: (element) => { sidebarTriggerRef.current = element; },
setSidebarSurface: (element) => { sidebarSurfaceRef.current = element; },
};
return (
@@ -176,8 +177,8 @@ export function SidebarProvider({
}
export function Sidebar({ id, className, ...props }: ComponentProps<"aside">) {
const { isMobile, open, openMobile, setOpenMobile } = useSidebar();
const sidebar = <aside id={id ?? "chat-sidebar"} data-sidebar="sidebar" data-slot="sidebar" data-state={open ? "expanded" : "collapsed"} data-mobile-open={openMobile} className={cn("flex min-h-0 shrink-0 flex-col", className)} {...props} />;
const { isMobile, open, openMobile, setOpenMobile, setSidebarSurface } = useSidebarContext();
const sidebar = <aside id={id ?? "chat-sidebar"} data-sidebar="sidebar" data-slot="sidebar" data-state={open ? "expanded" : "collapsed"} data-mobile-open={openMobile} className={cn("flex min-h-0 shrink-0 flex-col", className)} tabIndex={-1} {...props} ref={setSidebarSurface} />;
return <>
{sidebar}
{isMobile && openMobile ? <button type="button" data-sidebar="scrim" data-slot="sidebar-scrim" aria-label="关闭聊天记录" className="sidebar-scrim" onClick={() => setOpenMobile(false)} /> : null}
@@ -239,7 +240,7 @@ export const SidebarTrigger = forwardRef<HTMLButtonElement, SidebarTriggerProps>
onClick,
...props
}, ref) {
const { isMobile, open, openMobile, setInsetTrigger, setSidebarTrigger, toggleSidebar } = useSidebarContext();
const { isMobile, open, openMobile, setInsetTrigger, toggleSidebar } = useSidebarContext();
const expanded = isMobile ? openMobile : open;
const label = isMobile
? openMobile ? "关闭聊天记录" : "打开聊天记录"
@@ -247,9 +248,12 @@ export const SidebarTrigger = forwardRef<HTMLButtonElement, SidebarTriggerProps>
const register = (element: HTMLButtonElement | null) => {
setRef(ref, element);
if (placement === "inset") setInsetTrigger(element);
else setSidebarTrigger(element);
};
return <button ref={register} type="button" data-sidebar="trigger" data-slot="sidebar-trigger" aria-label={label} aria-expanded={expanded} className={cn("min-h-11 min-w-11", className)} onClick={(event) => { onClick?.(event); if (!event.defaultPrevented) toggleSidebar(); }} {...props} />;
return (
<button ref={register} type="button" data-sidebar="trigger" data-slot="sidebar-trigger" aria-label={label} aria-expanded={expanded} className={cn("min-h-11 min-w-11", className)} onClick={(event) => { onClick?.(event); if (!event.defaultPrevented) toggleSidebar(); }} {...props}>
<PanelLeft aria-hidden="true" />
</button>
);
});
export function SidebarRail({ className, onClick, ...props }: ComponentProps<"button">) {
+6 -2
View File
@@ -65,6 +65,7 @@ test("keeps the sidebar subtree stable while toggling its mobile scrim", () => {
test("cancels a stale mobile drawer focus frame", () => {
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
assert.match(sidebar, /const focusDrawer = window\.requestAnimationFrame/);
assert.match(sidebar, /sidebarSurfaceRef\.current\?\.focus\(\)/);
assert.match(sidebar, /return \(\) => window\.cancelAnimationFrame\(focusDrawer\);/);
});
@@ -74,6 +75,9 @@ test("uses the approved localized trigger actions", () => {
assert.match(sidebar, /"展开侧边栏"/);
assert.match(sidebar, /"打开聊天记录"/);
assert.match(sidebar, /"关闭聊天记录"/);
assert.match(sidebar, /PanelLeft/);
assert.doesNotMatch(sidebar, /PanelLeftClose|PanelLeftOpen/);
assert.match(sidebar, /<PanelLeft aria-hidden="true" \/>/);
});
test("keeps provider primitive defaults and consumer handlers composable", () => {
@@ -104,10 +108,10 @@ test("composes the Jyotisha app sidebar from the generic shell", () => {
}
});
test("renders the mobile drawer close trigger in the app sidebar brand row", () => {
test("keeps the sidebar brand row free of a duplicate collapse trigger", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
assert.match(appSidebar, /<div className="brand-row">[\s\S]*<SidebarTrigger placement="sidebar" \/>[\s\S]*<\/div>/);
assert.doesNotMatch(appSidebar, /SidebarTrigger/);
});
test("uses one collapsed history action instead of icon-only session rows", () => {