fix(web): preserve settings sidebar on first click (#1744)

This commit is contained in:
Carlos Florêncio
2026-07-16 23:18:10 +01:00
committed by GitHub
parent 6e7c63ca56
commit 1248121fd5
4 changed files with 35 additions and 7 deletions
@@ -17,7 +17,6 @@ const state = {
],
},
appSidebar: { settingsMode: false },
toggleAppSidebarSettingsMode: mocks.toggleSettingsMode,
};
let officeEnabled = false;
@@ -64,7 +63,7 @@ import { AppSidebarFooter } from "./app-sidebar-footer";
function renderFooter() {
return render(
<TooltipProvider>
<AppSidebarFooter collapsed={false} />
<AppSidebarFooter collapsed={false} onToggleSettingsMode={mocks.toggleSettingsMode} />
</TooltipProvider>,
);
}
@@ -32,6 +32,7 @@ import {
type AppSidebarFooterProps = {
collapsed: boolean;
onToggleSettingsMode: () => void;
};
type FooterIconButtonProps = {
@@ -95,7 +96,7 @@ function FooterIconButton({
);
}
export function AppSidebarFooter({ collapsed }: AppSidebarFooterProps) {
export function AppSidebarFooter({ collapsed, onToggleSettingsMode }: AppSidebarFooterProps) {
const router = useRouter();
const workspaces = useAppStore((s) => s.workspaces);
const workspaceId = workspaces.activeId;
@@ -105,7 +106,6 @@ export function AppSidebarFooter({ collapsed }: AppSidebarFooterProps) {
? resolveLastKanbanWorkspace(workspaces.items)
: resolveLastOfficeWorkspace(workspaces.items);
const settingsMode = useAppStore((s) => s.appSidebar.settingsMode);
const toggleSettingsMode = useAppStore((s) => s.toggleAppSidebarSettingsMode);
const officeEnabled = useFeature("office");
const releaseNotes = useReleaseNotes();
const [improveOpen, setImproveOpen] = useState(false);
@@ -121,7 +121,7 @@ export function AppSidebarFooter({ collapsed }: AppSidebarFooterProps) {
icon={IconSettings}
label={settingsMode ? "Close settings" : "Settings"}
collapsed={collapsed}
onClick={toggleSettingsMode}
onClick={onToggleSettingsMode}
active={settingsMode}
testId="sidebar-settings-gear"
/>
@@ -103,6 +103,7 @@ export function AppSidebar() {
const storedWidth = useAppStore((s) => s.appSidebar.width);
const toggleSection = useAppStore((s) => s.toggleAppSidebarSection);
const toggleCollapsed = useAppStore((s) => s.toggleAppSidebar);
const toggleSettingsMode = useAppStore((s) => s.toggleAppSidebarSettingsMode);
const setSettingsMode = useAppStore((s) => s.setAppSidebarSettingsMode);
const setWidth = useAppStore((s) => s.setAppSidebarWidth);
const pathname = usePathname();
@@ -140,6 +141,14 @@ export function AppSidebar() {
);
const expandedWidth = Math.max(APP_SIDEBAR_EXPANDED_WIDTH, storedWidth);
const settingsModeTogglePathnameRef = useRef<string | null>(null);
const handleToggleSettingsMode = useCallback(() => {
// History updates before React renders the new pathname; remember which
// route the user actually clicked on so delayed route sync cannot undo it.
settingsModeTogglePathnameRef.current = window.location.pathname;
toggleSettingsMode();
}, [toggleSettingsMode]);
// Keep the transient settings takeover aligned with route ownership. It is
// intentionally not persisted, so direct reloads on `/settings/...` need to
@@ -150,10 +159,11 @@ export function AppSidebar() {
if (!pathname || prevPathnameRef.current === pathname) return;
prevPathnameRef.current = pathname;
if (isSettingsRoute(pathname)) {
settingsModeTogglePathnameRef.current = null;
if (!settingsMode) setSettingsMode(true);
return;
}
if (settingsMode) {
if (settingsMode && settingsModeTogglePathnameRef.current !== pathname) {
setSettingsMode(false);
}
}, [pathname, settingsMode, setSettingsMode]);
@@ -192,7 +202,7 @@ export function AppSidebar() {
>
<AppSidebarHeader collapsed={collapsed} onToggleCollapse={toggleCollapsed} />
<AppSidebarNavigation collapsed={collapsed} inOffice={inOffice} settingsMode={settingsMode} />
<AppSidebarFooter collapsed={collapsed} />
<AppSidebarFooter collapsed={collapsed} onToggleSettingsMode={handleToggleSettingsMode} />
{!collapsed && <AppSidebarResizeHandle onMouseDown={handleResize} />}
</aside>
);
@@ -72,4 +72,23 @@ test.describe("Settings sidebar takeover", () => {
await expect(sidebar).toHaveAttribute("data-collapsed", "false");
await expect(takeover).toBeVisible();
});
test("a route change cannot undo the first gear click", async ({ testPage }) => {
await testPage.goto("/");
const takeover = testPage.getByTestId("app-sidebar-settings-mode");
await expect(testPage.getByTestId("sidebar-settings-gear")).toBeVisible();
// Keep both actions in one browser task so the route-sync effect is still
// pending when the user action arrives. The click must win.
await testPage.evaluate(() => {
window.history.pushState({}, "", "/tasks");
window.dispatchEvent(new Event("kandev:navigation"));
const gear = document.querySelector<HTMLElement>('[data-testid="sidebar-settings-gear"]');
if (!gear) throw new Error("sidebar-settings-gear not found");
gear.click();
});
await expect(takeover).toBeVisible();
});
});