feat(dialogs): gedeelde primitives — useDirtyCloseGuard, useDialogSubmitShortcut, layout-classes
Story 1 van PBI "Alle dialogen conform docs/patterns/dialog.md". - components/shared/use-dirty-close-guard.tsx — hook + paired AlertDialog - components/shared/use-dialog-submit-shortcut.ts — Cmd/Ctrl+Enter handler - components/shared/entity-dialog-layout.ts — MD3-conforme classes voor §4 - TaskDialog refactored om beide hooks + classes te gebruiken (geen gedragsverandering) - 8 nieuwe unit-tests Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b47f62966e
commit
b05c4d241b
6 changed files with 219 additions and 47 deletions
|
|
@ -28,6 +28,17 @@ import {
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import {
|
||||
useDirtyCloseGuard,
|
||||
DirtyCloseGuardDialog,
|
||||
} from '@/components/shared/use-dirty-close-guard'
|
||||
import { useDialogSubmitShortcut } from '@/components/shared/use-dialog-submit-shortcut'
|
||||
import {
|
||||
entityDialogBodyClasses,
|
||||
entityDialogContentClasses,
|
||||
entityDialogFooterClasses,
|
||||
entityDialogHeaderClasses,
|
||||
} from '@/components/shared/entity-dialog-layout'
|
||||
import { PrioritySegmented } from './priority-segmented'
|
||||
import { StatusSelect } from './status-select'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
|
@ -70,7 +81,6 @@ const textareaClass = cn(
|
|||
export function TaskDialog({ task, storyId, productId, closePath, isDemo = false }: TaskDialogProps) {
|
||||
const router = useRouter()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [confirmClose, setConfirmClose] = useState(false)
|
||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||
const isEdit = !!task
|
||||
|
||||
|
|
@ -90,20 +100,8 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
|
|||
router.push(closePath)
|
||||
}
|
||||
|
||||
function handleAttemptClose() {
|
||||
if (form.formState.isDirty) {
|
||||
setConfirmClose(true)
|
||||
} else {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
form.handleSubmit(onSubmit)()
|
||||
}
|
||||
}
|
||||
const closeGuard = useDirtyCloseGuard(form.formState.isDirty, handleClose)
|
||||
const handleKeyDown = useDialogSubmitShortcut(() => form.handleSubmit(onSubmit)())
|
||||
|
||||
function onSubmit(data: TaskInput) {
|
||||
startTransition(async () => {
|
||||
|
|
@ -167,19 +165,14 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dialog open onOpenChange={(open) => { if (!open) handleAttemptClose() }}>
|
||||
<Dialog open onOpenChange={(open) => { if (!open) closeGuard.attemptClose() }}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
'flex flex-col p-0 gap-0',
|
||||
'max-h-[90vh] w-full max-w-[calc(100%-2rem)]',
|
||||
'sm:max-w-[90vw] sm:max-h-[85vh]',
|
||||
'lg:max-w-[50vw] lg:min-w-[480px]',
|
||||
)}
|
||||
className={entityDialogContentClasses}
|
||||
>
|
||||
{/* Sticky header */}
|
||||
<div className="flex items-center justify-between px-6 pt-5 pb-4 border-b border-outline-variant shrink-0">
|
||||
<div className={entityDialogHeaderClasses}>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
{isEdit ? 'Taak bewerken' : 'Nieuwe taak'}
|
||||
</DialogTitle>
|
||||
|
|
@ -196,7 +189,7 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
|
|||
</div>
|
||||
|
||||
{/* Scrollable form body */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6">
|
||||
<div className={entityDialogBodyClasses}>
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">
|
||||
|
|
@ -323,7 +316,7 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
|
|||
</div>
|
||||
|
||||
{/* Sticky footer */}
|
||||
<div className="border-t border-outline-variant px-6 py-4 shrink-0">
|
||||
<div className={entityDialogFooterClasses}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{isEdit ? (
|
||||
<DemoTooltip show={isDemo}>
|
||||
|
|
@ -344,7 +337,7 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
|
|||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={handleAttemptClose}
|
||||
onClick={closeGuard.attemptClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
Annuleren
|
||||
|
|
@ -374,27 +367,7 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
|
|||
</Dialog>
|
||||
|
||||
{/* Dirty-check confirm */}
|
||||
<AlertDialog open={confirmClose} onOpenChange={setConfirmClose}>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Wijzigingen niet opgeslagen</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Wil je de wijzigingen weggooien?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setConfirmClose(false)}>
|
||||
Terug
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="destructive"
|
||||
onClick={() => { setConfirmClose(false); handleClose() }}
|
||||
>
|
||||
Weggooien
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DirtyCloseGuardDialog guard={closeGuard} />
|
||||
|
||||
{/* Delete confirm */}
|
||||
<AlertDialog open={confirmDelete} onOpenChange={setConfirmDelete}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue