لوحة منزلقة (Sheet)
التثبيت
npx @smicolon/fasla-ui@latest add sheetقبل إضافة أي مكوّن، شغّل الأمر init مرة واحدة في مشروعك. التفاصيل في صفحة التثبيت.
مكوّن اللوحة المنزلقة مبني على مكتبة Base UI، لذلك يحتاج إلى الحزمة @base-ui/react، ويستخدم مكوّن زر لزر الإغلاق. يثبّت الأمر add كليهما مع المكوّن، وكذلك أداة shadcn.
معاينة
اضغط أحد الأزرار لفتح اللوحة من تلك الجهة. يبقى التنقل بمفتاح Tab داخلها، ويغلقها مفتاح Escape أو زر الإغلاق أو الضغط على الطبقة الخلفية.
الجهة
تحدد الخاصية side الحافة التي تنزلق منها اللوحة. القيمة end هي الافتراضية، وتناسب الإعدادات والتفاصيل وعوامل التصفية. وتناسب القيمة start قوائم التنقل. عرض اللوحة في الجهتين 384px، وارتفاعها بارتفاع النافذة. وتتبع القيمة start والقيمة end اتجاه القراءة: فالقيمة end هي الحافة اليسرى في الصفحة العربية، والحافة اليمنى في الصفحة الإنجليزية، وتنزلق اللوحة من تلك الحافة. أما مع القيمة top والقيمة bottom فعرض اللوحة بعرض النافذة، وارتفاعها بقدر محتواها. استخدم القيمة bottom للإجراءات القصيرة والاختيارات السريعة على الهاتف، والقيمة top لتنبيه أو شريط أدوات.
المحتوى والتذييل
كل ما تضعه بين المكوّن SheetHeader والمكوّن SheetFooter هو محتوى اللوحة، على بُعد 16px من كل منهما. اترك له 16px من كل جانب بصنف مثل px-4، كما في الحقول أدناه. وحين يطول المحتوى أكثر من اللوحة، يتمرّر المحتوى وحده: يبقى رأس اللوحة في الأعلى والتذييل في الأسفل. ويرصّ المكوّن SheetFooter أزراره بعرض اللوحة كاملًا، بينها 16px، والإجراء الرئيسي أولًا.
زر الإغلاق
يقع زر الإغلاق في نهاية رأس اللوحة، على بُعد 16px من الحافة، في منتصف السطر الأول من العنوان. يتبع اسمه لدى قارئ الشاشة لغة الصفحة، فيكون «إغلاق» في الصفحة العربية و«Close» في الإنجليزية، ويمكنك تغييره بالخاصية closeLabel. ويترك رأس اللوحة 48px في نهايته، فيلتفّ العنوان الطويل قبل أن يصل إلى الزر. وللإغلاق من زر خاص بك، مثل زر الإلغاء، ضعه داخل المكوّن SheetClose. ولا تعيّن الخاصية showCloseButton على false إلا إذا كان في اللوحة زر آخر يغلقها.
العنوان الطويل
يلتفّ العنوان الطويل داخل رأس اللوحة، ويبقى زر الإغلاق على سطره الأول، وينزل كل ما تحته لإفساح المكان. اجعل العنوان كلمات قليلة متى أمكن، ليبدأ المحتوى قريبًا من أعلى اللوحة.
إتاحة الوصول (accessibility)
- تحمل اللوحة الدور
dialog. يسمّيها عنوانها ويصفها وصفها، فيقرأ قارئ الشاشة الاثنين عند فتحها. - عند فتح اللوحة ينتقل التركيز إلى أول عنصر تفاعلي فيها، مثل أول حقل. يبقى التنقل بمفتاح Tab وبمفتاحي Shift+Tab داخل اللوحة، ويعود التركيز إلى العنصر المُشغِّل عند إغلاقها.
- يغلق اللوحة كلٌّ من مفتاح Escape وزر الإغلاق وأي زر داخل المكوّن
SheetCloseوالضغط على الطبقة الخلفية. - اسم زر الإغلاق «إغلاق» في الصفحة العربية و«Close» في الإنجليزية، وعلامة × فيه مخفية عن قارئ الشاشة.
- ما دامت اللوحة مفتوحة، لا يمكن الوصول إلى الصفحة خلفها بلوحة المفاتيح ولا بمؤشر الفأرة، ولا تتمرّر.
- تنزلق اللوحة من حافتها. وإذا فعّل المستخدم إعداد تقليل الحركة، تظهر وتختفي بالتلاشي بدل الانزلاق.
- تأخذ اللوحة اتجاه الصفحة ولغتها وخطها، مع أنها تُعرض في نهاية العنصر
body. في الصفحة العربية تُفتح القيمةendمن اليسار والقيمةstartمن اليمين، وينتقل زر الإغلاق مع حافة النهاية. ولا يحتاج الاتجاه إلى خاصية.
الخصائص (props)
Sheet
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
open | boolean | لا يوجد | يفتح اللوحة أو يغلقها عند التحكّم بالقيمة. |
defaultOpen | boolean | false | يفتح اللوحة عند العرض الأول. |
onOpenChange | (open, details) => void | لا يوجد | تُستدعى عند فتح اللوحة أو إغلاقها. |
modal | boolean | "trap-focus" | true | ما دامت اللوحة مفتوحة، يبقي التركيز داخلها ويمنع تمرير الصفحة. مرّر false ليخرج التركيز من اللوحة وتتمرّر الصفحة، أو "trap-focus" ليبقى التركيز داخلها دون منع التمرير. |
children | ReactNode | لا يوجد | العنصر المُشغِّل ومحتوى اللوحة. |
SheetTrigger
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
render | ReactElement | لا يوجد | يعرض عنصرك مكان الزر الافتراضي، مثل مكوّن زر. |
disabled | boolean | false | يمنع هذا العنصر المُشغِّل من فتح اللوحة. |
children | ReactNode | لا يوجد | نص العنصر المُشغِّل. |
className | string | لا يوجد | أصناف CSS إضافية للعنصر المُشغِّل. |
SheetContent
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
side | "top" | "bottom" | "start" | "end" | "end" | الحافة التي تنزلق منها اللوحة. وتتبع القيمة start والقيمة end اتجاه القراءة. |
showCloseButton | boolean | true | يُظهر زر الإغلاق في نهاية رأس اللوحة. |
closeLabel | string | "Close" | اسم زر الإغلاق لدى قارئ الشاشة. الافتراضي «إغلاق» في الصفحة العربية و«Close» في الإنجليزية. |
container | HTMLElement | لا يوجد | العنصر الذي تُعرض فيه اللوحة، والقيمة الافتراضية هي body. |
children | ReactNode | لا يوجد | رأس اللوحة، ثم المحتوى، ثم التذييل. |
className | string | لا يوجد | أصناف CSS إضافية للوحة. |
SheetHeader
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | العنوان والوصف. |
className | string | لا يوجد | أصناف CSS إضافية لرأس اللوحة. |
SheetTitle
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | الغرض من اللوحة. |
className | string | لا يوجد | أصناف CSS إضافية للعنوان. |
SheetDescription
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | سطر يوضّح ما يفعله المستخدم في اللوحة. |
className | string | لا يوجد | أصناف CSS إضافية للوصف. |
SheetFooter
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
children | ReactNode | لا يوجد | الإجراءات، والإجراء الرئيسي أولًا. |
className | string | لا يوجد | أصناف CSS إضافية للتذييل. |
SheetClose
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
render | ReactElement | لا يوجد | يعرض عنصرك مكان الزر الافتراضي، مثل مكوّن زر. |
onClick | (event) => void | لا يوجد | تُستدعى قبل إغلاق اللوحة. |
disabled | boolean | false | يعطّل الزر. |
children | ReactNode | لا يوجد | نص الزر، مثل «إلغاء». |
className | string | لا يوجد | أصناف CSS إضافية للزر. |
الاستخدام
تعديل ملف شخصي
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function EditProfile() {
return (
<Sheet>
<SheetTrigger render={<Button variant="outline" />}>تعديل الملف الشخصي</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>تعديل الملف الشخصي</SheetTitle>
<SheetDescription>قم بإجراء تغييرات على ملفك الشخصي هنا. انقر حفظ عند الانتهاء.</SheetDescription>
</SheetHeader>
<div className="grid gap-4 px-4">
<Input label="الاسم" defaultValue="نادية رحمن" />
<Input label="اسم المستخدم" defaultValue="nadia.rahman" />
</div>
<SheetFooter>
<Button type="submit">حفظ التغييرات</Button>
<SheetClose render={<Button variant="outline" tone="primary" />}>إلغاء</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}فتح قائمة من جهة البداية
import { MenuIcon } from "lucide-react"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"
export function NavigationMenu() {
return (
<Sheet>
<SheetTrigger render={<Button variant="ghost" size="icon-md" aria-label="القائمة" />}>
<MenuIcon />
</SheetTrigger>
<SheetContent side="start">
<SheetHeader>
<SheetTitle>فاصلة</SheetTitle>
<SheetDescription>انتقل إلى أي جزء من مساحة عملك.</SheetDescription>
</SheetHeader>
<nav className="grid gap-1 px-4">
<a href="/">الرئيسية</a>
<a href="/projects">المشاريع</a>
<a href="/settings">الإعدادات</a>
</nav>
</SheetContent>
</Sheet>
)
}الإغلاق بعد الحفظ
"use client"
import * as React from "react"
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function EditProfileAndSave({ onSave }: { onSave: (name: string) => Promise<void> }) {
const [open, setOpen] = React.useState(false)
const [name, setName] = React.useState("نادية رحمن")
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger render={<Button variant="outline" />}>تعديل الملف الشخصي</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>تعديل الملف الشخصي</SheetTitle>
<SheetDescription>قم بإجراء تغييرات على ملفك الشخصي هنا. انقر حفظ عند الانتهاء.</SheetDescription>
</SheetHeader>
<div className="px-4">
<Input label="الاسم" value={name} onChange={(event) => setName(event.target.value)} />
</div>
<SheetFooter>
<Button
onClick={async () => {
await onSave(name)
setOpen(false)
}}
>
حفظ التغييرات
</Button>
<SheetClose render={<Button variant="outline" tone="primary" />}>إلغاء</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}