# رفع افتادن مودال‌ها به پایین صفحه (portal برای همه مودال‌ها) ## پروژه `clinicpro` (پنل ادمین React — `assets/admin/`) ## زمینه بعضی مودال‌ها هنگام باز شدن وسط صفحه نمی‌افتند و «خیلی پایین» یا آفست ظاهر می‌شوند. نمونه‌اش مودال کراپ عکس در صفحه‌ی دکتر بود که با انتقال به `createPortal(document.body)` حل شد. ## مشکل / هدف کلاس مشترک `.overlay` از `position: fixed; inset: 0` استفاده می‌کند (تعریف در `assets/admin/styles.css:613`). طبق مشخصات CSS، وقتی یک عنصر `position: fixed` داخل ancestorـی رندر شود که یکی از این خواص را دارد، دیگر نسبت به viewport نیست بلکه نسبت به همان ancestor محاسبه می‌شود: - `transform` (حتی `transform: translateZ(0)` یا انیمیشن‌های `pop`/`slidein` که transform دارند) - `filter` / `backdrop-filter` - `perspective`, `will-change: transform`, `contain: paint/layout` در نتیجه هر مودالی که `.overlay` را **inline در عمق درخت کامپوننت** (داخل کارت‌ها/کانتینرهای transform‌دار) رندر کند، پایین/آفست می‌افتد. **راه‌حل استاندارد پروژه:** رندر مودال از طریق `createPortal(..., document.body)` تا از زنجیره‌ی ancestorها خارج شود. کامپوننت‌های `components/ui/Modal.tsx` و `components/ImageCropModal.tsx` همین کار را می‌کنند و درست‌اند — الگوی مرجع همین‌هاست. هدف: **همه‌ی مودال‌های `.overlay` که هنوز portal ندارند** به portal منتقل شوند تا این مشکل در «هر جایی که مودال باز می‌شود» حل شود. ## فایل‌های مرتبط | فایل | وضعیت | نقش | |------|------|-----| | `assets/admin/components/ui/Modal.tsx` | ✅ portal دارد | الگوی مرجع — تغییر نده | | `assets/admin/components/ImageCropModal.tsx` | ✅ portal دارد | الگوی مرجع — تغییر نده | | `assets/admin/components/ui/ConfirmDialog.tsx` | ❌ بدون portal | پرکاربردترین؛ اولویت اول | | `assets/admin/components/ui/InviteDoctorModal.tsx` | ❌ بدون portal | مودال دعوت دکتر | | `assets/admin/components/layout/Topbar.tsx` | ❌ بدون portal | overlay (خط ~75) | | `assets/admin/pages/UsersPage.tsx` | ❌ بدون portal | مودال inline (خط ~104) | | `assets/admin/pages/ClinicsPage.tsx` | ❌ بدون portal | مودال افزودن (خط ~247) | | `assets/admin/pages/PreRegistrationsPage.tsx` | ❌ بدون portal | مودال رد کردن (خط ~207) | | `assets/admin/pages/ClinicDetailPage.tsx` | ❌ یک مورد بدون portal (خط ~354) | بقیه‌ی overlayهایش portal دارند | | `assets/admin/styles.css` | مرجع | تعریف `.overlay` (خط 613) — تغییر نده | ## وضعیت فعلی نمونه‌ی `ConfirmDialog.tsx` (بدون portal — همین الگو در بقیه هم هست): ```tsx import React from 'react'; import { XMarkIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'; export default function ConfirmDialog({ open, /* ... */ onConfirm, onCancel }: Props) { if (!open) return null; return (
e.stopPropagation()}> {/* ... */}
); } ``` `.overlay` (styles.css:613) — درست است، دست نزن: ```css .overlay { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 20px; background: rgba(8,13,22,.5); backdrop-filter: blur(4px); ... } ``` ## وظایف ### ۱. ساخت یک کامپوننت کمکی `Portal` (یک‌بار، برای جلوگیری از تکرار) فایل جدید: `assets/admin/components/ui/Portal.tsx` ```tsx import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; export default function Portal({ children }: { children: React.ReactNode }) { const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); if (!mounted) return null; return createPortal(children, document.body); } ``` > نکته: چون این SPA فقط client-side رندر می‌شود، `createPortal` مستقیم هم کار می‌کند؛ ولی داشتن wrapper واحد الگو را در همه‌جا یکدست و آینده‌محور می‌کند. اگر ترجیح می‌دهی بدون کامپوننت جدید پیش بروی، می‌توانی در هر فایل مستقیم `createPortal(..., document.body)` بزنی (مثل `ImageCropModal.tsx`). یکی از این دو روش را انتخاب و در همه‌ی موارد یکسان اعمال کن. ### ۲. انتقال `ConfirmDialog.tsx` به portal ```tsx import Portal from './Portal'; // ... if (!open) return null; return (
{/* بدون تغییر */}
); ``` ### ۳. همین کار برای بقیه‌ی مودال‌های `.overlay` بدون portal هر جای زیر که `.overlay` مستقیم رندر می‌شود را داخل `...` بپیچ (مسیر import را نسبت به محل فایل تنظیم کن: از `pages/` → `../components/ui/Portal`؛ از `components/layout/` → `../ui/Portal`؛ از `components/ui/` → `./Portal`): - `components/ui/InviteDoctorModal.tsx` (خط ~42) - `components/layout/Topbar.tsx` (خط ~75) — اگر این overlay فقط بک‌دراپ سایدبار موبایل است و مشکل جانمایی ندارد، فقط در صورت نیاز؛ ولی برای یکدستی بهتر است portal شود. - `pages/UsersPage.tsx` (خط ~104) - `pages/ClinicsPage.tsx` (خط ~247) - `pages/PreRegistrationsPage.tsx` (خط ~207) - `pages/ClinicDetailPage.tsx` (خط ~354) — این تنها overlay بدون portal این فایل است؛ بقیه (خطوط ~150، ~1177، ~1305، ~1315) از قبل `createPortal` دارند، آن‌ها را دست نزن. ### ۴. بررسی نبود مورد جاافتاده بعد از اعمال، این جستجو را بزن و مطمئن شو هر `className="overlay"` یا `className='overlay'` یا داخل `createPortal` است یا داخل ``: ```bash grep -rn "className=[\"']overlay" assets/admin --include="*.tsx" ``` هر موردی که هیچ‌کدام نبود را هم portal کن. ## نکات مهم - کلاس `.overlay` و `.modal` در `styles.css` درست‌اند (`place-items: center`)؛ **CSS را تغییر نده** — مشکل صرفاً از context جانمایی `fixed` به‌خاطر ancestor transform‌دار است، نه از خود overlay. - محتوای داخل `.overlay` (شامل `onClick={onCancel}` بک‌دراپ و `stopPropagation` روی `.modal`) نباید تغییر کند؛ فقط دور کل بلوک `.overlay` یک `` اضافه می‌شود. - `ui/Modal.tsx` و `ImageCropModal.tsx` را تغییر نده (از قبل درست‌اند). - تغییر فقط frontend است؛ هیچ endpoint/API عوض نمی‌شود → نیازی به به‌روزرسانی `docs/api/*` یا تست backend نیست. - بعد از تغییر: `ddev exec npx tsc --noEmit --project tsconfig.json` (صفر خطا) و `ddev exec yarn dev` (باید `webpack compiled successfully` بدهد؛ خطای `lightningcss.linux-arm64-gnu` پیش‌زمینه‌ای و بی‌ربط است). - تست دستی: در چند صفحه (کاربران، کلینیک‌ها، پیش‌ثبت‌نام، جزئیات دکتر/کلینیک) مودال/ConfirmDialog را باز کن و مطمئن شو وسط صفحه می‌افتد نه پایین.