feat(web): add a keyboard shortcut to show or hide the sidebar (#475)

* 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
This commit is contained in:
Dariusz Koryto authored and GitHub committed 2026-10-06 10:01:26 +08:00
1 parent 4a6f7947c2
commit ea759ab092
7 files changed
+66 -4

No files matched your search

+8 -3
View File
@@ -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 (
<TooltipProvider delayDuration={0} skipDelayDuration={0}>
@@ -275,7 +278,7 @@ const SidebarCollapseButton = ({
{collapsed ? <ChevronsRight className="h-4 w-4" aria-hidden="true" /> : <ChevronsLeft className="h-4 w-4" aria-hidden="true" />}
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>{label}</TooltipContent>
<TooltipContent side={tooltipSide}>{tooltipLabel}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
@@ -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}
/>
<SidebarCollapseButton collapsed onToggle={onToggleCollapsed} className="h-9 w-9" tooltipSide="right" />
<SidebarCollapseButton collapsed onToggle={onToggleCollapsed} className="h-9 w-9" tooltipSide="right" shortcutLabel={collapseShortcutLabel} />
</div>
</nav>
</div>
@@ -1176,7 +1181,7 @@ export const NotebookPane = ({
</button>
<DesktopUpdateNotice />
{onToggleCollapsed ? (
<SidebarCollapseButton collapsed={collapsed} onToggle={onToggleCollapsed} className="hidden lg:inline-flex" />
<SidebarCollapseButton collapsed={collapsed} onToggle={onToggleCollapsed} className="hidden lg:inline-flex" shortcutLabel={collapseShortcutLabel} />
) : null}
</div>
</div>
+20
View File
@@ -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)}
/>
</Suspense>
)}
+23
View File
@@ -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 })");
});
});
+12 -1
View File
@@ -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);
+1
View File
@@ -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: {
+1
View File
@@ -1681,6 +1681,7 @@ export const ja = {
toggleReadingProtection: { label: "閲覧保護を切り替え" },
toggleEditorMode: { label: "エディタモードを切り替え" },
toggleOutline: { label: "ドキュメントアウトラインの表示/非表示" },
toggleSidebar: { label: "サイドバーの表示/非表示" },
},
},
quickSwitcher: {
+1
View File
@@ -1681,6 +1681,7 @@ export const zhCN = {
toggleReadingProtection: { label: "切换阅读保护" },
toggleEditorMode: { label: "切换编辑模式" },
toggleOutline: { label: "显示/隐藏文档大纲" },
toggleSidebar: { label: "显示/隐藏侧边栏" },
},
},
quickSwitcher: {