Skip to content

Commit

Permalink
feat(components): Context Menu component
Browse files Browse the repository at this point in the history
  • Loading branch information
EdenComp committed Oct 20, 2024
1 parent 99f40c2 commit 5cb6c79
Show file tree
Hide file tree
Showing 4 changed files with 765 additions and 1 deletion.
1 change: 1 addition & 0 deletions front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/query-core": "^5.59.0",
Expand Down
109 changes: 109 additions & 0 deletions front/src/components/ui/context-menu.stories.tsx
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;
191 changes: 191 additions & 0 deletions front/src/components/ui/context-menu.tsx
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,
};
Loading

0 comments on commit 5cb6c79

Please sign in to comment.