From 607af01c0274bb5f013ca530dda7f0e416fa12e9 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Fri, 18 Sep 2026 19:06:47 +0900 Subject: [PATCH] Toggle the sidebar with Ctrl+B on a layout that does not write Latin letters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SidebarToggle's tooltip names Ctrl+B, or ⌘B on a Mac, and the sidebar's listener compared KeyboardEvent.key with "b". On a Russian layout the B key writes "и" and on Greek it writes "β", so the shortcut the tooltip names did nothing for anybody with one of those layouts selected. Shift+N had the same miss and was fixed with keyOf, which the paste shortcut on a Bot's screen now reads too. The sidebar reads it as well: the letter a layout writes when it writes one in ASCII, and the physical key when it writes another script. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 7 ++ app/src/components/ui/sidebar.tsx | 5 +- app/tests/sidebar-shortcut-layouts.test.tsx | 80 +++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 app/tests/sidebar-shortcut-layouts.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index c0db3abad..e534795bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### Ctrl+B shows and hides the sidebar on a layout that does not write Latin letters + +The sidebar toggle's tooltip names Ctrl+B, or ⌘B on a Mac, and the shortcut was recognised by the +character the key writes. On a Russian or Greek layout the B key writes "и" or "β", so the shortcut +did nothing there. It is now read the way the app's own shortcuts read it since Shift+N was fixed +for the same layouts: from the physical key when the layout writes a character outside ASCII there. + ### The proof-of-concept Bot reads a model name set with whitespace around it A `BOT_MODEL` carrying a leading space reached this Bot as it was written. Its startup check refuses diff --git a/app/src/components/ui/sidebar.tsx b/app/src/components/ui/sidebar.tsx index ceec714c1..1d35133d3 100644 --- a/app/src/components/ui/sidebar.tsx +++ b/app/src/components/ui/sidebar.tsx @@ -6,6 +6,7 @@ import { useRender } from "@base-ui/react/use-render"; import { cva, type VariantProps } from "class-variance-authority"; import { useIsMobile } from "@/hooks/use-mobile"; +import { keyOf } from "@/lib/hotkeys/hotkeys"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -112,8 +113,10 @@ function SidebarProvider({ // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { + // `keyOf` rather than `key`, as the app's own shortcuts read it: on a layout that writes + // another script the B key writes "и" or "β", and only its `code` still says B. if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && + keyOf(event) === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey) ) { event.preventDefault(); diff --git a/app/tests/sidebar-shortcut-layouts.test.tsx b/app/tests/sidebar-shortcut-layouts.test.tsx new file mode 100644 index 000000000..52713d12f --- /dev/null +++ b/app/tests/sidebar-shortcut-layouts.test.tsx @@ -0,0 +1,80 @@ +import { afterAll, afterEach, beforeAll, expect, test } from "bun:test"; +import { GlobalRegistrator } from "@happy-dom/global-registrator"; +import { act, cleanup, render } from "@testing-library/react"; +import { SidebarProvider, useSidebar } from "@/components/ui/sidebar"; + +/** + * The sidebar shortcut on a keyboard layout that does not write Latin letters. + * + * The toggle's tooltip names it as Ctrl+B, or ⌘B on a Mac, and the sidebar's listener compared + * `KeyboardEvent.key` with "b". On Russian the B key writes "и", and on Greek it writes "β", so the + * shortcut the tooltip names never fired for anybody with one of those layouts selected. It is the + * same miss Shift+N and the paste shortcut on a Bot's screen had. + */ + +beforeAll(() => { + GlobalRegistrator.register(); +}); + +afterEach(() => { + cleanup(); +}); + +afterAll(() => { + GlobalRegistrator.unregister(); +}); + +function SidebarState() { + return {useSidebar().open ? "open" : "closed"}; +} + +type Modifiers = Partial>; + +/** + * A sidebar, and a way to press a key at it that answers with the state it is left in. + * + * The keydown is the one a browser reports: `key` from the layout, `code` from the physical key. + */ +function renderSidebar() { + const { container } = render( + + + , + ); + return (key: string, code: string, modifiers: Modifiers) => { + act(() => { + window.dispatchEvent( + new KeyboardEvent("keydown", { + key, + code, + ...modifiers, + bubbles: true, + cancelable: true, + }), + ); + }); + return container.querySelector("output")?.textContent; + }; +} + +test("Ctrl+B and Cmd+B toggle the sidebar on a layout that writes another script", () => { + const press = renderSidebar(); + + expect(press("и", "KeyB", { ctrlKey: true })).toBe("closed"); + expect(press("β", "KeyB", { metaKey: true })).toBe("open"); +}); + +test("a layout that writes Latin letters still goes by the letter, wherever its key is", () => { + const press = renderSidebar(); + + // Dvorak writes B on the key QWERTY calls N, and X on the key QWERTY calls B. + expect(press("b", "KeyN", { ctrlKey: true })).toBe("closed"); + expect(press("x", "KeyB", { ctrlKey: true })).toBe("closed"); +}); + +test("another letter, or the B key without its modifier, still does nothing", () => { + const press = renderSidebar(); + + expect(press("т", "KeyN", { ctrlKey: true })).toBe("open"); + expect(press("и", "KeyB", {})).toBe("open"); +});