このコミットが含まれているのは:
2026-07-15 22:51:01 +09:00
コミット d0914fce3b
6個のファイルの変更199行の追加140行の削除
+175 -120
ファイルの表示
@@ -7,7 +7,7 @@ import { Dialog,
DialogFooter,
DialogHeader,
DialogTitle } from '@/components/ui/dialog'
import { DialogueContext, useDialogue } from '@/lib/dialogues/useDialogue'
import { DialogueContext } from '@/lib/dialogues/useDialogue'
import type { FC, ReactNode } from 'react'
import type { AlertOptions,
@@ -42,20 +42,24 @@ type Props = { children: ReactNode }
const DialogueProvider: FC<Props> = ({ children }) => {
const [stack, setStack] = useState<DialogueRequest[]> ([])
const [queue, setQueue] = useState<DialogueRequest[]> ([])
const [pendingIds, setPendingIds] = useState<number[]> ([])
const [formActions, setFormActions] = useState<Record<number, DialogueFormAction[]>> ({ })
const formControls = useRef<Record<number, DialogueFormControls>> ({ })
const [nestedConfirm, setNestedConfirm] = useState<{
parentId: number
options: ConfirmOptions
resolve: (value: boolean) => void } | null> (null)
const push = useCallback ((request: Omit<DialogueRequest, 'id'>) => {
const id = nextDialogueId
++nextDialogueId
setStack (current => [...current, { ...request, id } as DialogueRequest])
setQueue (current => [...current, { ...request, id } as DialogueRequest])
}, [])
const closeRequest = useCallback ((id: number, result?: unknown) => {
setStack (current => {
setQueue (current => {
const active = current.find (request => request.id === id)
if (active == null)
@@ -96,6 +100,23 @@ const DialogueProvider: FC<Props> = ({ children }) => {
},
[])
const openNestedConfirm = useCallback (
(parentId: number, options: ConfirmOptions) =>
new Promise<boolean> (resolve => {
setNestedConfirm ({ parentId, options, resolve })
}),
[])
const closeNestedConfirm = useCallback ((result: boolean) => {
setNestedConfirm (current => {
if (current == null)
return current
current.resolve (result)
return null
})
}, [])
const api = useMemo<DialogueAPI> (() => ({
confirm: options => new Promise<boolean> (resolve => {
push ({ kind: 'confirm', options, resolve })
@@ -131,138 +152,172 @@ const DialogueProvider: FC<Props> = ({ children }) => {
},
[closeRequest, pendingIds])
const active = queue[0]
return (
<DialogueContext.Provider value={api}>
{children}
{stack.map ((request, index) => {
const isTop = index === stack.length - 1
const pending = pendingIds.includes (request.id)
const controls =
request.kind === 'form'
? (formControls.current[request.id] ??= {
close: () => closeRequest (request.id),
setActions: actions => setRequestActions (request.id, actions) })
: null
const requestActions = formActions[request.id] ?? []
return (
<Dialog
key={request.id}
open
onOpenChange={open => {
if (!(open) && isTop && !(pending))
closeRequest (request.id, request.kind !== 'confirm' && null)
{active && (
<Dialog
open
onOpenChange={open => {
const blocked =
nestedConfirm?.parentId === active.id
|| pendingIds.includes (active.id)
if (!(open) && !(blocked))
closeRequest (active.id, active.kind !== 'confirm' && null)
}}>
<DialogContent
className={
active.kind === 'form'
? `flex max-h-[calc(100dvh-1rem)] ${ active.options.size === 'large'
? 'max-w-3xl'
: 'max-w-lg' } flex-col overflow-hidden gap-0`
: 'px-6 pb-6 pt-7'
}
onEscapeKeyDown={event => {
if (nestedConfirm?.parentId === active.id || pendingIds.includes (active.id))
event.preventDefault ()
}}
onPointerDownOutside={event => {
if (nestedConfirm?.parentId === active.id || pendingIds.includes (active.id))
event.preventDefault ()
}}>
<DialogContent
className={
request.kind === 'form'
? `flex max-h-[calc(100dvh-1rem)] ${ request.options.size === 'large'
? 'max-w-3xl'
: 'max-w-lg' } flex-col overflow-hidden gap-0`
: 'px-6 pb-6 pt-7'
}
onEscapeKeyDown={event => {
if (pending)
event.preventDefault ()
}}
onPointerDownOutside={event => {
if (pending)
event.preventDefault ()
}}>
{request.kind === 'form'
? (
<>
<DialogHeader className="shrink-0 pl-8 pr-0 pt-1 text-left">
<DialogTitle>{request.options.title}</DialogTitle>
{active.kind === 'form'
? (
<>
<DialogHeader className="shrink-0 pl-8 pr-0 pt-1 text-left">
<DialogTitle>{active.options.title}</DialogTitle>
{request.options.description && (
<DialogDescription asChild>
<div>{request.options.description}</div>
</DialogDescription>)}
</DialogHeader>
{active.options.description && (
<DialogDescription asChild>
<div>{active.options.description}</div>
</DialogDescription>)}
</DialogHeader>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
{controls && request.options.body (controls)}
</div>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
{active.options.body (
formControls.current[active.id] ??= {
close: () => closeRequest (active.id),
setActions: actions => setRequestActions (active.id, actions),
confirm: options => openNestedConfirm (active.id, options) })}
</div>
<DialogFooter className="shrink-0 pt-4">
<Button
variant="outline"
onClick={() => closeRequest (request.id)}
disabled={pending}>
{request.options.cancelText ?? '取消'}
</Button>
<DialogFooter className="shrink-0 pt-4">
<Button
variant="outline"
onClick={() => closeRequest (active.id)}
disabled={pendingIds.includes (active.id)
|| nestedConfirm?.parentId === active.id}>
{active.options.cancelText ?? '取消'}
</Button>
{requestActions.map (action => (
{(formActions[active.id] ?? []).map (action => (
<Button
key={action.label}
variant={action.variant === 'danger'
? 'destructive'
: 'default'}
onClick={() => void handleFormAction (active.id, action)}
disabled={pendingIds.includes (active.id)
|| nestedConfirm?.parentId === active.id
|| action.disabled}>
{action.label}
</Button>))}
</DialogFooter>
</>)
: (
<>
<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
key={action.label}
variant={action.variant === 'danger'
? 'destructive'
: 'default'}
onClick={() => void handleFormAction (request.id, action)}
disabled={pending || action.disabled}>
{action.label}
</Button>))}
</DialogFooter>
</>)
: (
<>
<DialogHeader className="pl-8">
<DialogTitle>{request.options.title}</DialogTitle>
variant="outline"
onClick={() => closeRequest (active.id, false)}>
{active.options.cancelText ?? '取消'}
</Button>
{request.options.description && (
<DialogDescription asChild>
<div>{request.options.description}</div>
</DialogDescription>)}
</DialogHeader>
<Button
variant={(active.options.variant === 'danger')
? 'destructive'
: 'default'}
onClick={() => closeRequest (active.id, true)}>
{active.options.confirmText ?? '確定'}
</Button>
</>)}
<DialogFooter>
{request.kind === 'confirm' && (
<>
<Button
variant="outline"
onClick={() => closeRequest (request.id, false)}>
{request.options.cancelText ?? '取消'}
</Button>
{active.kind === 'alert' && (
<Button onClick={() => closeRequest (active.id)}>
{active.options.okText ?? '確定'}
</Button>)}
<Button
variant={(request.options.variant === 'danger')
? 'destructive'
: 'default'}
onClick={() => closeRequest (request.id, true)}>
{request.options.confirmText ?? '確定'}
</Button>
</>)}
{active.kind === 'choice' && (
<>
<Button
variant="outline"
onClick={() => closeRequest (active.id, null)}>
{active.options.cancelText ?? '取消'}
</Button>
{request.kind === 'alert' && (
<Button onClick={() => closeRequest (request.id)}>
{request.options.okText ?? '確定'}
</Button>)}
{active.options.choices.map (choice => (
<Button
key={choice.value}
variant={(choice.variant === 'danger')
? 'destructive'
: 'default'}
onClick={() => closeRequest (active.id, choice.value)}>
{choice.label}
</Button>))}
</>)}
</DialogFooter>
</>)}
</DialogContent>
</Dialog>)}
{request.kind === 'choice' && (
<>
<Button
variant="outline"
onClick={() => closeRequest (request.id, null)}>
{request.options.cancelText ?? '取消'}
</Button>
{nestedConfirm && (
<Dialog
open
onOpenChange={open => {
if (!(open))
closeNestedConfirm (false)
}}>
<DialogContent className="px-6 pb-6 pt-7">
<DialogHeader className="pl-8">
<DialogTitle>{nestedConfirm.options.title}</DialogTitle>
{request.options.choices.map (choice => (
<Button
key={choice.value}
variant={(choice.variant === 'danger')
? 'destructive'
: 'default'}
onClick={() => closeRequest (request.id, choice.value)}>
{choice.label}
</Button>))}
</>)}
</DialogFooter>
</>)}
</DialogContent>
</Dialog>)
})}
{nestedConfirm.options.description && (
<DialogDescription asChild>
<div>{nestedConfirm.options.description}</div>
</DialogDescription>)}
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => closeNestedConfirm (false)}>
{nestedConfirm.options.cancelText ?? '取消'}
</Button>
<Button
variant={nestedConfirm.options.variant === 'danger'
? 'destructive'
: 'default'}
onClick={() => closeNestedConfirm (true)}>
{nestedConfirm.options.confirmText ?? '確定'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>)}
</DialogueContext.Provider>)
}
export { useDialogue }