حوار التنبيه (Alert Dialog)
التثبيت
npx @smicolon/fasla-ui@latest add alert-dialogقبل إضافة أي مكوّن، شغّل الأمر init مرة واحدة في مشروعك. التفاصيل في صفحة التثبيت.
مكوّن حوار التنبيه مبني على مكتبة Base UI، لذلك يحتاج إلى الحزمة @base-ui/react، ويستخدم مكوّن زر للإجراءات. يثبّت الأمر add كليهما مع المكوّن، وكذلك أداة shadcn.
معاينة
اضغط الزر لفتح الحوار. ينقلك مفتاح Tab بين أزراره، ويغلقه مفتاح Escape.
المحاذاة
تحدد الخاصية align ترتيب الحوار. القيمة edge هي الافتراضية، وفيها يبدأ العنوان والوصف من بداية السطر، وتقع الإجراءات في نهاية الصف. أما القيمة center فتضع النص والإجراءات في المنتصف. استخدمها مع الوصف القصير فقط، لأن قراءة الأسطر المتوسّطة تصعب كلما طال النص.
الإجراء الخطر
فعّل الخاصية destructive حين لا يمكن التراجع عن الإجراء، مثل حذف مشروع. يتحوّل الإجراء الرئيسي إلى زر الإجراءات الخطرة، ويبقى زر الإلغاء كما هو. سمِّ الإجراء في نص الزر، مثل «حذف»، حتى يوضّح الزر وحده ما سيحدث.
الأيقونة
تضيف الخاصية icon أيقونة تنبيه إلى العنوان، بحجم 24px وبلون العنوان نفسه. مع القيمة edge تظهر بجانب العنوان في منتصف سطره الأول، ويبدأ الوصف وأي محتوى إضافي من حيث يبدأ العنوان، لا من الأيقونة. ومع القيمة center تظهر فوق العنوان. ولعرض أيقونة أخرى، مرّر أي أيقونة بدل القيمة true، مثل أيقونة تسجيل الخروج أدناه. وتظهر بحجم أيقونة التنبيه ولونها وموضعها.
المحتوى الإضافي
كل ما تضعه بين المكوّن AlertDialogHeader والمكوّن AlertDialogFooter يظهر بين النص والإجراءات، على بُعد 16px من كل منهما، وبعرض النص نفسه. استخدمه لحقل قصير واحد أو خانة اختيار يعتمد عليها القرار، مثل كتابة اسم المشروع قبل حذفه. أما النموذج الأطول فمكانه صفحة مستقلة أو حوار عادي.
انتظار الإجراء
يغلق الإجراء الرئيسي الحوار بعد تنفيذ الدالة onClick. وحين يستغرق الإجراء وقتًا، استدعِ event.preventBaseUIHandler() داخل onClick ليبقى الحوار مفتوحًا، وفعّل الخاصية loading على زر الإجراء أثناء تنفيذ الطلب، ثم أغلق الحوار بالخاصية open عند انتهائه.
النص الطويل
يلتفّ العنوان والوصف الطويلان داخل الحوار الذي لا يزيد عرضه على 512px. وحين يصبح الحوار أطول من النافذة، يتمرّر محتواه في داخله. وعلى الهاتف يبقى على بُعد 16px من كل حافة للشاشة.
إتاحة الوصول (accessibility)
- يحمل الحوار الدور
alertdialog. يسمّيه عنوانه ويصفه وصفه، فيقرأ قارئ الشاشة الاثنين عند فتحه. - الأيقونة، سواء كانت أيقونة التنبيه أو أيقونة أخرى، مخفية عن قارئ الشاشة. لذلك اجعل العنوان يسمّي الإجراء وحده، مثل «تسجيل الخروج من كل الأجهزة».
- عند فتح الحوار ينتقل التركيز إلى أول عنصر تفاعلي فيه: زر الإلغاء، أو أول حقل في المحتوى الإضافي. يبقى التنقل بمفتاح Tab وبمفتاحي Shift+Tab داخل الحوار، ويعود التركيز إلى العنصر المُشغِّل عند إغلاقه.
- يغلق مفتاح Escape الحوار كما يغلقه زر الإلغاء. ولا يفعل الضغط على الطبقة الخلفية شيئًا، فلا تتخذ نقرة عابرة القرار نيابة عن المستخدم.
- ما دام الحوار مفتوحًا، لا يمكن الوصول إلى الصفحة خلفه بلوحة المفاتيح ولا بمؤشر الفأرة، ولا تتمرّر.
- يأخذ الحوار اتجاه الصفحة ولغتها وخطها، مع أنه يُعرض في نهاية العنصر
body. في الصفحات العربية ينعكس ترتيب الحوار كله، ويبقى زر الإلغاء أولًا في ترتيب القراءة. ولا يحتاج الاتجاه إلى خاصية.
الخصائص (props)
AlertDialog
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
open | boolean | لا يوجد | يفتح الحوار أو يغلقه عند التحكّم بالقيمة. |
defaultOpen | boolean | false | يفتح الحوار عند العرض الأول. |
onOpenChange | (open, details) => void | لا يوجد | تُستدعى عند فتح الحوار أو إغلاقه. |
onOpenChangeComplete | (open) => void | لا يوجد | تُستدعى بعد اكتمال فتح الحوار أو إغلاقه. |
actionsRef | RefObject | لا يوجد | مرجع فيه الدالة close() لإغلاق الحوار من الشيفرة. |
handle | AlertDialogHandle | لا يوجد | يربط الحوار بعناصر مُشغِّلة في مكان آخر من الصفحة. أنشئه بالدالة AlertDialog.createHandle() من الحزمة @base-ui/react/alert-dialog. |
triggerId | string | null | لا يوجد | العنصر المُشغِّل الذي يتبعه الحوار من بين عدة عناصر، حين تتحكم بالخاصية open. |
defaultTriggerId | string | null | لا يوجد | العنصر المُشغِّل الذي يتبعه الحوار عند العرض الأول، مع الخاصية defaultOpen. |
children | ReactNode | لا يوجد | العنصر المُشغِّل ومحتوى الحوار. |
AlertDialogTrigger
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
render | ReactElement | لا يوجد | يعرض عنصرك مكان الزر الافتراضي، مثل مكوّن زر. |
handle | AlertDialogHandle | لا يوجد | مقبض حوار موجود في مكان آخر يفتحه هذا العنصر المُشغِّل. |
payload | unknown | لا يوجد | بيانات تُمرَّر إلى الحوار حين يفتحه هذا العنصر المُشغِّل. |
disabled | boolean | false | يمنع هذا العنصر المُشغِّل من فتح الحوار. |
children | ReactNode | لا يوجد | نص العنصر المُشغِّل. |
className | string | لا يوجد | أصناف CSS إضافية للعنصر المُشغِّل. |
AlertDialogContent
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
align | "edge" | "center" | "edge" | تبدأ القيمة edge النص من بداية السطر وتضع الإجراءات في نهايته. وتضع القيمة center الاثنين في المنتصف. |
destructive | boolean | false | يحوّل الإجراء الرئيسي إلى زر الإجراءات الخطرة. |
icon | boolean | ReactNode | false | القيمة true لأيقونة التنبيه، أو أي أيقونة أخرى. وفي الحالتين تظهر بحجم 24px وبلون العنوان. |
container | HTMLElement | لا يوجد | العنصر الذي يُعرض فيه الحوار، والقيمة الافتراضية هي body. |
initialFocus | boolean | RefObject | لا يوجد | العنصر الذي يأخذ التركيز عند فتح الحوار. الافتراضي هو أول عنصر تفاعلي. |
finalFocus | boolean | RefObject | لا يوجد | العنصر الذي يأخذ التركيز عند إغلاق الحوار. الافتراضي هو العنصر المُشغِّل. |
children | ReactNode | لا يوجد | رأس الحوار، ثم أي محتوى إضافي، ثم التذييل. |
className | string | لا يوجد | أصناف CSS إضافية لصندوق الحوار. |
AlertDialogHeader
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | العنوان والوصف. |
className | string | لا يوجد | أصناف CSS إضافية لرأس الحوار. |
AlertDialogTitle
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | السؤال الذي يطرحه الحوار. |
className | string | لا يوجد | أصناف CSS إضافية للعنوان. |
AlertDialogDescription
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | ما سيحدث إذا تابع المستخدم. |
className | string | لا يوجد | أصناف CSS إضافية للوصف. |
AlertDialogFooter
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | زر الإلغاء، ثم الإجراء الرئيسي. |
className | string | لا يوجد | أصناف CSS إضافية لصف الإجراءات. |
AlertDialogCancel
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
onClick | (event) => void | لا يوجد | تُستدعى قبل إغلاق الحوار. |
disabled | boolean | false | يعطّل الزر. |
children | ReactNode | لا يوجد | نص الزر، وعادةً «إلغاء». |
className | string | لا يوجد | أصناف CSS إضافية للزر. |
AlertDialogAction
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
onClick | (event) => void | لا يوجد | تنفّذ الإجراء ثم تغلق الحوار. استدعِ event.preventBaseUIHandler() ليبقى مفتوحًا. |
loading | boolean | false | يعرض مؤشر التحميل، ويعطّل الزر، ويُعلن نص الخاصية loadingLabel. |
loadingLabel | string | "Loading" | نص يُعلنه قارئ الشاشة أثناء التحميل. |
disabled | boolean | false | يعطّل الزر. |
children | ReactNode | لا يوجد | نص الإجراء. |
className | string | لا يوجد | أصناف CSS إضافية للزر. |
الاستخدام
تأكيد الحذف
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export function DeleteProject({ onDelete }: { onDelete: () => void }) {
return (
<AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" tone="destructive" />}>
حذف المشروع
</AlertDialogTrigger>
<AlertDialogContent destructive>
<AlertDialogHeader>
<AlertDialogTitle>حذف هذا المشروع؟</AlertDialogTitle>
<AlertDialogDescription>
سيُحذف المشروع وجميع ملفاته لكل أعضاء فريقك، ولا يمكن استعادتها بعد ذلك.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>إلغاء</AlertDialogCancel>
<AlertDialogAction onClick={onDelete}>حذف</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}طلب اسم المشروع
"use client"
import * as React from "react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function DeleteProjectByName({ onDelete }: { onDelete: () => void }) {
const [name, setName] = React.useState("")
return (
<AlertDialog onOpenChange={(open) => open && setName("")}>
<AlertDialogTrigger render={<Button variant="outline" tone="destructive" />}>
حذف المشروع
</AlertDialogTrigger>
<AlertDialogContent destructive icon>
<AlertDialogHeader>
<AlertDialogTitle>حذف هذا المشروع؟</AlertDialogTitle>
<AlertDialogDescription>
سيُحذف المشروع وجميع ملفاته لكل أعضاء فريقك، ولا يمكن استعادتها بعد ذلك.
</AlertDialogDescription>
</AlertDialogHeader>
<Input
label="اكتب اسم المشروع للتأكيد"
placeholder="مدار"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<AlertDialogFooter>
<AlertDialogCancel>إلغاء</AlertDialogCancel>
<AlertDialogAction disabled={name !== "مدار"} onClick={onDelete}>
حذف
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}استخدام أيقونة أخرى
import { LogOutIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export function SignOutEverywhere({ onSignOut }: { onSignOut: () => void }) {
return (
<AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" />}>تسجيل الخروج</AlertDialogTrigger>
<AlertDialogContent icon={<LogOutIcon strokeWidth={1.5} className="[&:dir(rtl)]:-scale-x-100" />}>
<AlertDialogHeader>
<AlertDialogTitle>تسجيل الخروج من كل الأجهزة؟</AlertDialogTitle>
<AlertDialogDescription>
ستحتاج إلى تسجيل الدخول مجددًا على هاتفك وحاسوبك وأي جهاز آخر تستخدمه.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>إلغاء</AlertDialogCancel>
<AlertDialogAction onClick={onSignOut}>تسجيل الخروج</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}إبقاء الحوار مفتوحًا أثناء تنفيذ الإجراء
"use client"
import * as React from "react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export function DeleteProjectAndWait({ onDelete }: { onDelete: () => Promise<void> }) {
const [open, setOpen] = React.useState(false)
const [pending, setPending] = React.useState(false)
return (
<AlertDialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
<AlertDialogTrigger render={<Button variant="outline" tone="destructive" />}>
حذف المشروع
</AlertDialogTrigger>
<AlertDialogContent destructive>
<AlertDialogHeader>
<AlertDialogTitle>حذف هذا المشروع؟</AlertDialogTitle>
<AlertDialogDescription>
سيُحذف المشروع وجميع ملفاته لكل أعضاء فريقك، ولا يمكن استعادتها بعد ذلك.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={pending}>إلغاء</AlertDialogCancel>
<AlertDialogAction
loading={pending}
loadingLabel="جارٍ الحذف"
onClick={async (event) => {
event.preventBaseUIHandler()
setPending(true)
try {
await onDelete()
setOpen(false)
} finally {
setPending(false)
}
}}
>
حذف
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}