הטמעת חלונות מודאליים/פופ-אפ באתר
במהלך פיתוח פורטל ארגוני ב-React, נתקלנו בבעיה: לאחר פתיחת חלון אישור למחיקת רשומה, גלילת הרקע המשיכה לפעול, והפוקוס עבר לכפתור בתחתית העמוד. זה הוביל לשגיאות משתמש — הם מחקו נתונים בטעות מבלי לראות את האישור. ההמרה על פעולת היעד ירדה ב-12% בשבוע הראשון. נאלצנו לשכתב את כל המערכת מאפס באמצעות <dialog> מקורי ומלכודת פוקוס מותאמת אישית. כעת אנו מטמיעים מודאלים במפתח מלא, ופותרים בעיות אלו ברמת הארכיטקטורה. ניסיון מלמעלה מ-50 פרויקטים מראה שהטמעת מודאל נכונה חוסכת עד 40% מזמן ניפוי ה-UI ומפחיתה תקלות ב-25%. החיסכון הממוצע בתקציב התמיכה הוא כ-30%.
למה <dialog> מקורי עדיף על פתרונות מותאמים אישית?
מפתחים רבים יוצרים מודאלים עם divs מאפס, ושוכחים נגישות וניהול פוקוס. <dialog> מקורי עם <dialog> חוסם אוטומטית את הרקע, לוכד פוקוס, ותומך בסגירה עם Escape. הניסיון שלנו מראה שגישה זו מקצרת את זמן הפיתוח בחצי בהשוואה לפתרונות מותאמים אישית. יתר על כן, דיאלוג מקורי מבטיח התנהגות נכונה עם קוראי מסך ואינו דורש ספריות נוספות.
| פרמטר | <div> מקורי |
useFocusTrap מותאם אישית |
|---|---|---|
| מלכודת פוקוס | מובנית ב-showModal() | דורשת הטמעה ידנית |
| נעילת גלילה | אוטומטית דרך ::backdrop | דורשת <dialog> על body |
| נגישות | תמיכת ARIA, role=dialog | יש להוסיף תפקידים ומאפיינים |
| מודאלים מקוננים | המחסנית מנוהלת באופן מקורי | נדרש מנהל מחסנית משל עצמך |
| אנימציה | דרך CSS animation על אלמנטים | זהה |
איך ליישם מלכודת פוקוס ללא דיאלוג מקורי?
אם פרויקט דורש פתרון מותאם אישית (למשל, בשל אילוצי מערכת עיצוב), מלכודת הפוקוס מיושמת ידנית. אנו אוספים את כל האלמנטים הניתנים לפוקוס בתוך המודאל ומיירטים Tab/Shift+Tab. בעת פתיחה, אנו שומרים את האלמנט הפעיל הקודם כדי להחזיר את הפוקוס בעת סגירה. ב-React, זה עטוף בנוחות ב-hook של import { useEffect, useRef, ReactNode } from 'react' import { createPortal } from 'react-dom' interface ModalProps { isOpen: boolean onClose: () => void title?: string children: ReactNode size?: 'sm' | 'md' | 'lg' | 'xl' | 'full' closeOnBackdrop?: boolean } export function Modal({ isOpen, onClose, title, children, size = 'md', closeOnBackdrop = true, }: ModalProps) { const dialogRef = useRef<HTMLDialogElement>(null) const previousFocusRef = useRef<HTMLElement | null>(null) useEffect(() => { const dialog = dialogRef.current if (!dialog) return if (isOpen) { previousFocusRef.current = document.activeElement as HTMLElement dialog.showModal() document.body.style.overflow = 'hidden' } else { dialog.close() document.body.style.overflow = '' previousFocusRef.current?.focus() } }, [isOpen]) useEffect(() => { const dialog = dialogRef.current const handleClose = () => onClose() dialog?.addEventListener('close', handleClose) return () => dialog?.removeEventListener('close', handleClose) }, [onClose]) function handleBackdropClick(e: React.MouseEvent<HTMLDialogElement>) { if (!closeOnBackdrop) return const rect = dialogRef.current!.getBoundingClientRect() if ( e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom ) { onClose() } } return createPortal( <dialog ref={dialogRef} className={`modal modal--${size}`} onClick={handleBackdropClick} aria-labelledby={title ? 'modal-title' : undefined} > <div className="modal__content" onClick={e => e.stopPropagation()}> {title && ( <div className="modal__header"> <h2 id="modal-title" className="modal__title">{title}</h2> <button className="modal__close" onClick={onClose} aria-label="Закрыть"> <svg viewBox="0 0 24 24" width="20" height="20"> <path d="M6 6l12 12M18 6l-12 12" stroke="currentColor" strokeWidth="2"/> </svg> </button> </div> )} <div className="modal__body">{children}</div> </div> </dialog>, document.body ) } .
הטמעת מודאל ב-React: שלב אחר שלב
- צור קומפוננטת Modal שמקבלת
overflow: hidden,<body>,<dialog>,previousFocusRefופרופס נוספים. - השתמש ב-
<dialog>כדי לרנדר את המודאל ב-body. - בתוך
dvh, פתח וסגור את<dialog>המקורי דרךshowModal()/close(). - נהל את
overflow: hiddenעל body כשהמודאל פתוח. - יישם מלכודת פוקוס: שמור את האלמנט הפעיל הקודם והחזר את הפוקוס בעת סגירה.
- הוסף טיפול בלחיצה על הרקע לסגירה.
- החל אנימציות CSS דרך keyframes.
דוגמת קומפוננטה:
import { useEffect, useRef, ReactNode } from 'react'
import { createPortal } from 'react-dom'
interface ModalProps {
isOpen: boolean
onClose: () => void
title?: string
children: ReactNode
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
closeOnBackdrop?: boolean
}
export function Modal({
isOpen,
onClose,
title,
children,
size = 'md',
closeOnBackdrop = true,
}: ModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement
dialog.showModal()
document.body.style.overflow = 'hidden'
} else {
dialog.close()
document.body.style.overflow = ''
previousFocusRef.current?.focus()
}
}, [isOpen])
useEffect(() => {
const dialog = dialogRef.current
const handleClose = () => onClose()
dialog?.addEventListener('close', handleClose)
return () => dialog?.removeEventListener('close', handleClose)
}, [onClose])
function handleBackdropClick(e: React.MouseEvent<HTMLDialogElement>) {
if (!closeOnBackdrop) return
const rect = dialogRef.current!.getBoundingClientRect()
if (
e.clientX < rect.left ||
e.clientX > rect.right ||
e.clientY < rect.top ||
e.clientY > rect.bottom
) {
onClose()
}
}
return createPortal(
<dialog
ref={dialogRef}
className={`modal modal--${size}`}
onClick={handleBackdropClick}
aria-labelledby={title ? 'modal-title' : undefined}
>
<div className="modal__content" onClick={e => e.stopPropagation()}>
{title && (
<div className="modal__header">
<h2 id="modal-title" className="modal__title">{title}</h2>
<button className="modal__close" onClick={onClose} aria-label="Закрыть">
<svg viewBox="0 0 24 24" width="20" height="20">
<path d="M6 6l12 12M18 6l-12 12" stroke="currentColor" strokeWidth="2"/>
</svg>
</button>
</div>
)}
<div className="modal__body">{children}</div>
</div>
</dialog>,
document.body
)
} טעויות נפוצות בהטמעת מודאלים
- התעלמות מנעילת גלילה — הרקע גולל מתחת למודאל הפתוח. פתרון: הגדר
overflow: hiddenעל<body>בעת פתיחה והסר בעת סגירה. בשימוש ב-<dialog>מקורי, זה לא קורה אוטומטית, אז הוסף זאת ידנית. - אובדן פוקוס — לאחר סגירת המודאל, הפוקוס לא חוזר לאלמנט שהפעיל אותו. בקומפוננטת React שלמעלה,
previousFocusRefנשמר כדי לתקן זאת. - התנהגות לא נכונה במובייל —
<dialog>סטנדרטי לא נראה כמו bottomsheet. השתמש ב-media queries כדי לגרום למודאל להחליק מלמטה במובייל.
התאמה למובייל: Bottomsheet
במכשירים ניידים, bottomsheets נפוצים — המודאל מחליק מלמטה. ב-CSS, זה נעשה עם media query: במסכים ברוחב עד 768px, שנה את האנימציה ל-translateY ועגל רק את הפינות העליונות. כמו כן, התחשב בהיסט של שורת הכתובת של הדפדפן באמצעות dvh לגובה.
מה כלול בהטמעת מודאל?
- תיעוד API לקומפוננטה
- קוד מקור ב-TypeScript/React/Vue
- בדיקות על דסקטופ ומובייל (iOS, Android)
- שילוב אנליטיקה (טריגרים לתצוגה)
- תמיכה לאחר פריסה (חודש)
הערכות זמן
| שלב | זמן |
|---|---|
הטמעה בסיסית (<dialog> מקורי) |
3–4 שעות |
| קומפוננטת React עם פורטל ומלכודת פוקוס | יום אחד |
| מערכת עם מחסנית מודאלים ו-bottomsheet | 1.5–2 ימים |
| התאמה מלאה ל-WCAG | +0.5 יום |
יש לנו ניסיון של למעלה מ-7 שנים בפיתוח ממשקים ויותר מ-50 פרויקטים עם חלונות מודאליים. אנו מבטיחים תאימות בין דפדפנים ונגישות. הזמינו ייעוץ על הטמעת מודאלים לפרויקט שלכם — נמצא את הפתרון האופטימלי. צרו קשר כדי לדון בפרטים.







