adamelmore 1 месяц назад
Родитель
Сommit
7ba25c6afb
1 измененных файлов с 85 добавлено и 7 удалено
  1. 85 7
      packages/app/src/components/dialog-select-model.tsx

+ 85 - 7
packages/app/src/components/dialog-select-model.tsx

@@ -1,5 +1,6 @@
 import { Popover as Kobalte } from "@kobalte/core/popover"
-import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js"
+import { Component, ComponentProps, createEffect, createMemo, JSX, onCleanup, Show, ValidComponent } from "solid-js"
+import { createStore } from "solid-js/store"
 import { useLocal } from "@/context/local"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { popularProviders } from "@/hooks/use-providers"
@@ -92,26 +93,103 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
   triggerAs?: T
   triggerProps?: ComponentProps<T>
 }) {
-  const [open, setOpen] = createSignal(false)
+  const [store, setStore] = createStore<{
+    open: boolean
+    dismiss: "escape" | "outside" | null
+    trigger?: HTMLElement
+    content?: HTMLElement
+  }>({
+    open: false,
+    dismiss: null,
+    trigger: undefined,
+    content: undefined,
+  })
   const dialog = useDialog()
 
   const handleManage = () => {
-    setOpen(false)
+    setStore("open", false)
     dialog.show(() => <DialogManageModels />)
   }
   const language = useLanguage()
 
+  createEffect(() => {
+    if (!store.open) return
+
+    const inside = (node: Node | null | undefined) => {
+      if (!node) return false
+      const el = store.content
+      if (el && el.contains(node)) return true
+      const anchor = store.trigger
+      if (anchor && anchor.contains(node)) return true
+      return false
+    }
+
+    const onKeyDown = (event: KeyboardEvent) => {
+      if (event.key !== "Escape") return
+      setStore("dismiss", "escape")
+      setStore("open", false)
+      event.preventDefault()
+      event.stopPropagation()
+    }
+
+    const onPointerDown = (event: PointerEvent) => {
+      const target = event.target
+      if (!(target instanceof Node)) return
+      if (inside(target)) return
+      setStore("dismiss", "outside")
+      setStore("open", false)
+    }
+
+    const onFocusIn = (event: FocusEvent) => {
+      if (!store.content) return
+      const target = event.target
+      if (!(target instanceof Node)) return
+      if (inside(target)) return
+      setStore("dismiss", "outside")
+      setStore("open", false)
+    }
+
+    window.addEventListener("keydown", onKeyDown, true)
+    window.addEventListener("pointerdown", onPointerDown, true)
+    window.addEventListener("focusin", onFocusIn, true)
+
+    onCleanup(() => {
+      window.removeEventListener("keydown", onKeyDown, true)
+      window.removeEventListener("pointerdown", onPointerDown, true)
+      window.removeEventListener("focusin", onFocusIn, true)
+    })
+  })
+
   return (
-    <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
-      <Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}>
+    <Kobalte
+      open={store.open}
+      onOpenChange={(next) => {
+        if (next) setStore("dismiss", null)
+        setStore("open", next)
+      }}
+      placement="top-start"
+      gutter={8}
+    >
+      <Kobalte.Trigger
+        ref={(el) => setStore("trigger", el)}
+        as={props.triggerAs ?? "div"}
+        {...(props.triggerProps as any)}
+      >
         {props.children}
       </Kobalte.Trigger>
       <Kobalte.Portal>
-        <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
+        <Kobalte.Content
+          ref={(el) => setStore("content", el)}
+          class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
+          onCloseAutoFocus={(event) => {
+            if (store.dismiss === "outside") event.preventDefault()
+            setStore("dismiss", null)
+          }}
+        >
           <Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
           <ModelList
             provider={props.provider}
-            onSelect={() => setOpen(false)}
+            onSelect={() => setStore("open", false)}
             class="p-1"
             action={
               <IconButton