نافذة منبثقة (Dialog)
استخدم النافذة المنبثقة لمهمة مركّزة تحتاج إلى مدخلات من المستخدم، مثل تعديل الملف الشخصي أو اختيار خطة، دون مغادرة الصفحة. أما لطلب تأكيد إجراء واحد أو إلغائه، مثل حذف مشروع، فاستخدم حوار التنبيه.
التثبيت
npx @smicolon/fasla-ui@latest add dialogقبل إضافة أي مكوّن، شغّل الأمر init مرة واحدة في مشروعك. التفاصيل في صفحة التثبيت.
مكوّن نافذة منبثقة مبني على مكتبة Base UI، لذلك يحتاج إلى الحزمة @base-ui/react، ويستخدم مكوّن زر لزرّي الإغلاق والرجوع. يثبّت الأمر add كليهما مع المكوّن، وكذلك أداة shadcn.
معاينة
اضغط الزر لفتح النافذة. ينتقل التركيز إلى أول حقل، ويبقى التنقل بمفتاح Tab داخلها. ويغلقها مفتاح Escape أو زر الإغلاق أو زر الإلغاء أو الضغط على الطبقة الخلفية.
الأحجام
تحدد الخاصية size على DialogContent عرض النافذة: القيمة lg هي الافتراضية وعرضها 512px، والقيمة xl عرضها 672px، والقيمة 2xl عرضها 1148px. وارتفاع كل الأحجام 634px مهما كان محتواها. وعلى الشاشة الأصغر تبقى النافذة على بُعد 16px من كل حافة. اختر الحجم حسب المحتوى: lg لنموذج قصير، و 2xl لجدول أو محرّر. اضغط زرًّا لفتح النافذة بحجمه.
الرأس
يضم المكوّن DialogHeader العنوان ووصفًا اختياريًا تحته بمسافة 4px، وتحتهما خط فاصل. ويبقى كل منهما في سطر واحد، وينتهي بنقاط ثلاث إذا طال، لذلك اجعلهما قصيرين. ويقع زر الإغلاق في نهاية سطر العنوان، وتخفيه الخاصية showCloseButton على DialogContent. ولخطوة ضمن سلسلة خطوات، مرّر الخاصية onBack إلى DialogHeader: يظهر زر الرجوع قبل العنوان. يشير سهمه إلى بداية السطر، فينعكس في الصفحات العربية.
الإجراءات
ضع الأزرار في المكوّن DialogFooter، وضع زر الإلغاء داخل DialogClose ليغلق النافذة. في الصف، وهو الترتيب الافتراضي، اكتب زر الإلغاء ثم الإجراء الرئيسي، فيقعان في نهاية الصف وبينهما 8px. وإذا كتبت قبلهما إجراءً ثالثًا، مثل «تخطٍّ»، فإنه ينتقل إلى بداية الصف. أما القيمة layout="stacked" فتجعل كل إجراء بعرض النافذة، واحدًا تحت الآخر، لذلك اكتب الإجراء الرئيسي أولًا. وللإجراء الذي لا يمكن التراجع عنه، أعطِ الزر الرئيسي variant="destructive".
المحتوى الطويل
ارتفاع النافذة 634px مهما كان محتواها. وحين يكون المحتوى أطول، يبقى الرأس والتذييل في مكانيهما، ويتمرّر ما بينهما فقط، فمرّر داخل النافذة أدناه لتكمل القراءة. وعلى الشاشة الأقصر تبقى النافذة على بُعد 16px من الأعلى والأسفل، وتتمرّر بالطريقة نفسها. وأي حقل تصل إليه بمفتاح Tab يظهر كاملًا بعيدًا عن الرأس والتذييل.
إتاحة الوصول (accessibility)
- تحمل النافذة الدور
dialog. يسمّيها عنوانها ويصفها وصفها، فيقرأ قارئ الشاشة الاثنين عند فتحها. - عند فتح النافذة ينتقل التركيز إلى أول عنصر تفاعلي فيها، مثل أول حقل. يبقى التنقل بمفتاح Tab وبمفتاحي Shift+Tab داخل النافذة، ويعود التركيز إلى العنصر المُشغِّل عند إغلاقها.
- يغلق النافذة مفتاح Escape، وزر الإغلاق، وأي زر داخل
DialogClose، والضغط على الطبقة الخلفية. - اسم زر الإغلاق «إغلاق» واسم زر الرجوع «رجوع» في الصفحات العربية، ويأخذان اسميهما الإنجليزيين في الصفحات الإنجليزية. وأيقونتاهما مخفيتان عن قارئ الشاشة.
- ما دامت النافذة مفتوحة، لا يمكن الوصول إلى الصفحة خلفها بلوحة المفاتيح ولا بمؤشر الفأرة، ولا تتمرّر.
- تظهر النافذة تدريجيًا وهي تكبر من 98%. وعند تفعيل تقليل الحركة تظهر تدريجيًا فقط.
- تأخذ النافذة اتجاه الصفحة ولغتها وخطها، مع أنها تُعرض في نهاية العنصر
body. في الصفحات العربية ينعكس ترتيب النافذة كله، وينعكس سهم الرجوع. ولا يحتاج الاتجاه إلى خاصية.
الخصائص (props)
Dialog
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
open | boolean | لا يوجد | يفتح النافذة أو يغلقها عند التحكّم بالقيمة. |
defaultOpen | boolean | false | يفتح النافذة عند العرض الأول. |
onOpenChange | (open, details) => void | لا يوجد | تُستدعى عند فتح النافذة أو إغلاقها. |
modal | boolean | "trap-focus" | true | ما دامت النافذة مفتوحة، يبقى التركيز داخلها ولا تتمرّر الصفحة. مرّر false ليخرج التركيز من النافذة وتتمرّر الصفحة، أو "trap-focus" ليبقى التركيز داخلها دون إيقاف التمرير. |
children | ReactNode | لا يوجد | العنصر المُشغِّل ومحتوى النافذة. |
DialogTrigger
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
render | ReactElement | لا يوجد | يعرض عنصرك مكان الزر الافتراضي، مثل مكوّن زر. |
disabled | boolean | false | يمنع هذا العنصر المُشغِّل من فتح النافذة. |
children | ReactNode | لا يوجد | نص العنصر المُشغِّل. |
className | string | لا يوجد | أصناف CSS إضافية للعنصر المُشغِّل. |
DialogContent
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
size | "lg" | "xl" | "2xl" | "lg" | عرض النافذة: lg بعرض 512px، و xl بعرض 672px، و 2xl بعرض 1148px. |
showCloseButton | boolean | true | يعرض زر الإغلاق في نهاية سطر العنوان. |
closeLabel | string | "Close" | اسم زر الإغلاق لقارئ الشاشة. الافتراضي «إغلاق» في الصفحات العربية، و Close في الإنجليزية. |
container | HTMLElement | لا يوجد | العنصر الذي تُعرض فيه النافذة، والقيمة الافتراضية هي body. |
children | ReactNode | لا يوجد | الرأس، ثم المحتوى، ثم التذييل. |
className | string | لا يوجد | أصناف CSS إضافية لصندوق النافذة. |
DialogHeader
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
onBack | (event) => void | لا يوجد | يعرض زر الرجوع قبل العنوان، وتُستدعى عند الضغط عليه. |
backLabel | string | "Back" | اسم زر الرجوع لقارئ الشاشة. الافتراضي «رجوع» في الصفحات العربية، و Back في الإنجليزية. |
children | ReactNode | لا يوجد | العنوان والوصف. |
className | string | لا يوجد | أصناف CSS إضافية للرأس. |
DialogTitle
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | الغرض من النافذة. |
className | string | لا يوجد | أصناف CSS إضافية للعنوان. |
DialogDescription
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | سطر يوضّح ما يفعله المستخدم في النافذة. |
className | string | لا يوجد | أصناف CSS إضافية للوصف. |
DialogFooter
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
layout | "horizontal" | "stacked" | "horizontal" | تضع القيمة horizontal الإجراءات في صف عند نهاية السطر. وتجعل القيمة stacked كل إجراء بعرض النافذة، واحدًا تحت الآخر. |
showCloseButton | boolean | false | يضيف زر إغلاق بالنمط Outline يغلق النافذة. |
children | ReactNode | لا يوجد | الإجراءات: أزرار، وزر الإلغاء داخل DialogClose. |
className | string | لا يوجد | أصناف CSS إضافية للتذييل. |
DialogClose
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
render | ReactElement | لا يوجد | يعرض عنصرك مكان الزر الافتراضي، مثل مكوّن زر. |
onClick | (event) => void | لا يوجد | تُستدعى قبل إغلاق النافذة. |
disabled | boolean | false | يعطّل الزر. |
children | ReactNode | لا يوجد | نص الزر، مثل «إلغاء». |
className | string | لا يوجد | أصناف CSS إضافية للزر. |
الاستخدام
تعديل الملف الشخصي
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function EditProfile() {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>تعديل الملف الشخصي</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>تعديل الملف الشخصي</DialogTitle>
<DialogDescription>
حدّث اسمك وصورتك.
</DialogDescription>
</DialogHeader>
<Input label="البريد الإلكتروني" type="email" placeholder="اكتب هنا" />
<DialogFooter>
<DialogClose render={<Button variant="outline" />}>إلغاء</DialogClose>
<Button type="submit">حفظ التغييرات</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}الرجوع خطوة
"use client"
import * as React from "react"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function NewProject() {
const [step, setStep] = React.useState(1)
return (
<Dialog onOpenChange={(open) => open && setStep(1)}>
<DialogTrigger render={<Button />}>مشروع جديد</DialogTrigger>
<DialogContent>
{step === 1 ? (
<DialogHeader>
<DialogTitle>سمِّ مشروعك</DialogTitle>
<DialogDescription>الخطوة 1 من 3.</DialogDescription>
</DialogHeader>
) : (
<DialogHeader onBack={() => setStep(1)}>
<DialogTitle>اختر مساحة العمل</DialogTitle>
<DialogDescription>الخطوة 2 من 3.</DialogDescription>
</DialogHeader>
)}
{step === 1 ? (
<Input label="اسم المشروع" placeholder="مدار" />
) : (
<Input label="اسم مساحة العمل" placeholder="فريق التصميم" />
)}
<DialogFooter>
<DialogClose render={<Button variant="outline" />}>إلغاء</DialogClose>
{step === 1 ? (
<Button onClick={() => setStep(2)}>التالي</Button>
) : (
<DialogClose render={<Button />}>إنشاء المشروع</DialogClose>
)}
</DialogFooter>
</DialogContent>
</Dialog>
)
}الإغلاق بعد الحفظ
"use client"
import * as React from "react"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function EditProfileAndSave({ onSave }: { onSave: (email: string) => Promise<void> }) {
const [open, setOpen] = React.useState(false)
const [pending, setPending] = React.useState(false)
const [email, setEmail] = React.useState("")
return (
<Dialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
<DialogTrigger render={<Button variant="outline" />}>تعديل الملف الشخصي</DialogTrigger>
<DialogContent>
<form
className="contents"
onSubmit={async (event) => {
event.preventDefault()
setPending(true)
try {
await onSave(email)
setOpen(false)
} finally {
setPending(false)
}
}}
>
<DialogHeader>
<DialogTitle>تعديل الملف الشخصي</DialogTitle>
</DialogHeader>
<Input
label="البريد الإلكتروني"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<DialogFooter>
<DialogClose disabled={pending} render={<Button variant="outline" />}>
إلغاء
</DialogClose>
<Button type="submit" loading={pending} loadingLabel="جارٍ الحفظ">
حفظ التغييرات
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}