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:
Janpeter Visser 2026-05-04 07:14:07 +02:00
parent b47f62966e
commit b05c4d241b
6 changed files with 219 additions and 47 deletions

View file

@ -0,0 +1,57 @@
// @vitest-environment jsdom
import { describe, it, expect, vi } from 'vitest'
import { useDialogSubmitShortcut } from '@/components/shared/use-dialog-submit-shortcut'
function makeEvent(opts: Partial<KeyboardEvent>) {
return {
metaKey: false,
ctrlKey: false,
key: '',
preventDefault: vi.fn(),
...opts,
} as unknown as React.KeyboardEvent
}
describe('useDialogSubmitShortcut', () => {
it('triggert submit op Cmd+Enter', () => {
const submit = vi.fn()
const handler = useDialogSubmitShortcut(submit)
const e = makeEvent({ metaKey: true, key: 'Enter' })
handler(e)
expect(submit).toHaveBeenCalledTimes(1)
expect(e.preventDefault).toHaveBeenCalled()
})
it('triggert submit op Ctrl+Enter', () => {
const submit = vi.fn()
const handler = useDialogSubmitShortcut(submit)
const e = makeEvent({ ctrlKey: true, key: 'Enter' })
handler(e)
expect(submit).toHaveBeenCalledTimes(1)
})
it('triggert NIET op Enter zonder modifier', () => {
const submit = vi.fn()
const handler = useDialogSubmitShortcut(submit)
const e = makeEvent({ key: 'Enter' })
handler(e)
expect(submit).not.toHaveBeenCalled()
expect(e.preventDefault).not.toHaveBeenCalled()
})
it('triggert NIET op Cmd+andere toets', () => {
const submit = vi.fn()
const handler = useDialogSubmitShortcut(submit)
const e = makeEvent({ metaKey: true, key: 'a' })
handler(e)
expect(submit).not.toHaveBeenCalled()
})
})

View file

@ -0,0 +1,50 @@
// @vitest-environment jsdom
import { describe, it, expect, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useDirtyCloseGuard } from '@/components/shared/use-dirty-close-guard'
describe('useDirtyCloseGuard', () => {
it('sluit direct als form niet dirty is', () => {
const onClose = vi.fn()
const { result } = renderHook(() => useDirtyCloseGuard(false, onClose))
act(() => result.current.attemptClose())
expect(onClose).toHaveBeenCalledTimes(1)
expect(result.current.confirmOpen).toBe(false)
})
it('opent confirm als form dirty is', () => {
const onClose = vi.fn()
const { result } = renderHook(() => useDirtyCloseGuard(true, onClose))
act(() => result.current.attemptClose())
expect(onClose).not.toHaveBeenCalled()
expect(result.current.confirmOpen).toBe(true)
})
it('confirmDiscard sluit confirm en roept onClose', () => {
const onClose = vi.fn()
const { result } = renderHook(() => useDirtyCloseGuard(true, onClose))
act(() => result.current.attemptClose())
expect(result.current.confirmOpen).toBe(true)
act(() => result.current.confirmDiscard())
expect(onClose).toHaveBeenCalledTimes(1)
expect(result.current.confirmOpen).toBe(false)
})
it('setConfirmOpen(false) annuleert zonder onClose te roepen', () => {
const onClose = vi.fn()
const { result } = renderHook(() => useDirtyCloseGuard(true, onClose))
act(() => result.current.attemptClose())
act(() => result.current.setConfirmOpen(false))
expect(onClose).not.toHaveBeenCalled()
expect(result.current.confirmOpen).toBe(false)
})
})