Skip to content

Commit

Permalink
feat: add dropdown menu
Browse files Browse the repository at this point in the history
  • Loading branch information
Minozzzi committed Mar 27, 2024
1 parent 38e3b9d commit 97b42da
Show file tree
Hide file tree
Showing 13 changed files with 533 additions and 29 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"storybook:build": "storybook build -o docs-build"
},
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"axios": "^1.4.0",
Expand Down
218 changes: 189 additions & 29 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions src/presentation/components/ui/dropdown/checkboxItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'

import {
CheckboxItem as CheckboxItemRadix,
ItemIndicator
} from '@radix-ui/react-dropdown-menu'
import { Check } from 'lucide-react'

import { cn } from '@/main/utils'

export const CheckboxItem = React.forwardRef<
React.ElementRef<typeof CheckboxItemRadix>,
React.ComponentPropsWithoutRef<typeof CheckboxItemRadix>
>(({ className, children, checked, ...props }, ref) => (
<CheckboxItemRadix
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 transition-colors 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>
<Check className="h-4 w-4" />
</ItemIndicator>
</span>
{children}
</CheckboxItemRadix>
))

CheckboxItem.displayName = CheckboxItemRadix.displayName
24 changes: 24 additions & 0 deletions src/presentation/components/ui/dropdown/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'

import { Content as ContentRadix, Portal } from '@radix-ui/react-dropdown-menu'

import { cn } from '@/main/utils'

export const Content = React.forwardRef<
React.ElementRef<typeof ContentRadix>,
React.ComponentPropsWithoutRef<typeof ContentRadix>
>(({ className, sideOffset = 4, ...props }, ref) => (
<Portal>
<ContentRadix
ref={ref}
sideOffset={sideOffset}
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>
))

Content.displayName = ContentRadix.displayName
90 changes: 90 additions & 0 deletions src/presentation/components/ui/dropdown/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useState } from 'react'

import { DropdownMenuCheckboxItemProps } from '@radix-ui/react-dropdown-menu'
import { Meta, StoryFn } from '@storybook/react/'

import { Button } from '../button'

import { DropdownMenu } from '.'

export default {
title: 'Components/UI/DropdownMenu'
} as Meta

type Checked = DropdownMenuCheckboxItemProps['checked']

const TemplateDefault: StoryFn = () => (
<DropdownMenu.Root>
<DropdownMenu.Trigger>Open</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>My Account</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item>Profile</DropdownMenu.Item>
<DropdownMenu.Item>Billing</DropdownMenu.Item>
<DropdownMenu.Item>Team</DropdownMenu.Item>
<DropdownMenu.Item>Subscription</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
)

const TemplateCheckbox: StoryFn = () => {
const [showStatusBar, setShowStatusBar] = useState<Checked>(true)
const [showActivityBar, setShowActivityBar] = useState<Checked>(false)
const [showPanel, setShowPanel] = useState<Checked>(false)

return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<Button variant="outline">Open</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="w-56">
<DropdownMenu.Label>Appearance</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem
checked={showStatusBar}
onCheckedChange={setShowStatusBar}
>
Status Bar
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={showActivityBar}
onCheckedChange={setShowActivityBar}
disabled
>
Activity Bar
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={showPanel}
onCheckedChange={setShowPanel}
>
Panel
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Root>
)
}

const TemplateRadioGroup: StoryFn = () => {
const [position, setPosition] = useState('bottom')

return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<Button variant="outline">Open</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="w-56">
<DropdownMenu.Label>Panel Position</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.RadioGroup value={position} onValueChange={setPosition}>
<DropdownMenu.RadioItem value="top">Top</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="bottom">Bottom</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="right">Right</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
)
}

export const Default = TemplateDefault.bind({})
export const WithCheckbox = TemplateCheckbox.bind({})
export const WithRadioGroup = TemplateRadioGroup.bind({})
36 changes: 36 additions & 0 deletions src/presentation/components/ui/dropdown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
Group,
Portal,
RadioGroup,
Root,
Sub,
Trigger
} from '@radix-ui/react-dropdown-menu'

import { CheckboxItem } from './checkboxItem'
import { Content } from './content'
import { Item } from './item'
import { Label } from './label'
import { RadioItem } from './radioItem'
import { Separator } from './separator'
import { Shortcut } from './shortcut'
import { SubContent } from './subContent'
import { SubTrigger } from './subTrigger'

export const DropdownMenu = {
Root,
Trigger,
Content,
Item,
CheckboxItem,
RadioItem,
Label,
Separator,
Shortcut,
Group,
Portal,
Sub,
SubContent,
SubTrigger,
RadioGroup
}
24 changes: 24 additions & 0 deletions src/presentation/components/ui/dropdown/item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'

import { Item as ItemRadix } from '@radix-ui/react-dropdown-menu'

import { cn } from '@/main/utils'

export const Item = React.forwardRef<
React.ElementRef<typeof ItemRadix>,
React.ComponentPropsWithoutRef<typeof ItemRadix> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ItemRadix
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className
)}
{...props}
/>
))

Item.displayName = ItemRadix.displayName
24 changes: 24 additions & 0 deletions src/presentation/components/ui/dropdown/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'

import { Label as LabelRadix } from '@radix-ui/react-dropdown-menu'

import { cn } from '@/main/utils'

export const Label = React.forwardRef<
React.ElementRef<typeof LabelRadix>,
React.ComponentPropsWithoutRef<typeof LabelRadix> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<LabelRadix
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
))

Label.displayName = LabelRadix.displayName
32 changes: 32 additions & 0 deletions src/presentation/components/ui/dropdown/radioItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react'

import {
ItemIndicator,
RadioItem as RadioItemRadix
} from '@radix-ui/react-dropdown-menu'
import { Circle } from 'lucide-react'

import { cn } from '@/main/utils'

export const RadioItem = React.forwardRef<
React.ElementRef<typeof RadioItemRadix>,
React.ComponentPropsWithoutRef<typeof RadioItemRadix>
>(({ className, children, ...props }, ref) => (
<RadioItemRadix
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 transition-colors 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>
<Circle className="h-2 w-2 fill-current" />
</ItemIndicator>
</span>
{children}
</RadioItemRadix>
))

RadioItem.displayName = RadioItemRadix.displayName
18 changes: 18 additions & 0 deletions src/presentation/components/ui/dropdown/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'

import { Separator as SeparatorRadix } from '@radix-ui/react-dropdown-menu'

import { cn } from '@/main/utils'

export const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorRadix>,
React.ComponentPropsWithoutRef<typeof SeparatorRadix>
>(({ className, ...props }, ref) => (
<SeparatorRadix
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))

Separator.displayName = SeparatorRadix.displayName
13 changes: 13 additions & 0 deletions src/presentation/components/ui/dropdown/shortcut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { cn } from '@/main/utils'

export const Shortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
)

Shortcut.displayName = 'DropdownMenuShortcut'
21 changes: 21 additions & 0 deletions src/presentation/components/ui/dropdown/subContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react'

import { SubContent as SubContentRadix } from '@radix-ui/react-dropdown-menu'

import { cn } from '@/main/utils'

export const SubContent = React.forwardRef<
React.ElementRef<typeof SubContentRadix>,
React.ComponentPropsWithoutRef<typeof SubContentRadix>
>(({ className, ...props }, ref) => (
<SubContentRadix
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}
/>
))

SubContent.displayName = SubContentRadix.displayName
28 changes: 28 additions & 0 deletions src/presentation/components/ui/dropdown/subTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'

import { SubTrigger as SubTriggerRadix } from '@radix-ui/react-dropdown-menu'
import { ChevronRight } from 'lucide-react'

import { cn } from '@/main/utils'

export const SubTrigger = React.forwardRef<
React.ElementRef<typeof SubTriggerRadix>,
React.ComponentPropsWithoutRef<typeof SubTriggerRadix> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<SubTriggerRadix
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 data-[state=open]:bg-accent',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</SubTriggerRadix>
))

SubTrigger.displayName = SubTriggerRadix.displayName

0 comments on commit 97b42da

Please sign in to comment.