dropdown-menu.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. "use client"
  2. import * as React from "react"
  3. import { Menu as MenuPrimitive } from "@base-ui/react/menu"
  4. import { Check, ChevronRight, Circle } from "lucide-react"
  5. import { cn } from "@/lib/utils/client"
  6. const DropdownMenu = MenuPrimitive.Root
  7. const DropdownMenuTrigger = React.forwardRef<
  8. HTMLButtonElement,
  9. React.ComponentPropsWithoutRef<typeof MenuPrimitive.Trigger> & { asChild?: boolean }
  10. >(({ asChild, children, ...props }, ref) => {
  11. if (asChild) {
  12. const child = children as React.ReactElement<Record<string, unknown>>
  13. const isNonButton = child?.type === 'label' || child?.type === 'div' || child?.type === 'span' || child?.type === 'a'
  14. return <MenuPrimitive.Trigger ref={ref} render={child} nativeButton={!isNonButton} {...props} />
  15. }
  16. return <MenuPrimitive.Trigger ref={ref} {...props}>{children}</MenuPrimitive.Trigger>
  17. })
  18. DropdownMenuTrigger.displayName = "DropdownMenuTrigger"
  19. const DropdownMenuGroup = MenuPrimitive.Group
  20. const DropdownMenuPortal = MenuPrimitive.Portal
  21. const DropdownMenuSub = MenuPrimitive.SubmenuRoot
  22. const DropdownMenuRadioGroup = MenuPrimitive.RadioGroup
  23. const DropdownMenuSubTrigger = React.forwardRef<
  24. HTMLDivElement,
  25. React.ComponentPropsWithoutRef<typeof MenuPrimitive.SubmenuTrigger> & {
  26. inset?: boolean
  27. }
  28. >(({ className, inset, children, ...props }, ref) => (
  29. <MenuPrimitive.SubmenuTrigger
  30. ref={ref}
  31. className={cn(
  32. "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  33. inset && "pl-8",
  34. className
  35. )}
  36. {...props}
  37. >
  38. {children}
  39. <ChevronRight className="ml-auto" />
  40. </MenuPrimitive.SubmenuTrigger>
  41. ))
  42. DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger"
  43. const DropdownMenuSubContent = React.forwardRef<
  44. HTMLDivElement,
  45. React.ComponentPropsWithoutRef<typeof MenuPrimitive.Popup>
  46. >(({ className, ...props }, ref) => (
  47. <MenuPrimitive.Portal>
  48. <MenuPrimitive.Positioner>
  49. <MenuPrimitive.Popup
  50. ref={ref}
  51. className={cn(
  52. "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[open]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[open]:fade-in-0 data-[closed]:zoom-out-95 data-[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 origin-[--transform-origin]",
  53. className
  54. )}
  55. {...props}
  56. />
  57. </MenuPrimitive.Positioner>
  58. </MenuPrimitive.Portal>
  59. ))
  60. DropdownMenuSubContent.displayName = "DropdownMenuSubContent"
  61. const DropdownMenuContent = React.forwardRef<
  62. HTMLDivElement,
  63. React.ComponentPropsWithoutRef<typeof MenuPrimitive.Popup> & {
  64. sideOffset?: number
  65. side?: "top" | "bottom" | "left" | "right"
  66. align?: "start" | "center" | "end"
  67. }
  68. >(({ className, sideOffset = 4, side, align, ...props }, ref) => (
  69. <MenuPrimitive.Portal>
  70. <MenuPrimitive.Positioner sideOffset={sideOffset} side={side} align={align}>
  71. <MenuPrimitive.Popup
  72. ref={ref}
  73. className={cn(
  74. "z-50 max-h-[var(--available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
  75. "data-[open]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[open]:fade-in-0 data-[closed]:zoom-out-95 data-[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 origin-[--transform-origin]",
  76. className
  77. )}
  78. {...props}
  79. />
  80. </MenuPrimitive.Positioner>
  81. </MenuPrimitive.Portal>
  82. ))
  83. DropdownMenuContent.displayName = "DropdownMenuContent"
  84. const DropdownMenuItem = React.forwardRef<
  85. HTMLDivElement,
  86. React.ComponentPropsWithoutRef<typeof MenuPrimitive.Item> & {
  87. inset?: boolean
  88. asChild?: boolean
  89. }
  90. >(({ className, inset, asChild, children, ...props }, ref) => {
  91. if (asChild) {
  92. return (
  93. <MenuPrimitive.Item
  94. ref={ref}
  95. className={cn(
  96. "relative flex cursor-pointer select-none items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0",
  97. inset && "pl-8",
  98. className
  99. )}
  100. render={children as React.ReactElement}
  101. {...props}
  102. />
  103. )
  104. }
  105. return (
  106. <MenuPrimitive.Item
  107. ref={ref}
  108. className={cn(
  109. "relative flex cursor-pointer select-none items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0",
  110. inset && "pl-8",
  111. className
  112. )}
  113. {...props}
  114. >{children}</MenuPrimitive.Item>
  115. )
  116. })
  117. DropdownMenuItem.displayName = "DropdownMenuItem"
  118. const DropdownMenuCheckboxItem = React.forwardRef<
  119. HTMLDivElement,
  120. React.ComponentPropsWithoutRef<typeof MenuPrimitive.CheckboxItem>
  121. >(({ className, children, checked, ...props }, ref) => (
  122. <MenuPrimitive.CheckboxItem
  123. ref={ref}
  124. className={cn(
  125. "relative flex cursor-pointer 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",
  126. className
  127. )}
  128. checked={checked}
  129. {...props}
  130. >
  131. <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
  132. <MenuPrimitive.CheckboxItemIndicator>
  133. <Check className="h-4 w-4" />
  134. </MenuPrimitive.CheckboxItemIndicator>
  135. </span>
  136. {children}
  137. </MenuPrimitive.CheckboxItem>
  138. ))
  139. DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem"
  140. const DropdownMenuRadioItem = React.forwardRef<
  141. HTMLDivElement,
  142. React.ComponentPropsWithoutRef<typeof MenuPrimitive.RadioItem>
  143. >(({ className, children, ...props }, ref) => (
  144. <MenuPrimitive.RadioItem
  145. ref={ref}
  146. className={cn(
  147. "relative flex cursor-pointer 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",
  148. className
  149. )}
  150. {...props}
  151. >
  152. <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
  153. <MenuPrimitive.RadioItemIndicator>
  154. <Circle className="h-2 w-2 fill-current" />
  155. </MenuPrimitive.RadioItemIndicator>
  156. </span>
  157. {children}
  158. </MenuPrimitive.RadioItem>
  159. ))
  160. DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem"
  161. const DropdownMenuLabel = React.forwardRef<
  162. HTMLDivElement,
  163. React.HTMLAttributes<HTMLDivElement> & {
  164. inset?: boolean
  165. }
  166. >(({ className, inset, ...props }, ref) => (
  167. <div
  168. ref={ref}
  169. className={cn(
  170. "px-2 py-1.5 text-sm font-semibold",
  171. inset && "pl-8",
  172. className
  173. )}
  174. {...props}
  175. />
  176. ))
  177. DropdownMenuLabel.displayName = "DropdownMenuLabel"
  178. const DropdownMenuSeparator = React.forwardRef<
  179. HTMLDivElement,
  180. React.HTMLAttributes<HTMLDivElement>
  181. >(({ className, ...props }, ref) => (
  182. <div
  183. ref={ref}
  184. role="separator"
  185. className={cn("-mx-1 my-1 h-px bg-muted", className)}
  186. {...props}
  187. />
  188. ))
  189. DropdownMenuSeparator.displayName = "DropdownMenuSeparator"
  190. const DropdownMenuShortcut = ({
  191. className,
  192. ...props
  193. }: React.HTMLAttributes<HTMLSpanElement>) => {
  194. return (
  195. <span
  196. className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
  197. {...props}
  198. />
  199. )
  200. }
  201. DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
  202. export {
  203. DropdownMenu,
  204. DropdownMenuTrigger,
  205. DropdownMenuContent,
  206. DropdownMenuItem,
  207. DropdownMenuCheckboxItem,
  208. DropdownMenuRadioItem,
  209. DropdownMenuLabel,
  210. DropdownMenuSeparator,
  211. DropdownMenuShortcut,
  212. DropdownMenuGroup,
  213. DropdownMenuPortal,
  214. DropdownMenuSub,
  215. DropdownMenuSubContent,
  216. DropdownMenuSubTrigger,
  217. DropdownMenuRadioGroup,
  218. }