Files
shopify-ai-backup/opencode/packages/ui/src/context/dialog.tsx
2026-02-07 20:54:46 +00:00

164 lines
3.7 KiB
TypeScript

import {
createContext,
createEffect,
createRoot,
createSignal,
getOwner,
onCleanup,
type Owner,
type ParentProps,
runWithOwner,
useContext,
type JSX,
} from "solid-js"
import { Dialog as Kobalte } from "@kobalte/core/dialog"
type DialogElement = () => JSX.Element
type Active = {
id: string
node: JSX.Element
dispose: () => void
owner: Owner
onClose?: () => void
setClosing: (closing: boolean) => void
}
const Context = createContext<ReturnType<typeof init>>()
function init() {
const [active, setActive] = createSignal<Active | undefined>()
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
const lock = { value: false }
onCleanup(() => {
if (timer.current === undefined) return
clearTimeout(timer.current)
timer.current = undefined
})
const close = () => {
const current = active()
if (!current || lock.value) return
lock.value = true
current.onClose?.()
current.setClosing(true)
const id = current.id
if (timer.current !== undefined) {
clearTimeout(timer.current)
timer.current = undefined
}
timer.current = setTimeout(() => {
timer.current = undefined
current.dispose()
if (active()?.id === id) setActive(undefined)
lock.value = false
}, 100)
}
createEffect(() => {
if (!active()) return
const onKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Escape") return
close()
event.preventDefault()
event.stopPropagation()
}
window.addEventListener("keydown", onKeyDown, true)
onCleanup(() => window.removeEventListener("keydown", onKeyDown, true))
})
const show = (element: DialogElement, owner: Owner, onClose?: () => void) => {
// Immediately dispose any existing dialog when showing a new one
const current = active()
if (current) {
current.dispose()
setActive(undefined)
}
if (timer.current !== undefined) {
clearTimeout(timer.current)
timer.current = undefined
}
lock.value = false
const id = Math.random().toString(36).slice(2)
let dispose: (() => void) | undefined
let setClosing: ((closing: boolean) => void) | undefined
const node = runWithOwner(owner, () =>
createRoot((d: () => void) => {
dispose = d
const [closing, setClosingSignal] = createSignal(false)
setClosing = setClosingSignal
return (
<Kobalte
modal
open={!closing()}
onOpenChange={(open: boolean) => {
if (open) return
close()
}}
>
<Kobalte.Portal>
<Kobalte.Overlay data-component="dialog-overlay" onClick={close} />
{element()}
</Kobalte.Portal>
</Kobalte>
)
}),
)
if (!dispose || !setClosing) return
setActive({ id, node, dispose, owner, onClose, setClosing })
}
return {
get active() {
return active()
},
close,
show,
}
}
export function DialogProvider(props: ParentProps) {
const ctx = init()
return (
<Context.Provider value={ctx}>
{props.children}
<div data-component="dialog-stack">{ctx.active?.node}</div>
</Context.Provider>
)
}
export function useDialog() {
const ctx = useContext(Context)
const owner = getOwner()
if (!owner) {
throw new Error("useDialog must be used within a DialogProvider")
}
if (!ctx) {
throw new Error("useDialog must be used within a DialogProvider")
}
return {
get active() {
return ctx.active
},
show(element: DialogElement, onClose?: () => void) {
const base = ctx.active?.owner ?? owner
ctx.show(element, base, onClose)
},
close() {
ctx.close()
},
}
}