feat(notes): add note deletion with confirmation

This commit is contained in:
devcodetools committed 2026-09-13 15:32:33 +02:00
1 parent 0976c51cc1
commit 0af0cdc405
4 files changed
+142 -4

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import DeleteModal from './DeleteModal'
describe('DeleteModal', () => {
it('renders nothing when closed', () => {
render(<DeleteModal open={false} title="Courses" onConfirm={vi.fn()} onCancel={vi.fn()} />)
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
})
it('renders the dialog with the note title when open', () => {
render(<DeleteModal open title="Courses" onConfirm={vi.fn()} onCancel={vi.fn()} />)
expect(screen.getByRole('dialog', { name: 'Supprimer la note ?' })).toBeInTheDocument()
expect(screen.getByText(/Courses/)).toBeInTheDocument()
})
it('shows a placeholder when the note has no title', () => {
render(<DeleteModal open title="" onConfirm={vi.fn()} onCancel={vi.fn()} />)
expect(screen.getByText(/Sans titre/)).toBeInTheDocument()
})
it('calls onConfirm when the confirm button is clicked', () => {
const onConfirm = vi.fn()
render(<DeleteModal open title="Courses" onConfirm={onConfirm} onCancel={vi.fn()} />)
fireEvent.click(screen.getByRole('button', { name: 'Supprimer' }))
expect(onConfirm).toHaveBeenCalled()
})
it('calls onCancel when the cancel button is clicked', () => {
const onCancel = vi.fn()
render(<DeleteModal open title="Courses" onConfirm={vi.fn()} onCancel={onCancel} />)
fireEvent.click(screen.getByRole('button', { name: 'Annuler' }))
expect(onCancel).toHaveBeenCalled()
})
})
+44
View File
@@ -0,0 +1,44 @@
interface DeleteModalProps {
open: boolean
title: string
onConfirm: () => void
onCancel: () => void
}
function DeleteModal({ open, title, onConfirm, onCancel }: DeleteModalProps) {
if (!open) return null
return (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50">
<div
role="dialog"
aria-modal="true"
aria-labelledby="delete-confirm-title"
className="w-full max-w-sm bg-white rounded-lg p-5 flex flex-col gap-4"
>
<h2 id="delete-confirm-title" className="text-lg font-semibold">
Supprimer la note ?
</h2>
<p className="text-gray-600">{title || 'Sans titre'} sera définitivement supprimée.</p>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 rounded border border-gray-300 text-gray-600"
>
Annuler
</button>
<button
type="button"
onClick={onConfirm}
className="px-4 py-2 rounded bg-red-600 text-white"
>
Supprimer
</button>
</div>
</div>
</div>
)
}
export default DeleteModal
+32 -2
View File
@@ -7,8 +7,9 @@ const note: Note = { id: 42, title: 'Courses', content: 'Lait\nOeufs' }
const renderEditor = (overrides: Partial<Note> = {}) => {
const onUpdate = vi.fn()
render(<NoteEditor note={{ ...note, ...overrides }} onUpdate={onUpdate} />)
return { onUpdate }
const onDelete = vi.fn()
render(<NoteEditor note={{ ...note, ...overrides }} onUpdate={onUpdate} onDelete={onDelete} />)
return { onUpdate, onDelete }
}
describe('NoteEditor', () => {
@@ -70,4 +71,33 @@ describe('NoteEditor', () => {
expect(onUpdate).not.toHaveBeenCalled()
expect(screen.getByText('Courses')).toBeInTheDocument()
})
it('opens the delete confirmation when the × button is clicked', () => {
renderEditor()
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Supprimer la note' }))
expect(screen.getByRole('dialog', { name: 'Supprimer la note ?' })).toBeInTheDocument()
})
it('deletes the note after confirmation', () => {
const { onDelete } = renderEditor()
fireEvent.click(screen.getByRole('button', { name: 'Supprimer la note' }))
fireEvent.click(screen.getByRole('button', { name: 'Supprimer' }))
expect(onDelete).toHaveBeenCalledWith(42)
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
})
it('does not delete when the confirmation is cancelled', () => {
const { onDelete } = renderEditor()
fireEvent.click(screen.getByRole('button', { name: 'Supprimer la note' }))
fireEvent.click(screen.getByRole('button', { name: 'Annuler' }))
expect(onDelete).not.toHaveBeenCalled()
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
})
})
+24 -2
View File
@@ -1,13 +1,16 @@
import { useState } from 'react'
import type { Note } from '../types'
import DeleteModal from './DeleteModal'
interface NoteEditorProps {
note: Note
onUpdate: (id: number, patch: Partial<Pick<Note, 'title' | 'content'>>) => void
onDelete: (id: number) => void
}
function NoteEditor({ note, onUpdate }: NoteEditorProps) {
function NoteEditor({ note, onUpdate, onDelete }: NoteEditorProps) {
const [editing, setEditing] = useState(false)
const [confirmDelete, setConfirmDelete] = useState(false)
const [title, setTitle] = useState(note.title)
const [content, setContent] = useState(note.content)
@@ -71,7 +74,17 @@ function NoteEditor({ note, onUpdate }: NoteEditorProps) {
return (
<article className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">{note.title || 'Sans titre'}</h2>
<div className="flex items-start justify-between gap-4">
<h2 className="text-2xl font-semibold">{note.title || 'Sans titre'}</h2>
<button
type="button"
onClick={() => setConfirmDelete(true)}
aria-label="Supprimer la note"
className="px-2 py-1 rounded text-gray-600 hover:text-red-600"
>
×
</button>
</div>
<p className="whitespace-pre-wrap">
{note.content || <span className="text-gray-400 italic">Note vide</span>}
</p>
@@ -84,6 +97,15 @@ function NoteEditor({ note, onUpdate }: NoteEditorProps) {
Éditer
</button>
</div>
<DeleteModal
open={confirmDelete}
title={note.title}
onConfirm={() => {
setConfirmDelete(false)
onDelete(note.id)
}}
onCancel={() => setConfirmDelete(false)}
/>
</article>
)
}