fix(ui): make Settings Escape contextual (#128099)

* fix(ui): make Settings Escape contextual

Co-authored-by: shakkernerd <165377636+shakkernerd@users.noreply.github.com>

* fix(ui): scope Settings Escape to search

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: shakkernerd <165377636+shakkernerd@users.noreply.github.com>
This commit is contained in:
ClawSweeper
2026-08-22 21:35:24 -07:00
committed by GitHub
parent 3651289881
commit 663f4e796d
3 changed files with 95 additions and 2 deletions
+41
View File
@@ -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 }) => {
@@ -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<HTMLInputElement>(".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: {
+6 -2
View File
@@ -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