From ea759ab0922459713c365ec00ce8f8d449bc4ddf Mon Sep 17 00:00:00 2001 From: Dariusz Koryto Date: Tue, 6 Oct 2026 04:01:26 +0200 Subject: [PATCH] feat(web): add a keyboard shortcut to show or hide the sidebar (#475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(web): add a keyboard shortcut to show or hide the sidebar The notebook sidebar can already collapse into an icon rail, but only with the mouse. Add a configurable "Show/hide sidebar" shortcut, Ctrl/⌘+\ by default, next to the existing outline shortcut. It toggles the same persisted collapsed state, works only in the desktop layout, and is ignored while a dialog or another view is open. The collapse button tooltip now shows the shortcut. Refs #97 * fix(web): ignore the sidebar shortcut while Focus Mode is on Focus Mode hides the notebook sidebar, so the shortcut used to change and save its collapsed state without any visible feedback. It now does nothing while Focus Mode is active, and the saved state is left as it was when Focus Mode ends. Refs #97 --- apps/web/src/components/NotebookPane.tsx | 11 ++++++++--- apps/web/src/components/WorkspaceApp.tsx | 20 ++++++++++++++++++++ apps/web/src/lib/app-helpers.test.mjs | 23 +++++++++++++++++++++++ apps/web/src/lib/app-helpers.ts | 13 ++++++++++++- packages/shared/src/i18n/en-US.ts | 1 + packages/shared/src/i18n/ja.ts | 1 + packages/shared/src/i18n/zh-CN.ts | 1 + 7 files changed, 66 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/NotebookPane.tsx b/apps/web/src/components/NotebookPane.tsx index 2a543ea7..9c7fa805 100644 --- a/apps/web/src/components/NotebookPane.tsx +++ b/apps/web/src/components/NotebookPane.tsx @@ -248,14 +248,17 @@ const SidebarCollapseButton = ({ onToggle, className, tooltipSide = "bottom", + shortcutLabel, }: { collapsed: boolean; onToggle: () => void; className?: string; tooltipSide?: "top" | "right" | "bottom" | "left"; + shortcutLabel?: string; }) => { const { t } = useTranslation(); const label = t(collapsed ? "notebookPane.expandSidebar" : "notebookPane.collapseSidebar"); + const tooltipLabel = shortcutLabel ? `${label} (${shortcutLabel})` : label; return ( @@ -275,7 +278,7 @@ const SidebarCollapseButton = ({ {collapsed ? ); @@ -512,6 +515,7 @@ export const NotebookPane = ({ isResettingDemo = false, collapsed = false, onToggleCollapsed, + collapseShortcutLabel, }: { repository: EdgeEverRepository; user: AuthUser | null; @@ -550,6 +554,7 @@ export const NotebookPane = ({ isResettingDemo?: boolean; collapsed?: boolean; onToggleCollapsed?: () => void; + collapseShortcutLabel?: string; }) => { const { t } = useTranslation(); const showDescendantNotes = useShowDescendantNotesPreference(); @@ -946,7 +951,7 @@ export const NotebookPane = ({ notice={deployedUpdateUnseen} onClick={onOpenSettings} /> - + @@ -1176,7 +1181,7 @@ export const NotebookPane = ({ {onToggleCollapsed ? ( - + ) : null} diff --git a/apps/web/src/components/WorkspaceApp.tsx b/apps/web/src/components/WorkspaceApp.tsx index 8aca7455..8da6de73 100644 --- a/apps/web/src/components/WorkspaceApp.tsx +++ b/apps/web/src/components/WorkspaceApp.tsx @@ -71,6 +71,8 @@ import { isTextEntryTarget, getSearchShortcutScope, getShortcutActionForEvent, + formatShortcutBinding, + canToggleNotebookSidebar, getNotebookDropSortOrder, getNotebookMoveOptions, getMemoIdsNeedingMove, @@ -2810,6 +2812,19 @@ export const WorkspaceApp = ({ return; } + if (action === "toggleSidebar") { + event.preventDefault(); + if ( + event.repeat + || event.isComposing + || !canToggleNotebookSidebar({ isDesktop, focusModeActive: desktopFocusModeActive }) + ) { + return; + } + setNotebookSidebarCollapsed(!notebookSidebarCollapsed); + return; + } + if (action === "focusSearch") { event.preventDefault(); if (getSearchShortcutScope(selectedMemoId) === "memo-list") { @@ -2868,6 +2883,10 @@ export const WorkspaceApp = ({ handleGlobalSearch, handleMobileSearch, shortcutSettings, + isDesktop, + desktopFocusModeActive, + notebookSidebarCollapsed, + setNotebookSidebarCollapsed, emptyTrashConfirmationOpen, memoDeleteConfirmation, memoView, @@ -3083,6 +3102,7 @@ export const WorkspaceApp = ({ isResettingDemo={resetDemoMutation.isPending} collapsed={desktopNotebookSidebarCollapsed} onToggleCollapsed={() => setNotebookSidebarCollapsed(!notebookSidebarCollapsed)} + collapseShortcutLabel={formatShortcutBinding(shortcutSettings.toggleSidebar)} /> )} diff --git a/apps/web/src/lib/app-helpers.test.mjs b/apps/web/src/lib/app-helpers.test.mjs index 3bc0b3a6..9506a990 100644 --- a/apps/web/src/lib/app-helpers.test.mjs +++ b/apps/web/src/lib/app-helpers.test.mjs @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; import { DEFAULT_SHORTCUT_SETTINGS, DESKTOP_FOCUS_MODE_STORAGE_KEY, @@ -14,6 +15,7 @@ import { SHORTCUT_SETTINGS_STORAGE_KEY, getSearchShortcutScope, getShortcutActionForEvent, + canToggleNotebookSidebar, getNotebookSortComparator, readEditorContentAlignmentPreference, readNotebookSortPreference, @@ -445,6 +447,12 @@ describe("workspace shortcut preferences", () => { shift: true, alt: false, }); + expect(DEFAULT_SHORTCUT_SETTINGS.toggleSidebar).toEqual({ + key: "\\", + ctrlOrMeta: true, + shift: false, + alt: false, + }); }); test("migrates the unreleased reading protection shortcut without replacing custom bindings", () => { @@ -532,6 +540,21 @@ describe("workspace shortcut preferences", () => { keyboardEvent("!", { code: "Digit1", ctrlKey: true, shiftKey: true }), DEFAULT_SHORTCUT_SETTINGS, )).toBe("toggleOutline"); + expect(getShortcutActionForEvent( + keyboardEvent("\\", { metaKey: true }), + DEFAULT_SHORTCUT_SETTINGS, + )).toBe("toggleSidebar"); + }); + + test("toggles the sidebar only when it is visible", () => { + expect(canToggleNotebookSidebar({ isDesktop: true, focusModeActive: false })).toBe(true); + // Focus Mode hides the sidebar, so the shortcut must not change its saved state. + expect(canToggleNotebookSidebar({ isDesktop: true, focusModeActive: true })).toBe(false); + expect(canToggleNotebookSidebar({ isDesktop: false, focusModeActive: false })).toBe(false); + expect(canToggleNotebookSidebar({ isDesktop: false, focusModeActive: true })).toBe(false); + // The workspace checks the same flag that hides the sidebar in its layout. + const workspaceSource = readFileSync(new URL("../components/WorkspaceApp.tsx", import.meta.url), "utf8"); + expect(workspaceSource).toContain("canToggleNotebookSidebar({ isDesktop, focusModeActive: desktopFocusModeActive })"); }); }); diff --git a/apps/web/src/lib/app-helpers.ts b/apps/web/src/lib/app-helpers.ts index 153bfab4..3f6a3d9b 100644 --- a/apps/web/src/lib/app-helpers.ts +++ b/apps/web/src/lib/app-helpers.ts @@ -26,7 +26,8 @@ export type ShortcutAction = | "saveAndSync" | "toggleReadingProtection" | "toggleEditorMode" - | "toggleOutline"; + | "toggleOutline" + | "toggleSidebar"; export type ShortcutBinding = { key: string; ctrlOrMeta: boolean; @@ -190,6 +191,7 @@ export const getShortcutActionOptions = ( { value: "toggleReadingProtection", label: t("shortcuts.actions.toggleReadingProtection.label") }, { value: "toggleEditorMode", label: t("shortcuts.actions.toggleEditorMode.label") }, { value: "toggleOutline", label: t("shortcuts.actions.toggleOutline.label") }, + { value: "toggleSidebar", label: t("shortcuts.actions.toggleSidebar.label") }, ]; export const DEFAULT_SHORTCUT_SETTINGS: ShortcutSettings = { @@ -206,6 +208,7 @@ export const DEFAULT_SHORTCUT_SETTINGS: ShortcutSettings = { toggleReadingProtection: { key: "e", ctrlOrMeta: true, shift: false, alt: false }, toggleEditorMode: { key: "/", ctrlOrMeta: true, shift: false, alt: false }, toggleOutline: { key: "1", ctrlOrMeta: true, shift: true, alt: false }, + toggleSidebar: { key: "\\", ctrlOrMeta: true, shift: false, alt: false }, }; const LEGACY_READING_PROTECTION_SHORTCUT: ShortcutBinding = { @@ -233,6 +236,7 @@ const SHORTCUT_ACTION_VALUES: ShortcutAction[] = [ "toggleReadingProtection", "toggleEditorMode", "toggleOutline", + "toggleSidebar", ]; export const isDefaultMemoTitle = (title: string | null | undefined) => title?.trim() === DEFAULT_MEMO_TITLE; @@ -583,6 +587,13 @@ export const shortcutBindingsEqual = (first: ShortcutBinding, second: ShortcutBi first.shift === second.shift && first.alt === second.alt; +// The sidebar shortcut only acts when the sidebar can be seen: Focus Mode hides +// it, so toggling there would change a persisted state without any feedback. +export const canToggleNotebookSidebar = ({ isDesktop, focusModeActive }: { + isDesktop: boolean; + focusModeActive: boolean; +}) => isDesktop && !focusModeActive; + export const getShortcutActionForEvent = (event: KeyboardEvent, settings: ShortcutSettings): ShortcutAction | null => { const eventBinding = shortcutBindingFromKeyboardEvent(event); diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 1b70e904..3bdf1c36 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -1681,6 +1681,7 @@ export const enUS = { toggleReadingProtection: { label: "Toggle reading protection" }, toggleEditorMode: { label: "Switch editor mode" }, toggleOutline: { label: "Show/hide document outline" }, + toggleSidebar: { label: "Show/hide sidebar" }, }, }, quickSwitcher: { diff --git a/packages/shared/src/i18n/ja.ts b/packages/shared/src/i18n/ja.ts index e4a35334..83c7d9fe 100644 --- a/packages/shared/src/i18n/ja.ts +++ b/packages/shared/src/i18n/ja.ts @@ -1681,6 +1681,7 @@ export const ja = { toggleReadingProtection: { label: "閲覧保護を切り替え" }, toggleEditorMode: { label: "エディタモードを切り替え" }, toggleOutline: { label: "ドキュメントアウトラインの表示/非表示" }, + toggleSidebar: { label: "サイドバーの表示/非表示" }, }, }, quickSwitcher: { diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 890435fb..b95021a5 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -1681,6 +1681,7 @@ export const zhCN = { toggleReadingProtection: { label: "切换阅读保护" }, toggleEditorMode: { label: "切换编辑模式" }, toggleOutline: { label: "显示/隐藏文档大纲" }, + toggleSidebar: { label: "显示/隐藏侧边栏" }, }, }, quickSwitcher: {