diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index f8913eee4fbc..d5d3d8411d0c 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -1,6 +1,7 @@ import { Component, createMemo, createSignal, Show } from "solid-js" import { useSync } from "@/context/sync" import { useSDK } from "@/context/sdk" +import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" @@ -16,6 +17,7 @@ const statusLabels = { export const DialogSelectMcp: Component = () => { const sync = useSync() const sdk = useSDK() + const dialog = useDialog() const language = useLanguage() const [loading, setLoading] = createSignal(null) @@ -58,6 +60,12 @@ export const DialogSelectMcp: Component = () => { items={items} filterKeys={["name", "status"]} sortBy={(a, b) => a.name.localeCompare(b.name)} + onKeyEvent={(event) => { + if (event.key !== "Escape") return + event.preventDefault() + event.stopPropagation() + dialog.close() + }} onSelect={(x) => { if (x) toggle(x.name) }} diff --git a/packages/app/src/components/list-escape-key.test.tsx b/packages/app/src/components/list-escape-key.test.tsx new file mode 100644 index 000000000000..c5d785e0a7a8 --- /dev/null +++ b/packages/app/src/components/list-escape-key.test.tsx @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test" +import { dispatchListKeyEvent } from "@opencode-ai/ui/list-keyboard" + +describe("list keyboard handling", () => { + test("forwards Escape to onKeyEvent for searchable lists", () => { + const events: string[] = [] + const items = [{ id: "github", name: "github" }] + const event = new KeyboardEvent("keydown", { key: "Escape" }) + + const { selected, index } = dispatchListKeyEvent( + event, + items, + null, + (item) => item.id, + (keyboardEvent, item) => { + events.push(`${keyboardEvent.key}:${item?.id ?? "none"}`) + }, + ) + + expect(selected).toBeUndefined() + expect(index).toBe(-1) + expect(events).toEqual(["Escape:none"]) + }) +}) diff --git a/packages/ui/src/components/list-keyboard.tsx b/packages/ui/src/components/list-keyboard.tsx new file mode 100644 index 000000000000..494ac9910433 --- /dev/null +++ b/packages/ui/src/components/list-keyboard.tsx @@ -0,0 +1,17 @@ +export function resolveListSelection(items: T[], activeKey: string | null, keyOf: (item: T) => string) { + const selected = items.find((item) => keyOf(item) === activeKey) + const index = selected ? items.indexOf(selected) : -1 + return { selected, index } +} + +export function dispatchListKeyEvent( + event: KeyboardEvent, + items: T[], + activeKey: string | null, + keyOf: (item: T) => string, + onKeyEvent: ((event: KeyboardEvent, item: T | undefined) => void) | undefined, +) { + const { selected, index } = resolveListSelection(items, activeKey, keyOf) + onKeyEvent?.(event, selected) + return { selected, index } +} diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index aa2347037ea4..1f21853bf687 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -4,6 +4,7 @@ import { createStore } from "solid-js/store" import { useI18n } from "../context/i18n" import { Icon, type IconProps } from "./icon" import { IconButton } from "./icon-button" +import { dispatchListKeyEvent } from "./list-keyboard" import { TextField } from "./text-field" function findByKey(container: HTMLElement, key: string) { @@ -166,14 +167,12 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) const handleKey = (e: KeyboardEvent) => { setStore("mouseActive", false) - if (e.key === "Escape") return const all = flat() - const selected = all.find((x) => props.key(x) === active()) - const index = selected ? all.indexOf(selected) : -1 - props.onKeyEvent?.(e, selected) + const { selected, index } = dispatchListKeyEvent(e, all, active(), props.key, props.onKeyEvent) if (e.defaultPrevented) return + if (e.key === "Escape") return if (e.key === "Enter" && !e.isComposing) { e.preventDefault()