قائمة سياقية
قائمة تظهر عند النقر بزر الماوس الأيمن على صفوف الملفات، واللوحات، وشبكات البيانات — إجراءات سياقية بتصميم «نيوبروتاليستي» يتميز بالحدود والظلال.
import {
ContextMenu,
ContextMenuCheckboxItem,تفتح قائمة السياق (context menu) قائمة عائمة من الإجراءات عند المؤشر بالنقر الأيمن (أو الضغط الطويل على اللمس)، مع دعم القوائم الفرعية وعناصر checkbox وradio واختصارات لوحة المفاتيح. مبني على مكوّن Base UI Context Menu ومصمَّم وفق وصفة النيوبروتالية: حدود سميكة، وظلال حادة، وخط عريض.
استخدمه عندما:
- إجراءات الملفات والصفوف — إعادة تسمية، تكرار، حذف على مدخلات مدير الملفات أو صفوف الجداول دون إغراق كل صف بأزرار.
- أسطح اللوحات — سبورات بيضاء وأدوات تصميم ومحررات حيث تعتمد الإجراءات على ما نُقر عليه.
- شبكات البيانات — عمليات لكل خلية مثل نسخ القيمة أو التصفية أو تثبيت العمود، قرب البيانات التي تعمل عليها.
التثبيت#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/context-menu.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/context-menu.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/context-menu.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/context-menu.json
ثبّت التبعيات التالية:
pnpm add @base-ui/reactnpm install @base-ui/reactyarn add @base-ui/reactbun add @base-ui/react
انسخ الكود التالي والصقه في مشروعك.
"use client"
import * as React from "react"
import { CheckIcon, ChevronRightIcon } from "lucide-react"
import { ContextMenu as ContextMenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger
data-slot="context-menu-trigger"
className={cn("select-none", className)}
{...props}
/>
)
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn("z-50 max-h-(--radix-context-menu-content-available-height) min-w-36 origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded border-2 bg-popover p-1 text-popover-foreground shadow-md duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/context-menu-item relative flex cursor-default items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 focus:*:[svg]:text-accent-foreground data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="cn-rtl-flip ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn("z-50 min-w-32 origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded border-2 bg-popover p-1 text-popover-foreground shadow-md duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute right-2">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioItem({
className,
children,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute right-2">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
className
)}
{...props}
/>
)
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
حدّث مسارات الاستيراد لتطابق إعداد مشروعك.
الاستخدام#
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu"<ContextMenu>
<ContextMenuTrigger>Right click here</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Profile</ContextMenuItem>
<ContextMenuItem>Billing</ContextMenuItem>
<ContextMenuItem>Team</ContextMenuItem>
<ContextMenuItem>Subscription</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>التركيب#
استخدم التركيب التالي لبناء ContextMenu:
ContextMenu
├── ContextMenuTrigger
└── ContextMenuContent
├── ContextMenuGroup
│ ├── ContextMenuLabel
│ ├── ContextMenuItem
│ └── ContextMenuItem
├── ContextMenuSeparator
├── ContextMenuGroup
│ ├── ContextMenuLabel
│ ├── ContextMenuCheckboxItem
│ └── ContextMenuCheckboxItem
├── ContextMenuSeparator
├── ContextMenuGroup
│ ├── ContextMenuLabel
│ └── ContextMenuRadioGroup
│ ├── ContextMenuRadioItem
│ └── ContextMenuRadioItem
└── ContextMenuSub
├── ContextMenuSubTrigger
└── ContextMenuSubContent
└── ContextMenuGroup
├── ContextMenuItem
└── ContextMenuItemأمثلة#
أساسي#
قائمة سياق بسيطة مع بضعة إجراءات.
import {
ContextMenu,
ContextMenuContent,القائمة الفرعية#
استخدم ContextMenuSub لتضمين إجراءات ثانوية.
import {
ContextMenu,
ContextMenuContent,الاختصارات#
أضف ContextMenuShortcut لعرض تلميحات لوحة المفاتيح.
import {
ContextMenu,
ContextMenuContent,المجموعات#
جمّع الإجراءات المترابطة وافصل بينها بفواصل.
import {
ContextMenu,
ContextMenuContent,الأيقونات#
ادمج الأيقونات مع التسميات لمسح سريع بالنظر.
import {
ClipboardPasteIcon,
CopyIcon,مربّعات الاختيار#
استخدم ContextMenuCheckboxItem لعناصر التبديل.
import {
ContextMenu,
ContextMenuCheckboxItem,أزرار الاختيار#
استخدم ContextMenuRadioItem للخيارات الحصرية.
"use client"
import * as React from "react"الإجراء الهدّام#
استخدم variant="destructive" لتنسيق عنصر القائمة كعنصر هدّام.
import { ArchiveIcon, PencilIcon, ShareIcon, TrashIcon } from "lucide-react"
import {الجوانب#
تحكّم في موضع القائمة الفرعية باستخدام الخاصيتين side وalign.
import {
ContextMenu,
ContextMenuContent,RTL#
لتفعيل RTL في Neobrutalism، راجع دليل إعداد RTL.
"use client"
import * as React from "react"استخدم side="inline-end" لوضع القائمة على الجانب الأيمن المنطقي للمُشغِّل.
<ContextMenu>
<ContextMenuTrigger>Right click here</ContextMenuTrigger>
<ContextMenuContent side="inline-end">
<ContextMenuItem>Profile</ContextMenuItem>
<ContextMenuItem>Billing</ContextMenuItem>
<ContextMenuItem>Team</ContextMenuItem>
<ContextMenuItem>Subscription</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>إمكانية الوصول#
تتبع قائمة السياق نمط WAI-ARIA Menu: تعرض المنبثقة role="menu" مع أبناء menuitem وmenuitemcheckbox وmenuitemradio، وينتقل التركيز إلى القائمة عند الفتح ويعود إلى منطقة المشغّل عند الإغلاق. تُفتح من حدث المتصفح contextmenu، لذا يعمل Shift + F10 (أو مفتاح القائمة المخصّص) متى كانت منطقة المشغّل قابلة للتركيز — أضف tabIndex={0} لابن المشغّل إن لم يكن كذلك أصليًا.
تفاعلات لوحة المفاتيح:
| المفتاح | الإجراء |
|---|---|
Shift + F10 / مفتاح القائمة | فتح القائمة عندما يكون تركيز منطقة المشغّل |
ArrowDown / ArrowUp | نقل التركيز إلى العنصر التالي / السابق |
ArrowRight / ArrowLeft | فتح / إغلاق قائمة فرعية (معكوس في RTL) |
Enter / Space | تفعيل العنصر المركَّز |
Esc | إغلاق القائمة |
| كتابة حرف | نقل التركيز إلى العنصر المطابق التالي (typeahead) |
مرجع API#
لمزيد من المعلومات، راجع وثائق Base UI.