diff --git a/CHANGELOG.md b/CHANGELOG.md index b031ba16f..c0318b599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ 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. ### Scrolling a Bot's browser no longer scrolls or zooms the page around it While somebody drives a Bot's browser, a turn of the mouse wheel over its screen is sent to it, and diff --git a/app/src/components/ui/sidebar.tsx b/app/src/components/ui/sidebar.tsx index a9d79fb63..9e927cf9a 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"); +});