Files
HartOMat/frontend/src/components/ConfirmModal.tsx
T
Hartmut 915abe9d74 fix(ux): replace confirm() with ConfirmModal, fix dark-mode colors, add currency format
- Add reusable ConfirmModal component (themed, Escape key, focus trap)
- Replace all native confirm() calls in Orders, ProductLibrary, Materials, Admin, Billing
- Fix ValidationDialog (Upload.tsx) hardcoded bg-white/text-gray-* → semantic tokens
- Fix NewInvoiceModal (Billing.tsx) hardcoded colors → semantic tokens
- Add formatCurrency (Intl.NumberFormat de-DE/EUR) to NewProductOrder wizard
- Resolves audit issues C1, C3, M3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:59:13 +01:00

119 lines
3.5 KiB
TypeScript

import { useEffect, useRef } from 'react'
import { AlertTriangle } from 'lucide-react'
interface ConfirmModalProps {
open: boolean
title: string
message: string
confirmLabel?: string
confirmVariant?: 'danger' | 'primary'
onConfirm: () => void
onCancel: () => void
}
export default function ConfirmModal({
open,
title,
message,
confirmLabel = 'Delete',
confirmVariant = 'danger',
onConfirm,
onCancel,
}: ConfirmModalProps) {
const cancelRef = useRef<HTMLButtonElement>(null)
const dialogRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!open) return
// Focus cancel button when modal opens
cancelRef.current?.focus()
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onCancel()
return
}
// Trap focus inside the modal
if (e.key === 'Tab' && dialogRef.current) {
const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault()
last?.focus()
}
} else {
if (document.activeElement === last) {
e.preventDefault()
first?.focus()
}
}
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [open, onCancel])
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="confirm-modal-title"
className="rounded-xl shadow-2xl w-full max-w-sm mx-4 border"
style={{
backgroundColor: 'var(--color-bg-surface)',
borderColor: 'var(--color-border-default)',
}}
>
<div className="px-6 py-5">
<div className="flex items-start gap-3 mb-4">
<div className="shrink-0 w-9 h-9 rounded-full bg-red-100 flex items-center justify-center">
<AlertTriangle size={18} className="text-red-600" />
</div>
<div>
<h2
id="confirm-modal-title"
className="text-base font-semibold text-content"
>
{title}
</h2>
<p className="text-sm text-content mt-1" style={{ opacity: 0.7 }}>
{message}
</p>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
ref={cancelRef}
onClick={onCancel}
className="px-4 py-2 text-sm rounded-lg border transition-colors text-content-secondary hover:bg-surface-hover"
style={{ borderColor: 'var(--color-border-default)' }}
>
Cancel
</button>
<button
onClick={onConfirm}
className={`px-4 py-2 text-sm rounded-lg font-medium transition-colors ${
confirmVariant === 'danger'
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-accent hover:bg-accent text-white'
}`}
>
{confirmLabel}
</button>
</div>
</div>
</div>
</div>
)
}