React'ta Yeniden Kullanılabilir Modal Bileşeni Oluşturma
Modal bileşenleri, modern web uygulamalarının vazgeçilmez bir parçasıdır. Bu yazıda, React ve TypeScript kullanarak yeniden kullanılabilir, erişilebilir ve animasyonlu bir modal bileşeni oluşturmayı öğreneceğiz.
Modal Bileşeninin Temel Yapısı
İlk olarak, modal bileşenimizin temel yapısını ve prop tiplerini oluşturalım.
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { motion, AnimatePresence } from 'framer-motion';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
size?: 'sm' | 'md' | 'lg';
closeOnOutsideClick?: boolean;
closeOnEsc?: boolean;
}
const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
size = 'md',
closeOnOutsideClick = true,
closeOnEsc = true,
}) => {
const modalRef = useRef<HTMLDivElement>(null);
// ESC tuşu ile kapatma
useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => {
if (closeOnEsc && event.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscKey);
document.body.style.overflow = 'hidden'; // Scroll'u engelle
}
return () => {
document.removeEventListener('keydown', handleEscKey);
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose, closeOnEsc]);
// Dışarı tıklama ile kapatma
const handleOutsideClick = (event: React.MouseEvent) => {
if (closeOnOutsideClick && modalRef.current && !modalRef.current.contains(event.target as Node)) {
onClose();
}
};
// Modal içeriği
const modalContent = (
<AnimatePresence>
{isOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50"
onClick={handleOutsideClick}
>
<motion.div
ref={modalRef}
className={`
bg-white dark:bg-gray-800 rounded-lg shadow-xl
${size === 'sm' ? 'max-w-sm' : size === 'lg' ? 'max-w-2xl' : 'max-w-lg'}
w-full
`}
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: 'spring', duration: 0.3 }}
>
{/* Modal Header */}
{title && (
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
{title}
</h2>
</div>
)}
{/* Modal Body */}
<div className="px-6 py-4">{children}</div>
{/* Modal Footer - İsteğe bağlı */}
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-end space-x-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
>
Kapat
</button>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
// Portal ile body'e render etme
return createPortal(modalContent, document.body);
};
export default Modal;
Modal Hook'u ile State Yönetimi
Modal'ın state yönetimini kolaylaştırmak için özel bir hook oluşturalım.
import { useState, useCallback } from 'react';
interface UseModalReturn {
isOpen: boolean;
open: () => void;
close: () => void;
toggle: () => void;
}
export function useModal(initialState = false): UseModalReturn {
const [isOpen, setIsOpen] = useState(initialState);
const open = useCallback(() => setIsOpen(true), []);
const close = useCallback(() => setIsOpen(false), []);
const toggle = useCallback(() => setIsOpen(prev => !prev), []);
return { isOpen, open, close, toggle };
}
Erişilebilirlik (A11y) Özellikleri
Modal'ımızı ARIA standartlarına uygun hale getirelim.
interface ModalProps {
// ... diğer prop'lar ...
ariaLabel?: string;
ariaDescribedby?: string;
}
const Modal: React.FC<ModalProps> = ({
// ... diğer prop'lar ...
ariaLabel,
ariaDescribedby,
}) => {
// ... önceki kodlar ...
const modalContent = (
<div
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
aria-describedby={ariaDescribedby}
className="modal-container"
>
{/* ... modal içeriği ... */}
</div>
);
// ... kalan kodlar ...
};
Özelleştirilebilir Animasyonlar
Framer Motion ile farklı animasyon varyantları ekleyelim.
const animationVariants = {
fade: {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
},
scale: {
initial: { opacity: 0, scale: 0.9 },
animate: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 0.9 },
},
slideUp: {
initial: { opacity: 0, y: 50 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 50 },
},
};
interface ModalProps {
// ... diğer prop'lar ...
animation?: keyof typeof animationVariants;
}
const Modal: React.FC<ModalProps> = ({
// ... diğer prop'lar ...
animation = 'scale',
}) => {
const selectedAnimation = animationVariants[animation];
return (
<motion.div
initial={selectedAnimation.initial}
animate={selectedAnimation.animate}
exit={selectedAnimation.exit}
transition={{ type: 'spring', duration: 0.3 }}
>
{/* ... modal içeriği ... */}
</motion.div>
);
};
Kullanım Örnekleri
Modal bileşenimizi farklı senaryolarda nasıl kullanabileceğimizi görelim.
1. Basit Kullanım
function App() {
const { isOpen, open, close } = useModal();
return (
<div>
<button onClick={open}>Modal'ı Aç</button>
<Modal isOpen={isOpen} onClose={close} title="Örnek Modal">
<p>Modal içeriği burada yer alır.</p>
</Modal>
</div>
);
}
2. Form Modal'ı
function UserFormModal() {
const { isOpen, close } = useModal();
const [formData, setFormData] = useState({ name: '', email: '' });
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await saveUser(formData);
close();
} catch (error) {
console.error('Form gönderimi başarısız:', error);
}
};
return (
<Modal
isOpen={isOpen}
onClose={close}
title="Kullanıcı Ekle"
size="lg"
closeOnOutsideClick={false}
>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Ad Soyad
</label>
<input
type="text"
value={formData.name}
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
E-posta
</label>
<input
type="email"
value={formData.email}
onChange={e => setFormData(prev => ({ ...prev, email: e.target.value }))}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={close}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md"
>
İptal
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md"
>
Kaydet
</button>
</div>
</form>
</Modal>
);
}
3. Onay Modal'ı
function ConfirmationModal({ isOpen, onClose, onConfirm, message }: {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
message: string;
}) {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="Onay"
size="sm"
animation="slideUp"
>
<div className="text-center">
<p className="text-gray-700 dark:text-gray-300">{message}</p>
<div className="mt-6 flex justify-center space-x-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md"
>
İptal
</button>
<button
onClick={() => {
onConfirm();
onClose();
}}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md"
>
Onayla
</button>
</div>
</div>
</Modal>
);
}
Best Practices ve İpuçları
Performans Optimizasyonu
- Modal içeriğini lazy loading ile yükleyin
- Gereksiz render'ları önlemek için React.memo kullanın
- Animasyonlar için CSS transforms kullanın
Erişilebilirlik
- Focus trap kullanın
- Uygun ARIA attribute'larını ekleyin
- Klavye navigasyonunu destekleyin
Responsive Tasarım
- Mobile-first yaklaşımı benimseyin
- Farklı ekran boyutları için uygun padding ve margin değerleri kullanın
- Touch cihazlar için uygun interaction'ları ekleyin
State Yönetimi
- Complex state için useReducer kullanın
- Form state'i için React Hook Form gibi kütüphaneler tercih edin
- Global state gerekiyorsa Context API veya state management kütüphanesi kullanın
Sonuç
Modern bir modal bileşeni oluştururken dikkat edilmesi gereken birçok nokta vardır:
- Erişilebilirlik standartlarına uygunluk
- Performans optimizasyonu
- Responsive tasarım
- Kullanıcı deneyimi
- Yeniden kullanılabilirlik
- Type safety
Bu yazıda oluşturduğumuz modal bileşeni, tüm bu gereksinimleri karşılayan, production-ready bir çözüm sunmaktadır.
İlgili Etiketler: #React #Components #TypeScript #Accessibility #Animation #UIUX #WebDevelopment