diff --git a/ui/src/app/app-host-native-shell.test.ts b/ui/src/app/app-host-native-shell.test.ts index 31bdb0a6b1a0..88a8fbbd00f8 100644 --- a/ui/src/app/app-host-native-shell.test.ts +++ b/ui/src/app/app-host-native-shell.test.ts @@ -20,6 +20,12 @@ type ShellNavigationState = { updated: () => void; }; +type ShellSettingsEscapeState = ShellKeyboardState & { + lastWorkspaceLocation: { routeId: "usage"; pathname: string; search: string }; + navDrawerOpen: boolean; + routeState: { routeId: "appearance" }; +}; + type TestWebKitWindow = Window & { webkit?: { messageHandlers: { @@ -107,6 +113,41 @@ describe("OpenClaw native shell", () => { expect(navigate).toHaveBeenCalledWith("appearance", undefined); }); + it("keeps the raw config editor unchanged when Escape is pressed", () => { + const navigate = vi.fn(); + const shell = document.createElement( + "openclaw-app-shell", + ) as unknown as ShellSettingsEscapeState; + shell.runtime = { + context: { + navigate, + overlays: { snapshot: { devicePairSetupOpen: false } }, + } as unknown as ApplicationContext, + }; + shell.lastWorkspaceLocation = { routeId: "usage", pathname: "/usage", search: "" }; + shell.navDrawerOpen = false; + shell.routeState = { routeId: "appearance" }; + const rawField = document.body.appendChild(document.createElement("label")); + rawField.className = "config-raw-field"; + const rawEditor = rawField.appendChild(document.createElement("textarea")); + rawEditor.value = '{ "gateway": { "port": 18789 } }'; + rawEditor.focus(); + const onInput = vi.fn(); + rawEditor.addEventListener("input", onInput); + rawEditor.addEventListener("keydown", (event) => shell.handleDocumentKeydown(event)); + + try { + const event = new KeyboardEvent("keydown", { key: "Escape", cancelable: true }); + rawEditor.dispatchEvent(event); + + expect(rawEditor.value).toBe('{ "gateway": { "port": 18789 } }'); + expect(onInput).not.toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); + } finally { + rawField.remove(); + } + }); + it("toggles the navigation sidebar when the native macOS titlebar button fires", () => { const snapshot = { navCollapsed: false }; const update = vi.fn((next: { navCollapsed: boolean }) => { diff --git a/ui/src/components/settings-sidebar.test.ts b/ui/src/components/settings-sidebar.test.ts index 7c9982f2a1c3..2a2cb7c0d447 100644 --- a/ui/src/components/settings-sidebar.test.ts +++ b/ui/src/components/settings-sidebar.test.ts @@ -440,6 +440,54 @@ describe("settings sidebar search", () => { expect(onNavigate).toHaveBeenCalledWith("channels"); }); + it("clears a focused search before Escape exits Settings", () => { + let searchQuery = "gateway"; + const onExit = vi.fn(); + const rerender = () => { + render( + renderSettingsSidebar({ + basePath: "", + activeRouteId: "appearance", + offline: false, + lastError: null, + gatewayVersion: "", + updateAvailable: null, + updateBusy: false, + onUpdate: vi.fn(), + ...inactiveRefresh, + searchQuery, + onExit, + onRetryConnect: vi.fn(), + onNavigate: vi.fn(), + onSearchQueryChange: (nextQuery) => { + searchQuery = nextQuery; + rerender(); + }, + preloadTimers: new Map(), + saveIndicator: saveIndicator(), + }), + container, + ); + }; + + rerender(); + const input = container.querySelector(".settings-sidebar__search-input"); + expect(input).not.toBeNull(); + input?.focus(); + + input?.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }), + ); + expect(searchQuery).toBe(""); + expect(document.activeElement).toBe(input); + expect(onExit).not.toHaveBeenCalled(); + + input?.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }), + ); + expect(onExit).toHaveBeenCalledOnce(); + }); + it("renders refreshed settings route titles from the active locale", async () => { i18n.registerTranslation("pt-BR", { routeTitles: { diff --git a/ui/src/components/settings-sidebar.ts b/ui/src/components/settings-sidebar.ts index 493e92fb3c3b..0d0f3137953f 100644 --- a/ui/src/components/settings-sidebar.ts +++ b/ui/src/components/settings-sidebar.ts @@ -272,11 +272,15 @@ export function renderSettingsSidebar(props: SettingsSidebarProps) { @input=${(event: Event) => props.onSearchQueryChange((event.currentTarget as HTMLInputElement).value)} @keydown=${(event: KeyboardEvent) => { - if (event.key !== "Escape" || !props.searchQuery) { + if (event.key !== "Escape") { return; } event.preventDefault(); - props.onSearchQueryChange(""); + if (props.searchQuery) { + props.onSearchQueryChange(""); + return; + } + props.onExit(); }} /> ${props.searchQuery