diff --git a/CHANGELOG.md b/CHANGELOG.md index 88fc0ea0a..e3ba0fcab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Hide sidebar after opening, in Customize Sidebar, closes the host list once you open a host - Sort the dashboard's Host Status card by name, IP address or status +### Changed + +- The side menu now opens over the page when it expands on hover, so open sessions like remote desktop are not resized each time + ### Fixed - Picking a section lower down in the host editor, like Warpgate, stuttered on the way and sometimes stopped before it got there diff --git a/src/ui/sidebar/AppRail.tsx b/src/ui/sidebar/AppRail.tsx index e80c0700f..36d0383ba 100644 --- a/src/ui/sidebar/AppRail.tsx +++ b/src/ui/sidebar/AppRail.tsx @@ -312,255 +312,268 @@ export function AppRail({ ); }; + // Expanding on hover draws over the content instead of pushing it, so open + // sessions like remote desktop are not resized each time. + const takesSpace = pinned || managing; + return (
setHovered(true)} - onMouseLeave={() => setHovered(false)} - onContextMenu={(e) => { - e.preventDefault(); - // Keep the menu on screen when right-clicking near the viewport edges - const MENU_W = 200; - const MENU_H = 180; - setMenuPos({ - x: Math.min(e.clientX, window.innerWidth - MENU_W - 8), - y: Math.min(e.clientY, window.innerHeight - MENU_H - 8), - }); - if (!(e.target as HTMLElement).closest("[data-rail-item]")) { - setMenuTarget(null); - } - }} + className={`${isElectron() ? "flex" : "hidden md:flex"} relative shrink-0 transition-[width] duration-200 min-h-0`} + style={{ width: rem(takesSpace ? 172 : 40) }} > - {onOpenPalette && ( - <> - - - - )} - -
- {!pluginsSettled - ? Array.from({ length: mainItems.length || 8 }, (_, i) => ( -
- -
- )) - : bands.map((band, i) => ( -
- {i > 0 && ( - - )} - {band.items.map(renderItem)} -
- ))} - - {pluginsSettled && hiddenItems.length > 0 && ( - <> - - - {managing && - hiddenItems.map((item) => { - const Icon = item.icon; - return ( - - ); - })} - - )} -
- -
- {showPinButton && ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + onContextMenu={(e) => { + e.preventDefault(); + // Keep the menu on screen when right-clicking near the viewport edges + const MENU_W = 200; + const MENU_H = 180; + setMenuPos({ + x: Math.min(e.clientX, window.innerWidth - MENU_W - 8), + y: Math.min(e.clientY, window.innerHeight - MENU_H - 8), + }); + if (!(e.target as HTMLElement).closest("[data-rail-item]")) { + setMenuTarget(null); + } + }} + > + {onOpenPalette && ( <> )} - {footerItems.map(renderItem)} - {footerItems.length > 0 && !isElectron() && ( - - )} - {/* The desktop signs in to its own local profile by itself. */} - {!isElectron() && ( - - )} -
-
- -
+
+ {!pluginsSettled + ? Array.from({ length: mainItems.length || 8 }, (_, i) => ( +
+ +
+ )) + : bands.map((band, i) => ( +
+ {i > 0 && ( + + )} + {band.items.map(renderItem)} +
+ ))} - {menuPos && ( -
- {menuTarget && ( + {pluginsSettled && hiddenItems.length > 0 && ( <> - {menuTarget.promotable && ( - } - onClick={() => { - onOpenTab?.(menuTarget.id as TabType); - setMenuPos(null); - }} - > - {t("nav.openAsTab")} - - )} - {menuTarget.rightDockable && ( - } - onClick={() => { - onOpenInRightDock?.(menuTarget.id); - setMenuPos(null); - }} - > - {t("nav.openInRightDock")} - - )} - {menuTarget.hideable && ( - } - onClick={() => { - void toggleHiddenRailTab(menuTarget.id); - setMenuPos(null); - }} - > - {t("nav.hideFromRail")} - - )} -
+ + + {managing && + hiddenItems.map((item) => { + const Icon = item.icon; + return ( + + ); + })} )} - : null} - onClick={togglePinned} - checked={pinned} - > - {t("newUi.sidebar.userProfile.pinAppRail")} - - : null} - onClick={toggleExpandOnHover} - checked={expandOnHover} - > - {t("newUi.sidebar.userProfile.expandAppRailOnHover")} - -
- } - onClick={() => { - onOpenNavigationSettings(); - setMenuPos(null); - }} - > - {t("nav.openSettings")} -
- )} + +
+ {showPinButton && ( + <> + + + + )} + {footerItems.map(renderItem)} + {footerItems.length > 0 && !isElectron() && ( + + )} + {/* The desktop signs in to its own local profile by itself. */} + {!isElectron() && ( + + )} +
+ +
+ +
+ + {menuPos && ( +
+ {menuTarget && ( + <> + {menuTarget.promotable && ( + } + onClick={() => { + onOpenTab?.(menuTarget.id as TabType); + setMenuPos(null); + }} + > + {t("nav.openAsTab")} + + )} + {menuTarget.rightDockable && ( + } + onClick={() => { + onOpenInRightDock?.(menuTarget.id); + setMenuPos(null); + }} + > + {t("nav.openInRightDock")} + + )} + {menuTarget.hideable && ( + } + onClick={() => { + void toggleHiddenRailTab(menuTarget.id); + setMenuPos(null); + }} + > + {t("nav.hideFromRail")} + + )} +
+ + )} + : null} + onClick={togglePinned} + checked={pinned} + > + {t("newUi.sidebar.userProfile.pinAppRail")} + + : null} + onClick={toggleExpandOnHover} + checked={expandOnHover} + > + {t("newUi.sidebar.userProfile.expandAppRailOnHover")} + +
+ } + onClick={() => { + onOpenNavigationSettings(); + setMenuPos(null); + }} + > + {t("nav.openSettings")} + +
+ )} +
); } diff --git a/src/ui/tests/sidebar/AppRail.test.tsx b/src/ui/tests/sidebar/AppRail.test.tsx index fc22a105e..d4d108c93 100644 --- a/src/ui/tests/sidebar/AppRail.test.tsx +++ b/src/ui/tests/sidebar/AppRail.test.tsx @@ -54,6 +54,18 @@ describe("AppRail", () => { expect(screen.queryByLabelText("common.logout")).toBeNull(); }); + it("expands over the content on hover without taking more room", () => { + renderRail(); + const rail = screen + .getByLabelText("common.logout") + .closest(".absolute") as HTMLElement; + const slot = rail.parentElement as HTMLElement; + const collapsed = slot.style.width; + fireEvent.mouseEnter(rail); + expect(rail.style.width).not.toBe(collapsed); + expect(slot.style.width).toBe(collapsed); + }); + it("puts a footer item above the profile with its badge", async () => { registerRailItem({ id: "inbox",