e03cc01109
#171 #171 #171 #171 #171 #171 #171 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #345
188 lines
4.6 KiB
TypeScript
188 lines
4.6 KiB
TypeScript
import { createContext, useCallback, useContext, useMemo, useState } from 'react'
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
import { Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle } from '@/components/ui/dialog'
|
|
|
|
import type { FC, ReactNode } from 'react'
|
|
|
|
type DialogueVariant = 'default' | 'danger'
|
|
|
|
type ConfirmOptions = { title: string
|
|
description?: ReactNode
|
|
confirmText?: string
|
|
cancelText?: string
|
|
variant?: DialogueVariant }
|
|
|
|
type AlertOptions = { title: string
|
|
description?: ReactNode
|
|
okText?: string }
|
|
|
|
type Choice<T extends string> = { value: T
|
|
label: string
|
|
variant?: DialogueVariant }
|
|
|
|
type ChoiceOptions<T extends string> = { title: string
|
|
description?: ReactNode
|
|
choices: Choice<T>[]
|
|
cancelText?: string }
|
|
|
|
type DialogueRequest =
|
|
| { id: number
|
|
kind: 'confirm'
|
|
options: ConfirmOptions
|
|
resolve: (value: boolean) => void }
|
|
| { id: number
|
|
kind: 'alert'
|
|
options: AlertOptions
|
|
resolve: () => void }
|
|
| { id: number
|
|
kind: 'choice'
|
|
options: ChoiceOptions<string>
|
|
resolve: (value: string | null) => void }
|
|
|
|
type DialogueAPI =
|
|
{ confirm: (options: ConfirmOptions) => Promise<boolean>
|
|
alert: (options: AlertOptions) => Promise<void>
|
|
choice: <T extends string> (options: ChoiceOptions<T>) => Promise<T | null> }
|
|
|
|
const DialogueContext = createContext<DialogueAPI | null> (null)
|
|
|
|
let nextDialogueId = 1
|
|
|
|
type Props = { children: ReactNode }
|
|
|
|
|
|
export default (({ children }: Props) => {
|
|
const [queue, setQueue] = useState<DialogueRequest[]> ([])
|
|
|
|
const push = useCallback ((request: Omit<DialogueRequest, 'id'>) => {
|
|
const id = nextDialogueId
|
|
++nextDialogueId
|
|
|
|
setQueue (q => [...q, { ...request, id } as DialogueRequest])
|
|
}, [])
|
|
|
|
const closeActive = useCallback ((result?: unknown) => {
|
|
setQueue (q => {
|
|
const [active, ...rest] = q
|
|
|
|
if (!(active))
|
|
return rest
|
|
|
|
switch (active.kind)
|
|
{
|
|
case 'confirm':
|
|
active.resolve (Boolean (result))
|
|
break
|
|
|
|
case 'alert':
|
|
active.resolve ()
|
|
break
|
|
|
|
case 'choice':
|
|
active.resolve ((result ?? null) as string | null)
|
|
break
|
|
}
|
|
|
|
return rest
|
|
})
|
|
}, [])
|
|
|
|
const api = useMemo<DialogueAPI> (() => ({
|
|
confirm: options => new Promise<boolean> (resolve => {
|
|
push ({ kind: 'confirm', options, resolve })
|
|
}),
|
|
alert: options => new Promise<void> (resolve => {
|
|
push ({ kind: 'alert', options, resolve })
|
|
}),
|
|
choice: options => new Promise (resolve => {
|
|
push ({ kind: 'choice',
|
|
options: options as ChoiceOptions<string>,
|
|
resolve: resolve as (value: string | null) => void })
|
|
}) }), [push])
|
|
|
|
const active = queue[0]
|
|
|
|
return (
|
|
<DialogueContext.Provider value={api}>
|
|
{children}
|
|
|
|
<Dialog
|
|
open={Boolean (active)}
|
|
onOpenChange={open => {
|
|
if (!(open))
|
|
closeActive (active?.kind !== 'confirm' && null)
|
|
}}>
|
|
{active && (
|
|
<DialogContent className="px-6 pb-6 pt-7">
|
|
<DialogHeader className="pl-8">
|
|
<DialogTitle>{active.options.title}</DialogTitle>
|
|
|
|
{active.options.description && (
|
|
<DialogDescription asChild>
|
|
<div>{active.options.description}</div>
|
|
</DialogDescription>)}
|
|
</DialogHeader>
|
|
|
|
<DialogFooter>
|
|
{active.kind === 'confirm' && (
|
|
<>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => closeActive (false)}>
|
|
{active.options.cancelText ?? '取消'}
|
|
</Button>
|
|
|
|
<Button
|
|
variant={(active.options.variant === 'danger')
|
|
? 'destructive'
|
|
: 'default'}
|
|
onClick={() => closeActive (true)}>
|
|
{active.options.confirmText ?? '確定'}
|
|
</Button>
|
|
</>)}
|
|
|
|
{active.kind === 'alert' && (
|
|
<Button onClick={() => closeActive ()}>
|
|
{active.options.okText ?? '確定'}
|
|
</Button>)}
|
|
|
|
{active.kind === 'choice' && (
|
|
<>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => closeActive (null)}>
|
|
{active.options.cancelText ?? '取消'}
|
|
</Button>
|
|
|
|
{active.options.choices.map (choice => (
|
|
<Button
|
|
key={choice.value}
|
|
variant={(choice.variant === 'danger')
|
|
? 'destructive'
|
|
: 'default'}
|
|
onClick={() => closeActive (choice.value)}>
|
|
{choice.label}
|
|
</Button>))}
|
|
</>)}
|
|
</DialogFooter>
|
|
</DialogContent>)}
|
|
</Dialog>
|
|
</DialogueContext.Provider>)
|
|
}) satisfies FC<Props>
|
|
|
|
|
|
export const useDialogue = () => {
|
|
const dialogue = useContext (DialogueContext)
|
|
|
|
if (!(dialogue))
|
|
throw new Error ('useDialogue must be used inside DialogueProvider')
|
|
|
|
return dialogue
|
|
}
|