لوحة منزلقة (Sheet)

التثبيت

bash
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

الخاصيةالنوعالافتراضيالوصف
openbooleanلا يوجديفتح اللوحة أو يغلقها عند التحكّم بالقيمة.
defaultOpenbooleanfalseيفتح اللوحة عند العرض الأول.
onOpenChange(open, details) => voidلا يوجدتُستدعى عند فتح اللوحة أو إغلاقها.
modalboolean | "trap-focus"trueما دامت اللوحة مفتوحة، يبقي التركيز داخلها ويمنع تمرير الصفحة. مرّر false ليخرج التركيز من اللوحة وتتمرّر الصفحة، أو "trap-focus" ليبقى التركيز داخلها دون منع التمرير.
childrenReactNodeلا يوجدالعنصر المُشغِّل ومحتوى اللوحة.

SheetTrigger

الخاصيةالنوعالافتراضيالوصف
renderReactElementلا يوجديعرض عنصرك مكان الزر الافتراضي، مثل مكوّن زر.
disabledbooleanfalseيمنع هذا العنصر المُشغِّل من فتح اللوحة.
childrenReactNodeلا يوجدنص العنصر المُشغِّل.
classNamestringلا يوجدأصناف CSS إضافية للعنصر المُشغِّل.

SheetContent

الخاصيةالنوعالافتراضيالوصف
side"top" | "bottom" | "start" | "end""end"الحافة التي تنزلق منها اللوحة. وتتبع القيمة start والقيمة end اتجاه القراءة.
showCloseButtonbooleantrueيُظهر زر الإغلاق في نهاية رأس اللوحة.
closeLabelstring"Close"اسم زر الإغلاق لدى قارئ الشاشة. الافتراضي «إغلاق» في الصفحة العربية و«Close» في الإنجليزية.
containerHTMLElementلا يوجدالعنصر الذي تُعرض فيه اللوحة، والقيمة الافتراضية هي body.
childrenReactNodeلا يوجدرأس اللوحة، ثم المحتوى، ثم التذييل.
classNamestringلا يوجدأصناف CSS إضافية للوحة.

SheetHeader

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجدالعنوان والوصف.
classNamestringلا يوجدأصناف CSS إضافية لرأس اللوحة.

SheetTitle

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجدالغرض من اللوحة.
classNamestringلا يوجدأصناف CSS إضافية للعنوان.

SheetDescription

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجدسطر يوضّح ما يفعله المستخدم في اللوحة.
classNamestringلا يوجدأصناف CSS إضافية للوصف.

SheetFooter

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجدالإجراءات، والإجراء الرئيسي أولًا.
classNamestringلا يوجدأصناف CSS إضافية للتذييل.

SheetClose

الخاصيةالنوعالافتراضيالوصف
renderReactElementلا يوجديعرض عنصرك مكان الزر الافتراضي، مثل مكوّن زر.
onClick(event) => voidلا يوجدتُستدعى قبل إغلاق اللوحة.
disabledbooleanfalseيعطّل الزر.
childrenReactNodeلا يوجدنص الزر، مثل «إلغاء».
classNamestringلا يوجدأصناف CSS إضافية للزر.

الاستخدام

تعديل ملف شخصي

tsx
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>
  )
}

فتح قائمة من جهة البداية

tsx
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>
  )
}

الإغلاق بعد الحفظ

tsx
"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>
  )
}