summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-24 16:28:30 -0600
committerAdam <[email protected]>2026-01-24 16:57:38 -0600
commitd97cd56867c83ceea37d17d47b270d986bd9735c (patch)
tree9c49a410fc6bceb60b78434c472485b90ad2d8dc
parent43906f56c8640b224c6266e970a23097619efb93 (diff)
downloadopencode-d97cd56867c83ceea37d17d47b270d986bd9735c.tar.gz
opencode-d97cd56867c83ceea37d17d47b270d986bd9735c.zip
fix(ui): popover exit ux
-rw-r--r--packages/ui/src/components/popover.tsx100
1 files changed, 97 insertions, 3 deletions
diff --git a/packages/ui/src/components/popover.tsx b/packages/ui/src/components/popover.tsx
index 1c6588250..9644c8741 100644
--- a/packages/ui/src/components/popover.tsx
+++ b/packages/ui/src/components/popover.tsx
@@ -1,5 +1,15 @@
import { Popover as Kobalte } from "@kobalte/core/popover"
-import { ComponentProps, JSXElement, ParentProps, Show, splitProps, ValidComponent } from "solid-js"
+import {
+ ComponentProps,
+ JSXElement,
+ ParentProps,
+ Show,
+ createEffect,
+ createSignal,
+ onCleanup,
+ splitProps,
+ ValidComponent,
+} from "solid-js"
import { useI18n } from "../context/i18n"
import { IconButton } from "./icon-button"
@@ -27,17 +37,96 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
"description",
"class",
"classList",
+ "style",
"children",
"portal",
+ "open",
+ "defaultOpen",
+ "onOpenChange",
+ "modal",
])
+ const [contentRef, setContentRef] = createSignal<HTMLElement | undefined>(undefined)
+ const [triggerRef, setTriggerRef] = createSignal<HTMLElement | undefined>(undefined)
+ const [dismiss, setDismiss] = createSignal<"escape" | "outside" | null>(null)
+
+ const [uncontrolledOpen, setUncontrolledOpen] = createSignal<boolean>(local.defaultOpen ?? false)
+
+ const controlled = () => local.open !== undefined
+ const opened = () => {
+ if (controlled()) return local.open ?? false
+ return uncontrolledOpen()
+ }
+
+ const onOpenChange = (next: boolean) => {
+ if (next) setDismiss(null)
+ if (local.onOpenChange) local.onOpenChange(next)
+ if (controlled()) return
+ setUncontrolledOpen(next)
+ }
+
+ createEffect(() => {
+ if (!opened()) return
+
+ const inside = (node: Node | null | undefined) => {
+ if (!node) return false
+ const content = contentRef()
+ if (content && content.contains(node)) return true
+ const trigger = triggerRef()
+ if (trigger && trigger.contains(node)) return true
+ return false
+ }
+
+ const close = (reason: "escape" | "outside") => {
+ setDismiss(reason)
+ onOpenChange(false)
+ }
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key !== "Escape") return
+ close("escape")
+ event.preventDefault()
+ event.stopPropagation()
+ }
+
+ const onPointerDown = (event: PointerEvent) => {
+ const target = event.target
+ if (!(target instanceof Node)) return
+ if (inside(target)) return
+ close("outside")
+ }
+
+ const onFocusIn = (event: FocusEvent) => {
+ const target = event.target
+ if (!(target instanceof Node)) return
+ if (inside(target)) return
+ close("outside")
+ }
+
+ 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)
+ })
+ })
+
const content = () => (
<Kobalte.Content
+ ref={(el: HTMLElement) => setContentRef(el)}
data-component="popover-content"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
+ style={local.style}
+ onCloseAutoFocus={(event: Event) => {
+ if (dismiss() === "outside") event.preventDefault()
+ setDismiss(null)
+ }}
>
{/* <Kobalte.Arrow data-slot="popover-arrow" /> */}
<Show when={local.title}>
@@ -60,8 +149,13 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
)
return (
- <Kobalte gutter={4} {...rest}>
- <Kobalte.Trigger as={local.triggerAs ?? "div"} data-slot="popover-trigger" {...(local.triggerProps as any)}>
+ <Kobalte gutter={4} {...rest} open={opened()} onOpenChange={onOpenChange} modal={local.modal ?? false}>
+ <Kobalte.Trigger
+ ref={(el: HTMLElement) => setTriggerRef(el)}
+ as={local.triggerAs ?? "div"}
+ data-slot="popover-trigger"
+ {...(local.triggerProps as any)}
+ >
{local.trigger}
</Kobalte.Trigger>
<Show when={local.portal ?? true} fallback={content()}>