-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(components): Context Menu component
- Loading branch information
Showing
4 changed files
with
765 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { Meta, StoryObj } from "@storybook/react"; | ||
|
||
import { | ||
ContextMenu, | ||
ContextMenuCheckboxItem, | ||
ContextMenuContent, | ||
ContextMenuItem, | ||
ContextMenuLabel, | ||
ContextMenuRadioGroup, | ||
ContextMenuRadioItem, | ||
ContextMenuSeparator, | ||
ContextMenuShortcut, | ||
ContextMenuSub, | ||
ContextMenuSubContent, | ||
ContextMenuSubTrigger, | ||
ContextMenuTrigger, | ||
} from "@/components/ui/context-menu"; | ||
|
||
const meta: Meta = { | ||
component: ContextMenu, | ||
title: "Context Menu", | ||
args: { | ||
className: "", | ||
}, | ||
argTypes: { | ||
className: { | ||
type: "string", | ||
control: "text", | ||
}, | ||
}, | ||
}; | ||
type Story = StoryObj; | ||
|
||
const Render = (args: Meta) => ( | ||
<ContextMenu {...args}> | ||
<ContextMenuTrigger className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-primary text-sm text-primary"> | ||
Right click here | ||
</ContextMenuTrigger> | ||
<ContextMenuContent className="w-64"> | ||
<ContextMenuItem inset> | ||
Back | ||
<ContextMenuShortcut>⌘[</ContextMenuShortcut> | ||
</ContextMenuItem> | ||
<ContextMenuItem inset disabled> | ||
Forward | ||
<ContextMenuShortcut>⌘]</ContextMenuShortcut> | ||
</ContextMenuItem> | ||
<ContextMenuItem inset> | ||
Reload | ||
<ContextMenuShortcut>⌘R</ContextMenuShortcut> | ||
</ContextMenuItem> | ||
<ContextMenuSub> | ||
<ContextMenuSubTrigger inset>More Tools</ContextMenuSubTrigger> | ||
<ContextMenuSubContent className="w-48"> | ||
<ContextMenuItem> | ||
Save Page As... | ||
<ContextMenuShortcut>⇧⌘S</ContextMenuShortcut> | ||
</ContextMenuItem> | ||
<ContextMenuItem>Create Shortcut...</ContextMenuItem> | ||
<ContextMenuItem>Name Window...</ContextMenuItem> | ||
<ContextMenuSeparator /> | ||
<ContextMenuItem>Developer Tools</ContextMenuItem> | ||
</ContextMenuSubContent> | ||
</ContextMenuSub> | ||
<ContextMenuSeparator /> | ||
<ContextMenuCheckboxItem checked> | ||
Show Bookmarks Bar | ||
<ContextMenuShortcut>⌘⇧B</ContextMenuShortcut> | ||
</ContextMenuCheckboxItem> | ||
<ContextMenuCheckboxItem>Show Full URLs</ContextMenuCheckboxItem> | ||
<ContextMenuSeparator /> | ||
<ContextMenuRadioGroup value="right"> | ||
<ContextMenuLabel inset>Controls</ContextMenuLabel> | ||
<ContextMenuRadioItem value="left">Left click</ContextMenuRadioItem> | ||
<ContextMenuRadioItem value="right">Right click</ContextMenuRadioItem> | ||
</ContextMenuRadioGroup> | ||
</ContextMenuContent> | ||
</ContextMenu> | ||
); | ||
|
||
export const Default: Story = { | ||
args: { | ||
className: "", | ||
}, | ||
parameters: { | ||
backgrounds: { | ||
default: "light", | ||
}, | ||
}, | ||
render: Render, | ||
}; | ||
|
||
export const DarkMode: Story = { | ||
args: { | ||
className: "", | ||
}, | ||
parameters: { | ||
backgrounds: { | ||
default: "dark", | ||
}, | ||
}, | ||
render: (args) => ( | ||
<div className="dark"> | ||
<Render {...args} /> | ||
</div> | ||
), | ||
}; | ||
|
||
export default meta; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
"use client"; | ||
|
||
import { | ||
CheckboxItem, | ||
Content, | ||
Group, | ||
Item, | ||
ItemIndicator, | ||
Label, | ||
Portal, | ||
RadioGroup, | ||
RadioItem, | ||
Root, | ||
Separator, | ||
Sub, | ||
SubContent, | ||
SubTrigger, | ||
Trigger, | ||
} from "@radix-ui/react-context-menu"; | ||
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"; | ||
import React from "react"; | ||
|
||
import { cn } from "@/lib/utils"; | ||
|
||
const ContextMenu = Root; | ||
const ContextMenuTrigger = Trigger; | ||
const ContextMenuGroup = Group; | ||
const ContextMenuPortal = Portal; | ||
const ContextMenuSub = Sub; | ||
const ContextMenuRadioGroup = RadioGroup; | ||
|
||
const ContextMenuSubTrigger = React.forwardRef< | ||
React.ElementRef<typeof SubTrigger>, | ||
React.ComponentPropsWithoutRef<typeof SubTrigger> & { | ||
inset?: boolean; | ||
} | ||
>(({ className, inset, children, ...props }, ref) => ( | ||
<SubTrigger | ||
ref={ref} | ||
className={cn( | ||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", | ||
inset && "pl-8", | ||
className, | ||
)} | ||
{...props} | ||
> | ||
{children} | ||
<ChevronRightIcon className="ml-auto h-4 w-4" /> | ||
</SubTrigger> | ||
)); | ||
ContextMenuSubTrigger.displayName = SubTrigger.displayName; | ||
|
||
const ContextMenuSubContent = React.forwardRef< | ||
React.ElementRef<typeof SubContent>, | ||
React.ComponentPropsWithoutRef<typeof SubContent> | ||
>(({ className, ...props }, ref) => ( | ||
<SubContent | ||
ref={ref} | ||
className={cn( | ||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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", | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
)); | ||
ContextMenuSubContent.displayName = SubContent.displayName; | ||
|
||
const ContextMenuContent = React.forwardRef< | ||
React.ElementRef<typeof Content>, | ||
React.ComponentPropsWithoutRef<typeof Content> | ||
>(({ className, ...props }, ref) => ( | ||
<Portal> | ||
<Content | ||
ref={ref} | ||
className={cn( | ||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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", | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
</Portal> | ||
)); | ||
ContextMenuContent.displayName = Content.displayName; | ||
|
||
const ContextMenuItem = React.forwardRef< | ||
React.ElementRef<typeof Item>, | ||
React.ComponentPropsWithoutRef<typeof Item> & { | ||
inset?: boolean; | ||
} | ||
>(({ className, inset, ...props }, ref) => ( | ||
<Item | ||
ref={ref} | ||
className={cn( | ||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||
inset && "pl-8", | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
)); | ||
ContextMenuItem.displayName = Item.displayName; | ||
|
||
const ContextMenuCheckboxItem = React.forwardRef< | ||
React.ElementRef<typeof CheckboxItem>, | ||
React.ComponentPropsWithoutRef<typeof CheckboxItem> | ||
>(({ className, children, checked, ...props }, ref) => ( | ||
<CheckboxItem | ||
ref={ref} | ||
className={cn( | ||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||
className, | ||
)} | ||
checked={checked} | ||
{...props} | ||
> | ||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||
<ItemIndicator> | ||
<CheckIcon className="h-4 w-4" /> | ||
</ItemIndicator> | ||
</span> | ||
{children} | ||
</CheckboxItem> | ||
)); | ||
ContextMenuCheckboxItem.displayName = CheckboxItem.displayName; | ||
|
||
const ContextMenuRadioItem = React.forwardRef< | ||
React.ElementRef<typeof RadioItem>, | ||
React.ComponentPropsWithoutRef<typeof RadioItem> | ||
>(({ className, children, ...props }, ref) => ( | ||
<RadioItem | ||
ref={ref} | ||
className={cn( | ||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||
className, | ||
)} | ||
{...props} | ||
> | ||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||
<ItemIndicator> | ||
<DotFilledIcon className="h-4 w-4 fill-current" /> | ||
</ItemIndicator> | ||
</span> | ||
{children} | ||
</RadioItem> | ||
)); | ||
ContextMenuRadioItem.displayName = RadioItem.displayName; | ||
|
||
const ContextMenuLabel = React.forwardRef< | ||
React.ElementRef<typeof Label>, | ||
React.ComponentPropsWithoutRef<typeof Label> & { | ||
inset?: boolean; | ||
} | ||
>(({ className, inset, ...props }, ref) => ( | ||
<Label | ||
ref={ref} | ||
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)} | ||
{...props} | ||
/> | ||
)); | ||
ContextMenuLabel.displayName = Label.displayName; | ||
|
||
const ContextMenuSeparator = React.forwardRef< | ||
React.ElementRef<typeof Separator>, | ||
React.ComponentPropsWithoutRef<typeof Separator> | ||
>(({ className, ...props }, ref) => ( | ||
<Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} /> | ||
)); | ||
ContextMenuSeparator.displayName = Separator.displayName; | ||
|
||
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { | ||
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />; | ||
}; | ||
ContextMenuShortcut.displayName = "ContextMenuShortcut"; | ||
|
||
export { | ||
ContextMenu, | ||
ContextMenuTrigger, | ||
ContextMenuContent, | ||
ContextMenuItem, | ||
ContextMenuCheckboxItem, | ||
ContextMenuRadioItem, | ||
ContextMenuLabel, | ||
ContextMenuSeparator, | ||
ContextMenuShortcut, | ||
ContextMenuGroup, | ||
ContextMenuPortal, | ||
ContextMenuSub, | ||
ContextMenuSubContent, | ||
ContextMenuSubTrigger, | ||
ContextMenuRadioGroup, | ||
}; |
Oops, something went wrong.