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>() function init() { const [active, setActive] = createSignal() const timer = { current: undefined as ReturnType | 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 ( { if (open) return close() }} > {element()} ) }), ) 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 ( {props.children}
{ctx.active?.node}
) } 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() }, } }