ソースを参照

fix(ui): reserve space for pinned sidebar

Keep page content accessible when the desktop sidebar remains expanded and cover the complete pin lifecycle.
PathGao 22 時間 前
コミット
ac584cfc90

+ 1 - 1
frontend/src/layouts/AppSidebar.css

@@ -12,7 +12,7 @@
   align-self: flex-start;
 }
 
-.ant-sidebar > .ant-layout-sider:not(.ant-layout-sider-collapsed) {
+.ant-sidebar:not(.sidebar-pinned) > .ant-layout-sider:not(.ant-layout-sider-collapsed) {
   box-shadow: 0 0 32px rgba(0, 0, 0, 0.22);
 }
 

+ 12 - 10
frontend/src/layouts/AppSidebar.tsx

@@ -46,8 +46,8 @@ const DOCS_URL = 'https://docs.sanaei.dev/';
 const REPO_URL = 'https://github.com/MHSanaei/3x-ui';
 const LOGOUT_KEY = '__logout__';
 const RAIL_WIDTH = 72;
+const SIDER_WIDTH = 220;
 const SIDEBAR_PINNED_KEY = 'sidebar-pinned';
-const railStyle = { '--sider-rail': `${RAIL_WIDTH}px` } as CSSProperties;
 
 let hoveredAcrossRemounts = false;
 
@@ -164,6 +164,10 @@ export default function AppSidebar() {
   const [pinned, setPinned] = useState(readSidebarPinned);
   const [drawerOpen, setDrawerOpen] = useState(false);
   const railCollapsed = !hovered && !pinned;
+  const railStyle = useMemo(
+    () => ({ '--sider-rail': `${pinned ? SIDER_WIDTH : RAIL_WIDTH}px` }) as CSSProperties,
+    [pinned],
+  );
   const rootRef = useRef<HTMLDivElement>(null);
 
   const updateHovered = useCallback((value: boolean) => {
@@ -172,12 +176,10 @@ export default function AppSidebar() {
   }, []);
 
   const togglePinned = useCallback(() => {
-    setPinned((value) => {
-      const next = !value;
-      saveSidebarPinned(next);
-      return next;
-    });
-  }, []);
+    const next = !pinned;
+    saveSidebarPinned(next);
+    setPinned(next);
+  }, [pinned]);
 
   useEffect(() => {
     const timer = window.setTimeout(() => {
@@ -287,14 +289,14 @@ export default function AppSidebar() {
   return (
     <div
       ref={rootRef}
-      className="ant-sidebar"
+      className={`ant-sidebar${pinned ? ' sidebar-pinned' : ''}`}
       style={railStyle}
       onMouseEnter={() => updateHovered(true)}
       onMouseLeave={() => updateHovered(false)}
     >
       <Layout.Sider
         theme={currentTheme}
-        width={220}
+        width={SIDER_WIDTH}
         collapsedWidth={RAIL_WIDTH}
         collapsed={railCollapsed}
       >
@@ -307,7 +309,7 @@ export default function AppSidebar() {
               <button
                 type="button"
                 className="sidebar-pin"
-                aria-label={t(pinned ? 'menu.unpinSidebar' : 'menu.pinSidebar')}
+                aria-label={t('menu.pinSidebar')}
                 aria-pressed={pinned}
                 title={t(pinned ? 'menu.unpinSidebar' : 'menu.pinSidebar')}
                 onClick={togglePinned}

+ 19 - 1
frontend/src/test/app-sidebar.test.tsx

@@ -37,13 +37,31 @@ test('keeps the sidebar expanded after pinning it from the header and restores t
   fireEvent.mouseLeave(sidebarRoot!);
 
   expect(sidebar?.classList.contains('ant-layout-sider-collapsed')).toBe(false);
+  expect(sidebarRoot?.getAttribute('style')).toContain('--sider-rail: 220px');
   expect(localStorage.getItem('sidebar-pinned')).toBe('true');
 
   first.unmount();
 
   const second = renderSidebar();
   const restoredSidebar = second.container.querySelector('.ant-layout-sider');
+  const restoredSidebarRoot = second.container.querySelector('.ant-sidebar');
 
   expect(restoredSidebar?.classList.contains('ant-layout-sider-collapsed')).toBe(false);
-  expect(screen.getByRole('button', { name: 'Unpin sidebar' })).not.toBeNull();
+  expect(restoredSidebarRoot?.getAttribute('style')).toContain('--sider-rail: 220px');
+  expect(screen.getByRole('button', { name: 'Pin sidebar' })).not.toBeNull();
+});
+
+test('returns to the compact rail after unpinning', () => {
+  const view = renderSidebar();
+  const sidebar = view.container.querySelector('.ant-layout-sider');
+  const sidebarRoot = view.container.querySelector('.ant-sidebar');
+
+  fireEvent.mouseEnter(sidebarRoot!);
+  fireEvent.click(screen.getByRole('button', { name: 'Pin sidebar' }));
+  fireEvent.click(screen.getByRole('button', { name: 'Pin sidebar' }));
+  fireEvent.mouseLeave(sidebarRoot!);
+
+  expect(sidebar?.classList.contains('ant-layout-sider-collapsed')).toBe(true);
+  expect(sidebarRoot?.getAttribute('style')).toContain('--sider-rail: 72px');
+  expect(localStorage.getItem('sidebar-pinned')).toBe('false');
 });