mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-09 22:31:08 +00:00
Fix keyboard shortcuts for non-QWERTY layouts (#6162)
Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
parent
6c87ce8cd7
commit
878b0bd8d8
2 changed files with 290 additions and 2 deletions
|
|
@ -1693,8 +1693,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
// Check for Command/Ctrl + Period (with or without Shift)
|
||||
// Using event.code for better cross-platform compatibility
|
||||
if ((event.metaKey || event.ctrlKey) && event.code === "Period") {
|
||||
// Using event.key to respect keyboard layouts (e.g., Dvorak)
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === ".") {
|
||||
event.preventDefault() // Prevent default browser behavior
|
||||
|
||||
if (event.shiftKey) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
// npx vitest run src/components/chat/__tests__/ChatView.keyboard-fix.spec.tsx
|
||||
|
||||
import React from "react"
|
||||
import { render, fireEvent } from "@/utils/test-utils"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
|
||||
import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
import ChatView, { ChatViewProps } from "../ChatView"
|
||||
|
||||
// Mock vscode API
|
||||
vi.mock("@src/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock use-sound hook
|
||||
vi.mock("use-sound", () => ({
|
||||
default: vi.fn().mockImplementation(() => {
|
||||
return [vi.fn()]
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock components
|
||||
vi.mock("../BrowserSessionRow", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("../ChatRow", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("../AutoApproveMenu", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("../../common/VersionIndicator", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/modals/Announcement", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/welcome/RooCloudCTA", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/welcome/RooTips", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/welcome/RooHero", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock("../common/TelemetryBanner", () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
// Mock i18n
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
initReactI18next: {
|
||||
type: "3rdParty",
|
||||
init: () => {},
|
||||
},
|
||||
Trans: ({ i18nKey }: { i18nKey: string }) => <>{i18nKey}</>,
|
||||
}))
|
||||
|
||||
vi.mock("../ChatTextArea", () => {
|
||||
return {
|
||||
default: React.forwardRef(function MockChatTextArea(
|
||||
_props: any,
|
||||
ref: React.ForwardedRef<{ focus: () => void }>,
|
||||
) {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: vi.fn(),
|
||||
}))
|
||||
return <div data-testid="chat-textarea" />
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
// Mock VSCode components
|
||||
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodeButton: ({ children, onClick }: any) => <button onClick={onClick}>{children}</button>,
|
||||
VSCodeLink: ({ children, href }: any) => <a href={href}>{children}</a>,
|
||||
}))
|
||||
|
||||
// Mock window.postMessage to trigger state hydration
|
||||
const mockPostMessage = (state: any) => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: "state",
|
||||
state: {
|
||||
version: "1.0.0",
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
alwaysAllowExecute: false,
|
||||
cloudIsAuthenticated: false,
|
||||
telemetrySetting: "enabled",
|
||||
mode: "code",
|
||||
customModes: [],
|
||||
...state,
|
||||
},
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}
|
||||
|
||||
const defaultProps: ChatViewProps = {
|
||||
isHidden: false,
|
||||
showAnnouncement: false,
|
||||
hideAnnouncement: () => {},
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const renderChatView = (props: Partial<ChatViewProps> = {}) => {
|
||||
return render(
|
||||
<ExtensionStateContextProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatView {...defaultProps} {...props} />
|
||||
</QueryClientProvider>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe("ChatView - Keyboard Shortcut Fix for Dvorak", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("uses event.key instead of event.code for keyboard shortcuts", async () => {
|
||||
renderChatView()
|
||||
|
||||
// Hydrate state
|
||||
mockPostMessage({
|
||||
mode: "code",
|
||||
customModes: [],
|
||||
})
|
||||
|
||||
// Wait for component to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Clear any initial calls
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Test 1: Period key should trigger mode switch
|
||||
fireEvent.keyDown(window, {
|
||||
key: ".",
|
||||
code: "Period",
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
})
|
||||
|
||||
// Wait for event to be processed
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Check if mode switch was triggered
|
||||
const callsAfterPeriod = (vscode.postMessage as any).mock.calls
|
||||
const modeSwitchAfterPeriod = callsAfterPeriod.some((call: any[]) => call[0]?.type === "mode")
|
||||
expect(modeSwitchAfterPeriod).toBe(true)
|
||||
|
||||
// Clear mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Test 2: V key on physical Period key (Dvorak) should NOT trigger mode switch
|
||||
fireEvent.keyDown(window, {
|
||||
key: "v",
|
||||
code: "Period", // Physical key is Period, but produces 'v'
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
})
|
||||
|
||||
// Wait for event to be processed
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Check that NO mode switch was triggered
|
||||
const callsAfterV = (vscode.postMessage as any).mock.calls
|
||||
const modeSwitchAfterV = callsAfterV.some((call: any[]) => call[0]?.type === "mode")
|
||||
expect(modeSwitchAfterV).toBe(false)
|
||||
})
|
||||
|
||||
it("prevents default behavior when mode switch is triggered", () => {
|
||||
renderChatView()
|
||||
|
||||
// Hydrate state
|
||||
mockPostMessage({
|
||||
mode: "code",
|
||||
customModes: [],
|
||||
})
|
||||
|
||||
// Create a keyboard event with preventDefault spy
|
||||
const event = new KeyboardEvent("keydown", {
|
||||
key: ".",
|
||||
code: "Period",
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
|
||||
const preventDefaultSpy = vi.spyOn(event, "preventDefault")
|
||||
|
||||
// Dispatch the event
|
||||
window.dispatchEvent(event)
|
||||
|
||||
// Verify preventDefault was called
|
||||
expect(preventDefaultSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("works with Cmd key on Mac", async () => {
|
||||
renderChatView()
|
||||
|
||||
// Hydrate state
|
||||
mockPostMessage({
|
||||
mode: "code",
|
||||
customModes: [],
|
||||
})
|
||||
|
||||
// Wait for component to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Clear any initial calls
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Test with Cmd key (Mac)
|
||||
fireEvent.keyDown(window, {
|
||||
key: ".",
|
||||
code: "Period",
|
||||
ctrlKey: false,
|
||||
metaKey: true, // Cmd key on Mac
|
||||
shiftKey: false,
|
||||
})
|
||||
|
||||
// Wait for event to be processed
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Check if mode switch was triggered
|
||||
const calls = (vscode.postMessage as any).mock.calls
|
||||
const modeSwitch = calls.some((call: any[]) => call[0]?.type === "mode")
|
||||
expect(modeSwitch).toBe(true)
|
||||
})
|
||||
|
||||
it("handles Shift modifier for previous mode", async () => {
|
||||
renderChatView()
|
||||
|
||||
// Hydrate state
|
||||
mockPostMessage({
|
||||
mode: "code",
|
||||
customModes: [],
|
||||
})
|
||||
|
||||
// Wait for component to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Clear any initial calls
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Test with Shift modifier
|
||||
fireEvent.keyDown(window, {
|
||||
key: ".",
|
||||
code: "Period",
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
shiftKey: true, // Should go to previous mode
|
||||
})
|
||||
|
||||
// Wait for event to be processed
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Check if mode switch was triggered
|
||||
const calls = (vscode.postMessage as any).mock.calls
|
||||
const modeSwitch = calls.some((call: any[]) => call[0]?.type === "mode")
|
||||
expect(modeSwitch).toBe(true)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue