فهرست منبع

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
3فایلهای تغییر یافته به همراه32 افزوده شده و 12 حذف شده
  1. 1 1
      frontend/src/layouts/AppSidebar.css
  2. 12 10
      frontend/src/layouts/AppSidebar.tsx
  3. 19 1
      frontend/src/test/app-sidebar.test.tsx

+ 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');
 });