قائمة منسدلة (Dropdown Menu)

قائمة بالإجراءات والإعدادات تُفتح من زر. استخدمها لعرض قائمة قصيرة بما يستطيع القارئ فعله في الصفحة، أو بعنصر واحد فيها، مثل التعديل أو المشاركة أو تسجيل الخروج.

التثبيت

bash
npx @smicolon/fasla-ui@latest add dropdown-menu

قبل إضافة أي مكوّن، شغّل الأمر init مرة واحدة في مشروعك. التفاصيل في صفحة التثبيت.

مكوّن القائمة المنسدلة مبني على مكتبة Base UI، لذلك يحتاج إلى الحزمة @base-ui/react. ويتشارك أنماط القائمة مع المكوّن select، وتستخدم صفوف مفتاح التبديل المكوّن switch. يثبّت الأمر add كليهما مع المكوّن، وكذلك أداة shadcn.

معاينة

اضغط الزر، أو انتقل إليه بمفتاح Tab ثم اضغط مفتاح Enter أو السهم السفلي.

الأنماط (variant)

للصفوف نمطان (variant) تحددهما الخاصية variant. النمط default للإجراءات المعتادة، والنمط destructive لإجراء يحذف شيئًا أو لا يمكن التراجع عنه: ضعه في آخر القائمة بعد فاصل، واكتب في عنوانه ما يفعله بالضبط. وتضع الخاصية icon أيقونة بحجم 16px قبل النص.

الحالات

يُبرز الصف حين يستقر عليه مؤشر الفأرة أو تصل إليه مفاتيح الأسهم، فمرّر المؤشر فوق الصفوف أعلاه لترى ذلك. والصف الذي يحمل الخاصية disabled يظهر باهتًا ولا يمكن الضغط عليه، لكنه يبقى في متناول مفاتيح الأسهم، ليعلن قارئ الشاشة أنه غير متاح.

العناوين والمجموعات والأيقونات

يضع المكوّن DropdownMenuLabel عنوانًا فوق مجموعة من الصفوف. وإذا وضعت العنوان وصفوفه داخل DropdownMenuGroup، يعلن قارئ الشاشة المجموعة بهذا العنوان. ويمكن أن يحمل العنوان أكثر من نص، مثل صورة رمزية مع اسم وبريد إلكتروني. ويرسم المكوّن DropdownMenuSeparator خطًا يفصل بين المجموعات.

صفوف الاختيار

المكوّن DropdownMenuCheckboxItem إعداد يُفعَّل أو يُلغى. الضغط عليه يبدّل حالته ويُبقي القائمة مفتوحة. افتراضيًا تظهر علامة الصح قبل النص حين يكون مفعّلًا، ويبقى مكانها محجوزًا حين يكون ملغى، فلا يتحرك النص. ومع indicator="switch" يعرض مفتاح التبديل في نهاية الصف الحالة بدلًا منها.

مجموعة الخيارات

يضم المكوّن DropdownMenuRadioGroup صفوفًا يُختار منها صف واحد فقط، مثل إعداد له قيم قليلة. وتدل نقطة قبل النص على الصف المختار. الضغط على صف يختاره ويُبقي القائمة مفتوحة.

القوائم الفرعية والاختصارات

يضع المكوّن DropdownMenuSub قائمة ثانية داخل القائمة. وينتهي صف DropdownMenuSubTrigger بسهم صغير يشير إلى الجهة التي تُفتح فيها القائمة الفرعية، وهي نهاية السطر: اليمين في الإنجليزية واليسار في العربية. وتُفتح عند تمرير مؤشر الفأرة أو الضغط، أو بمفتاح السهم الذي يشير إليها. ويعرض المكوّن DropdownMenuShortcut اختصار لوحة المفاتيح في نهاية الصف، بالترتيب نفسه في اللغتين. وهو يعرض المفاتيح فقط، فاستمع لها في شيفرتك.

إتاحة الوصول (accessibility)

  • تمنح مكتبة Base UI القائمة الدور menu، وكل صف الدور menuitem أو menuitemcheckbox أو menuitemradio، فيعلن قارئ الشاشة نوع كل صف وهل هو مفعّل. ويُعلم العنصر المُشغِّل هل القائمة مفتوحة.
  • تُفتح القائمة من العنصر المُشغِّل بمفتاح Enter أو بمفتاح المسافة أو بالسهم السفلي. تنتقل بين الصفوف بمفاتيح الأسهم، ويقفز مفتاح Home إلى الصف الأول ومفتاح End إلى الأخير، وتقفز كتابة حرف إلى الصف التالي الذي يبدأ به. ويغلق مفتاح Escape القائمة ويعيد التركيز إلى العنصر المُشغِّل.
  • امنح العنصر المُشغِّل اسمًا يوضّح الغرض من القائمة، وإذا كان أيقونة فقط فأضف السمة aria-label. وضع العناوين داخل DropdownMenuGroup أو DropdownMenuRadioGroup لتسمّي صفوفها.
  • في صف مفتاح التبديل، الصف نفسه هو خانة الاختيار. ومفتاح التبديل يعرض الحالة فقط، لذلك يُخفى عن قارئ الشاشة ولا يستقبل الضغط ولا التركيز.
  • تأخذ القائمة اتجاه الصفحة ولغتها وخطها من العنصر المُشغِّل، مع أنها تُعرض في نهاية العنصر body. ففي الصفحة العربية تحاذي الحافة اليمنى للعنصر المُشغِّل، وتُفتح القوائم الفرعية إلى اليسار. ولا يحتاج الاتجاه إلى خاصية.

الخصائص (props)

DropdownMenu

الخاصيةالنوعالافتراضيالوصف
openbooleanلا يوجديفتح القائمة أو يغلقها حين تتحكم بها.
defaultOpenbooleanfalseيفتح القائمة عند العرض الأول.
onOpenChange(open, eventDetails) => voidلا يوجدتُستدعى عند فتح القائمة أو إغلاقها.
modalbooleantrueيمنع الضغط خارج القائمة وتمرير الصفحة ما دامت مفتوحة. مرّر false لتبقى الصفحة قابلة للاستخدام.
disabledbooleanfalseيمنع فتح القائمة.
childrenReactNodeلا يوجدمحتوى هذا الجزء.

DropdownMenuTrigger

الخاصيةالنوعالافتراضيالوصف
renderReactElementلا يوجديستبدل الزر الافتراضي بعنصرك، مثل مكوّن زر.
disabledbooleanfalseيمنع هذا العنصر المُشغِّل من فتح القائمة.
openOnHoverbooleanfalseيفتح القائمة أيضًا حين يستقر مؤشر الفأرة على العنصر المُشغِّل.
delaynumber100مدة الانتظار قبل الفتح بالتمرير، بالملّي ثانية.
closeDelaynumber0مدة الانتظار قبل إغلاق قائمة فُتحت بالتمرير، بالملّي ثانية.
childrenReactNodeلا يوجدمحتوى هذا الجزء.
classNamestringلا يوجدأصناف CSS إضافية لهذا الجزء.

DropdownMenuContent

الخاصيةالنوعالافتراضيالوصف
side"bottom" | "top" | "left" | "right" | "inline-start" | "inline-end""bottom"جهة العنصر المُشغِّل التي تُفتح فيها القائمة. وتتبع الجهتان inline-* اتجاه القراءة.
align"start" | "center" | "end""start"موضع القائمة على طول تلك الجهة. وتتبع القيمة start اتجاه القراءة.
sideOffsetnumber4المسافة بين العنصر المُشغِّل والقائمة، بالبكسل.
alignOffsetnumber0يُزيح القائمة على طول جهتها، بالبكسل.
collisionAvoidance{ side, align, fallbackAxisSide }لا يوجدكيف تتجنب القائمة حافة النافذة. تنتقل تلقائيًا إلى الجهة الأخرى من العنصر المُشغِّل حين لا يتسع لها المكان، وتبقيها القيمة { side: "none" } في مكانها.
containerHTMLElementلا يوجدالعنصر الذي تُعرض فيه القائمة وقوائمها الفرعية، والافتراضي العنصر body.
childrenReactNodeلا يوجدمحتوى هذا الجزء.
classNamestringلا يوجدأصناف CSS إضافية للقائمة، مثل العرض.

DropdownMenuLabel

الخاصيةالنوعالافتراضيالوصف
insetbooleanfalseيحاذي النص مع الصفوف التي فيها أيقونة أو علامة.
childrenReactNodeلا يوجدمحتوى هذا الجزء.
classNamestringلا يوجدأصناف CSS إضافية لهذا الجزء.

DropdownMenuItem

الخاصيةالنوعالافتراضيالوصف
variant"default" | "destructive""default"يدل النمط destructive على إجراء يحذف شيئًا أو لا يمكن التراجع عنه.
iconReactNodeلا يوجدأيقونة بحجم 16px قبل النص.
insetbooleanfalseيحاذي النص مع الصفوف التي فيها أيقونة أو علامة.
disabledbooleanfalseيُظهر الصف باهتًا ويمنع الضغط عليه.
onClick(event) => voidلا يوجدتُستدعى عند الضغط على الصف أو اختياره من لوحة المفاتيح.
closeOnClickbooleantrueيغلق القائمة عند الضغط على الصف.
labelstringلا يوجدالنص الذي تطابقه كتابة حرف، والافتراضي نص الصف.
childrenReactNodeلا يوجدمحتوى هذا الجزء.
classNamestringلا يوجدأصناف CSS إضافية لهذا الجزء.

DropdownMenuCheckboxItem

الخاصيةالنوعالافتراضيالوصف
indicator"check" | "switch""check"كيف يُظهر الصف أنه مفعّل: علامة الصح قبل النص، أو مفتاح التبديل في نهاية الصف.
checkedbooleanلا يوجدهل الصف مفعّل، حين تتحكم به.
defaultCheckedbooleanfalseهل الصف مفعّل عند العرض الأول.
onCheckedChange(checked, eventDetails) => voidلا يوجدتُستدعى بالحالة الجديدة عند تبديل الصف.
disabledbooleanfalseيُظهر الصف باهتًا ويمنع الضغط عليه.
closeOnClickbooleanfalseيغلق القائمة عند الضغط على الصف. وهو معطّل افتراضيًا، لتغيير عدة صفوف دفعة واحدة.
labelstringلا يوجدالنص الذي تطابقه كتابة حرف، والافتراضي نص الصف.
childrenReactNodeلا يوجدمحتوى هذا الجزء.
classNamestringلا يوجدأصناف CSS إضافية لهذا الجزء.

DropdownMenuRadioGroup

الخاصيةالنوعالافتراضيالوصف
valueunknownلا يوجدالقيمة المختارة، حين تتحكم بها.
defaultValueunknownلا يوجدالقيمة المختارة عند العرض الأول.
onValueChange(value, eventDetails) => voidلا يوجدتُستدعى بالقيمة الجديدة عند اختيار صف.
disabledbooleanfalseيمنع اختيار أي صف في المجموعة.
childrenReactNodeلا يوجدمحتوى هذا الجزء.
classNamestringلا يوجدأصناف CSS إضافية لهذا الجزء.

DropdownMenuRadioItem

الخاصيةالنوعالافتراضيالوصف
valueunknownلا يوجدالقيمة التي يضعها هذا الصف في مجموعته.
disabledbooleanfalseيُظهر الصف باهتًا ويمنع الضغط عليه.
closeOnClickbooleanfalseيغلق القائمة عند الضغط على الصف. وهو معطّل افتراضيًا، لتغيير عدة صفوف دفعة واحدة.
labelstringلا يوجدالنص الذي تطابقه كتابة حرف، والافتراضي نص الصف.
childrenReactNodeلا يوجدمحتوى هذا الجزء.
classNamestringلا يوجدأصناف CSS إضافية لهذا الجزء.

DropdownMenuSub

الخاصيةالنوعالافتراضيالوصف
openbooleanلا يوجديفتح القائمة الفرعية أو يغلقها حين تتحكم بها.
defaultOpenbooleanfalseيفتح القائمة الفرعية عند العرض الأول.
onOpenChange(open, eventDetails) => voidلا يوجدتُستدعى عند فتح القائمة الفرعية أو إغلاقها.
disabledbooleanfalseيمنع فتح القائمة الفرعية.
childrenReactNodeلا يوجدمحتوى هذا الجزء.

DropdownMenuSubTrigger

الخاصيةالنوعالافتراضيالوصف
iconReactNodeلا يوجدأيقونة بحجم 16px قبل النص.
insetbooleanfalseيحاذي النص مع الصفوف التي فيها أيقونة أو علامة.
disabledbooleanfalseيُظهر الصف باهتًا ويمنع الضغط عليه.
openOnHoverbooleantrueيفتح القائمة الفرعية حين يستقر مؤشر الفأرة على صفها.
delaynumber100مدة الانتظار قبل الفتح بالتمرير، بالملّي ثانية.
closeDelaynumber0مدة الانتظار قبل إغلاق قائمة فُتحت بالتمرير، بالملّي ثانية.
labelstringلا يوجدالنص الذي تطابقه كتابة حرف، والافتراضي نص الصف.
childrenReactNodeلا يوجدمحتوى هذا الجزء.
classNamestringلا يوجدأصناف CSS إضافية لهذا الجزء.

DropdownMenuSubContent

الخاصيةالنوعالافتراضيالوصف
sideOffsetnumber0المسافة بين الصف والقائمة الفرعية، بالبكسل.
alignOffsetnumber-5يُزيح القائمة الفرعية على طول الصف، بالبكسل. والقيمة الافتراضية تحاذي صفها الأول مع صف العنصر المُشغِّل.
collisionAvoidance{ side, align, fallbackAxisSide }لا يوجدكيف تتجنب القائمة حافة النافذة. تنتقل تلقائيًا إلى الجهة الأخرى من العنصر المُشغِّل حين لا يتسع لها المكان، وتبقيها القيمة { side: "none" } في مكانها.
containerHTMLElementلا يوجدالعنصر الذي تُعرض فيه القائمة الفرعية، والافتراضي عنصر القائمة نفسها.
childrenReactNodeلا يوجدمحتوى هذا الجزء.
classNamestringلا يوجدأصناف CSS إضافية للقائمة، مثل العرض.

لا يأخذ المكوّن DropdownMenuGroup ولا المكوّن DropdownMenuShortcut إلا الخاصية className مع children، ولا يأخذ المكوّن DropdownMenuSeparator إلا الخاصية className.

الاستخدام

قائمة بسيطة

tsx
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function AccountMenu() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>فتح القائمة</DropdownMenuTrigger>
      <DropdownMenuContent className="w-56">
        <DropdownMenuLabel>حسابي</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuItem>
          الملف الشخصي
          <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
        </DropdownMenuItem>
        <DropdownMenuItem>الفوترة</DropdownMenuItem>
        <DropdownMenuItem disabled>واجهة البرمجة</DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive">تسجيل الخروج</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

أيقونات وإجراء حذف

tsx
import { PencilLine, Share, Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function FileActionsMenu() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>فتح القائمة</DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem icon={<PencilLine strokeWidth={1.5} />}>تعديل</DropdownMenuItem>
        <DropdownMenuItem icon={<Share strokeWidth={1.5} />}>مشاركة</DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive" icon={<Trash2 strokeWidth={1.5} />}>
          حذف
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

صفوف الاختيار ومفتاح التبديل

tsx
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuLabel,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function NotificationsMenu() {
  const [email, setEmail] = useState(true)
  const [sms, setSms] = useState(false)

  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>الإشعارات</DropdownMenuTrigger>
      <DropdownMenuContent className="w-56">
        <DropdownMenuGroup>
          <DropdownMenuLabel>أبلغني عبر</DropdownMenuLabel>
          <DropdownMenuCheckboxItem checked={email} onCheckedChange={setEmail}>
            البريد الإلكتروني
          </DropdownMenuCheckboxItem>
          <DropdownMenuCheckboxItem indicator="switch" checked={sms} onCheckedChange={setSms}>
            رسالة نصية
          </DropdownMenuCheckboxItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

مجموعة الخيارات

tsx
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function PanelPositionMenu() {
  const [position, setPosition] = useState("top")

  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>موضع اللوحة</DropdownMenuTrigger>
      <DropdownMenuContent className="w-56">
        <DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
          <DropdownMenuLabel>موضع اللوحة</DropdownMenuLabel>
          <DropdownMenuSeparator />
          <DropdownMenuRadioItem value="top">أعلى</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="bottom">أسفل</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="end">يمين</DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

قائمة فرعية

tsx
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function ShareMenu() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>فتح القائمة</DropdownMenuTrigger>
      <DropdownMenuContent className="w-56">
        <DropdownMenuItem>علامة تبويب جديدة</DropdownMenuItem>
        <DropdownMenuSub>
          <DropdownMenuSubTrigger>مشاركة</DropdownMenuSubTrigger>
          <DropdownMenuSubContent>
            <DropdownMenuItem>البريد الإلكتروني</DropdownMenuItem>
            <DropdownMenuItem>رسالة</DropdownMenuItem>
          </DropdownMenuSubContent>
        </DropdownMenuSub>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}