164 lines
3.7 KiB
TypeScript
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()
|
|
},
|
|
}
|
|
}
|