Menubar
A simplified wrapper for Shadcn Menubar with flexible option handling and automatic type conversion
Installation
npx shadcn@latest add @glrk-ui/menubarIf you haven't set up the prerequisites yet, check out Prerequest section.
Update following code in ui/menubar.tsx
function MenubarCheckboxItem({
className,
children,
checked,
indicatorAt = "right",
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem> & { indicatorAt?: indicatorAtT }) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
indicatorAt === "right" ? "pr-8 pl-2" : "pr-2 pl-8",
)}
checked={checked}
{...props}
>
<span className={cn("pointer-events-none absolute flex size-3.5 items-center justify-center", indicatorAt === "right" ? "right-2" : "left-2")}>
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
function MenubarRadioItem({
className,
children,
indicatorAt = "right",
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem> & { indicatorAt?: indicatorAtT }) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
indicatorAt === "right" ? "pr-8 pl-2" : "pr-2 pl-8",
)}
{...props}
>
<span className={cn("pointer-events-none absolute flex size-3.5 items-center justify-center", indicatorAt === "right" ? "right-2" : "left-2")}>
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
)
}Create new file name types/menu.d.ts.
type menuOptionT = allowedPrimitiveT | optionT | (optionT & {
variant?: "default" | "destructive"
shortcut?: string
disabled?: boolean
})
type menuGroupT = {
group: string
options: menuOptionT[]
className?: string
groupLabelCls?: string
}
type subMenuT = {
submenu: string
options: (menuOptionT | menuGroupT)[]
triggerCls?: string
contentCls?: string
}
type menuOptionsT = (menuOptionT | menuGroupT | subMenuT)[]
type menuInputOptionT = allowedPrimitiveT | optionT | (optionT & {
disabled?: boolean
})
type menuInputGroupT = {
group: string
options: menuInputOptionT[]
className?: string
groupLabelCls?: string
}
type inputSubMenuT = {
submenu: string
options: (menuInputOptionT | menuInputGroupT)[]
triggerCls?: string
contentCls?: string
}
type menuInputOptionsT = (menuInputOptionT | menuInputGroupT | inputSubMenuT)[]Add following lines to lib/utils.ts
export const isGroupMenu = optionTypeChecker<menuGroupT>("group")
export const isSubMenu = optionTypeChecker<subMenuT>("submenu")
export const isInputGroupMenu = optionTypeChecker<menuInputGroupT>("group")
export const isInputSubMenu = optionTypeChecker<inputSubMenuT>("submenu")Create new file name ui/menubar-wrapper.tsx.
"use client"
import { useState } from "react"
import {
cn, getKey, getLabel, getValue,
isSeparator,
isSubMenu,
isGroupMenu,
isInputSubMenu,
isInputGroupMenu,
parseAllowedPrimitive,
} from "@/lib/utils"
import {
Menubar,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarLabel,
MenubarItem,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
MenubarMenu,
} from "@/components/ui/menubar"
type commomClsT = {
itemCls?: string
groupCls?: string
groupLabelCls?: string
}
type commonCheckboxProps = {
checked?: allowedPrimitiveT[]
indicatorAt?: indicatorAtT
onCheckedChange?: (value: allowedPrimitiveT, checked: boolean) => void
}
type commonRadioProps = {
value?: allowedPrimitiveT
indicatorAt?: indicatorAtT
onValueChange?: (value: allowedPrimitiveT) => void
}
type commonInner = commomClsT & {
trigger: React.ReactNode
contentProps?: React.ComponentProps<typeof MenubarContent>
}
type menubarBaseT = commomClsT & {
key: string
trigger: React.ReactNode
contentProps?: React.ComponentProps<typeof MenubarContent>
}
type menubarOptionsT = (menubarBaseT & {
options: menuOptionsT
onSelect?: (value: allowedPrimitiveT) => void
})[]
type menubarInputOptionT = menubarBaseT & {
options: menuInputOptionsT
}
type menubarCheckboxOptionsT = (menubarInputOptionT & commonCheckboxProps)[]
type menubarRadioOptionsT = (menubarInputOptionT & commonRadioProps)[]
type commonWrapT = commomClsT & Omit<React.ComponentProps<typeof Menubar>, "children" | "asChild" | "value"> & {
contentProps?: React.ComponentProps<typeof MenubarContent>
}
// -------
type itemProps = {
option: menuOptionT
className?: string
onSelect?: () => void
}
function Item({
option,
className,
onSelect
}: itemProps) {
const value = getValue(option)
if (isSeparator(value)) return <MenubarSeparator className={className} />
const label = getLabel(option)
const opt: any = typeof option === "object" ? option : {}
const shortcut = opt?.shortcut
return (
<MenubarItem
{...opt}
onSelect={onSelect}
className={cn(className, opt?.className)}
>
{label}
{shortcut && <MenubarShortcut>{shortcut}</MenubarShortcut>}
</MenubarItem>
)
}
type checkboxItemProps = {
option: menuInputOptionT
className?: string
checked?: boolean
onCheckedChange?: (checked: boolean) => void
indicatorAt?: indicatorAtT
}
function CheckboxItem({
option,
className,
checked = false,
indicatorAt,
onCheckedChange = () => { }
}: checkboxItemProps) {
const value = getValue(option)
if (isSeparator(value)) return <MenubarSeparator className={className} />
const label = getLabel(option)
const disabled = (option as any)?.disabled
return (
<MenubarCheckboxItem
checked={checked}
disabled={disabled}
className={className}
indicatorAt={indicatorAt}
onCheckedChange={onCheckedChange}
>
{label}
</MenubarCheckboxItem>
)
}
type radioItemProps = {
option: menuInputOptionT
className?: string
indicatorAt?: indicatorAtT
}
function RadioItem({
option,
className,
indicatorAt
}: radioItemProps) {
const value = getValue(option)
if (isSeparator(value)) return <MenubarSeparator className={className} />
const label = getLabel(option)
const disabled = (option as any)?.disabled
return (
<MenubarRadioItem
value={`${value}`}
disabled={disabled}
className={className}
indicatorAt={indicatorAt}
>
{label}
</MenubarRadioItem>
)
}
type SubMenuProps = commomClsT & {
submenu: subMenuT
onSelect?: (value: allowedPrimitiveT) => void
}
function SubMenu({
submenu,
itemCls,
groupCls,
groupLabelCls,
onSelect
}: SubMenuProps) {
return (
<MenubarSub>
<MenubarSubTrigger className={submenu.triggerCls}>
{submenu.submenu}
</MenubarSubTrigger>
<MenubarSubContent className={submenu.contentCls}>
{submenu.options.map((option, i) => {
if (isGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => (
<Item
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
onSelect={() => onSelect?.(getValue(grOpt))}
/>
))}
</MenubarGroup>
)
}
if (isSubMenu(option)) {
return (
<SubMenu
key={getKey(option, i)}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
onSelect={onSelect}
/>
)
}
return (
<Item
key={getKey(option, i)}
option={option}
className={itemCls}
onSelect={() => onSelect?.(getValue(option))}
/>
)
})}
</MenubarSubContent>
</MenubarSub>
)
}
type CheckboxSubMenuProps = commomClsT & commonCheckboxProps & {
submenu: inputSubMenuT
}
function CheckboxSubMenu({
submenu,
itemCls,
groupCls,
groupLabelCls,
checked = [],
indicatorAt,
onCheckedChange = () => { }
}: CheckboxSubMenuProps) {
return (
<MenubarSub>
<MenubarSubTrigger className={submenu.triggerCls}>
{submenu.submenu}
</MenubarSubTrigger>
<MenubarSubContent className={submenu.contentCls}>
{submenu.options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => {
const v = getValue(grOpt)
return (
<CheckboxItem
key={getKey(grOpt, j)}
option={grOpt}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarGroup>
)
}
if (isInputSubMenu(option)) {
return (
<CheckboxSubMenu
key={option.submenu}
submenu={option}
checked={checked}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onCheckedChange={onCheckedChange}
/>
)
}
const v = getValue(option)
return (
<CheckboxItem
key={getKey(option, i)}
option={option}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarSubContent>
</MenubarSub>
)
}
type RadioSubMenuProps = commomClsT & commonRadioProps & {
submenu: inputSubMenuT
}
function RadioSubMenu({
submenu,
itemCls,
groupCls,
groupLabelCls,
value = "",
indicatorAt,
onValueChange = () => { }
}: RadioSubMenuProps) {
return (
<MenubarSub>
<MenubarSubTrigger className={submenu.triggerCls}>
{submenu.submenu}
</MenubarSubTrigger>
<MenubarSubContent className={submenu.contentCls}>
<MenubarRadioGroup value={`${value}`} onValueChange={onValueChange}>
{submenu.options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => (
<RadioItem
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
indicatorAt={indicatorAt}
/>
))}
</MenubarGroup>
)
}
if (isInputSubMenu(option)) {
return (
<RadioSubMenu
key={option.submenu}
value={value}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onValueChange={onValueChange}
/>
)
}
return (
<RadioItem
key={getKey(option, i)}
option={option}
className={itemCls}
indicatorAt={indicatorAt}
/>
)
})}
</MenubarRadioGroup>
</MenubarSubContent>
</MenubarSub>
)
}
type wrapperInner = commonInner & {
options: menuOptionsT
onSelect?: (value: allowedPrimitiveT) => void
}
function MenubarWrapperInner({
trigger,
options,
itemCls,
groupCls,
groupLabelCls,
contentProps,
onSelect
}: wrapperInner) {
return (
<MenubarMenu>
<MenubarTrigger asChild={typeof trigger !== "string"}>
{trigger}
</MenubarTrigger>
<MenubarContent {...contentProps}>
{options.map((option, i) => {
if (isGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => (
<Item
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
onSelect={() => onSelect?.(getValue(grOpt))}
/>
))}
</MenubarGroup>
)
}
if (isSubMenu(option)) {
return (
<SubMenu
key={option.submenu}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
groupLabelCls={groupLabelCls}
onSelect={onSelect}
/>
)
}
return (
<Item
key={getKey(option, i)}
option={option}
className={itemCls}
onSelect={() => onSelect?.(getValue(option))}
/>
)
})}
</MenubarContent>
</MenubarMenu>
)
}
type checkboxWrapperInner = commonInner & commonCheckboxProps & {
options: menuInputOptionsT
label?: string
}
function MenubarCheckboxWrapperInner({
trigger,
options,
label,
contentProps,
itemCls,
groupCls,
groupLabelCls,
checked: o_checked,
onCheckedChange: o_onCheckedChange,
indicatorAt,
}: checkboxWrapperInner) {
const [i_checked, setIChecked] = useState<allowedPrimitiveT[]>([])
function i_Checked(v: allowedPrimitiveT, c: boolean) {
setIChecked(prev => !c ? prev.filter(p => p !== v) : [...prev, v])
}
const checked = o_checked ?? i_checked
const onCheckedChange = o_onCheckedChange ?? i_Checked
return (
<MenubarMenu>
<MenubarTrigger asChild={typeof trigger !== "string"}>
{trigger}
</MenubarTrigger>
<MenubarContent {...contentProps}>
{label && (
<>
<MenubarLabel>{label}</MenubarLabel>
<MenubarSeparator />
</>
)}
{options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => {
const v = getValue(grOpt)
return (
<CheckboxItem
key={getKey(grOpt, j)}
option={grOpt}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarGroup>
)
}
if (isInputSubMenu(option)) {
return (
<CheckboxSubMenu
key={option.submenu}
submenu={option}
checked={checked}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onCheckedChange={onCheckedChange}
/>
)
}
const v = getValue(option)
return (
<CheckboxItem
key={getKey(option, i)}
option={option}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarContent>
</MenubarMenu>
)
}
type radioWrapperInner = commonInner & commonRadioProps & {
options: menuInputOptionsT
label?: string
}
function MenubarRadioWrapperInner({
trigger,
options,
label,
itemCls,
groupCls,
groupLabelCls,
contentProps,
value: o_value,
onValueChange: o_onValueChange,
indicatorAt,
}: radioWrapperInner) {
const [i_value, setIValue] = useState<allowedPrimitiveT>("")
const value = o_value ?? i_value
const onValueChange = o_onValueChange ?? setIValue
return (
<MenubarMenu>
<MenubarTrigger asChild={typeof trigger !== "string"}>
{trigger}
</MenubarTrigger>
<MenubarContent {...contentProps}>
{label && (
<>
<MenubarLabel>{label}</MenubarLabel>
<MenubarSeparator />
</>
)}
<MenubarRadioGroup value={`${value}`} onValueChange={v => onValueChange(parseAllowedPrimitive(v))}>
{options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => (
<RadioItem
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
indicatorAt={indicatorAt}
/>
))}
</MenubarGroup>
)
}
if (isInputSubMenu(option)) {
return (
<RadioSubMenu
key={option.submenu}
value={value}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onValueChange={v => onValueChange(parseAllowedPrimitive(v))}
/>
)
}
return (
<RadioItem
key={getKey(option, i)}
option={option}
className={itemCls}
indicatorAt={indicatorAt}
/>
)
})}
</MenubarRadioGroup>
</MenubarContent>
</MenubarMenu>
)
}
type wrap = commonWrapT & {
options: menubarOptionsT
onSelect?: (value: allowedPrimitiveT) => void
}
function MenubarWrapper({
options,
itemCls,
groupCls,
groupLabelCls,
contentProps,
onSelect,
...props
}: wrap) {
return (
<Menubar {...props}>
{
options.map(op => (
<MenubarWrapperInner
key={op.key}
trigger={op.trigger}
options={op.options}
itemCls={cn(itemCls, op.itemCls)}
groupCls={cn(groupCls, op.groupCls)}
groupLabelCls={cn(groupLabelCls, op.groupLabelCls)}
contentProps={{ ...contentProps, ...op?.contentProps }}
onSelect={op?.onSelect || onSelect}
/>
))
}
</Menubar>
)
}
type wrapCheckboxT = commonWrapT & commonCheckboxProps & {
options: menubarCheckboxOptionsT
}
function MenubarCheckboxWrapper({
options,
contentProps,
itemCls,
groupCls,
groupLabelCls,
checked,
onCheckedChange,
indicatorAt,
...props
}: wrapCheckboxT) {
return (
<Menubar {...props}>
{options.map(op => (
<MenubarCheckboxWrapperInner
key={op.key}
trigger={op.trigger}
options={op.options}
itemCls={cn(itemCls, op.itemCls)}
groupCls={cn(groupCls, op.groupCls)}
groupLabelCls={cn(groupLabelCls, op.groupLabelCls)}
contentProps={{ ...contentProps, ...op?.contentProps }}
onCheckedChange={op.onCheckedChange ?? onCheckedChange}
indicatorAt={op.indicatorAt ?? indicatorAt}
checked={op.checked ?? checked}
/>
))}
</Menubar>
)
}
type wrapRadioT = commonWrapT & commonRadioProps & {
options: menubarRadioOptionsT
}
function MenubarRadioWrapper({
options,
contentProps,
itemCls,
groupCls,
groupLabelCls,
value,
onValueChange,
indicatorAt,
...props
}: wrapRadioT) {
return (
<Menubar {...props}>
{options.map(op => (
<MenubarRadioWrapperInner
key={op.key}
trigger={op.trigger}
options={op.options}
itemCls={cn(itemCls, op.itemCls)}
groupCls={cn(groupCls, op.groupCls)}
groupLabelCls={cn(groupLabelCls, op.groupLabelCls)}
contentProps={{ ...contentProps, ...op?.contentProps }}
onValueChange={op.onValueChange ?? onValueChange}
indicatorAt={op.indicatorAt ?? indicatorAt}
value={op.value ?? value}
/>
))}
</Menubar>
)
}
export {
MenubarWrapper,
MenubarCheckboxWrapper,
MenubarRadioWrapper,
type menubarOptionsT,
type menubarCheckboxOptionsT,
type menubarRadioOptionsT,
}Installation
npm install @radix-ui/react-menubarCopy and paste the following code into your project.
general.d.ts
type readOnlyChildren = Readonly<{
children: React.ReactNode;
}>
type allowedPrimitiveT = string | number | boolean
type optionT = {
label: React.ReactNode
value: allowedPrimitiveT
className?: string
}
type groupT = {
group: string
options: (allowedPrimitiveT | optionT)[]
className?: string
}
type optionsT = (allowedPrimitiveT | optionT | groupT)[]
type indicatorAtT = "right" | "left"menu.d.ts
type menuOptionT = allowedPrimitiveT | optionT | (optionT & {
variant?: "default" | "destructive"
shortcut?: string
disabled?: boolean
})
type menuGroupT = {
group: string
options: menuOptionT[]
className?: string
groupLabelCls?: string
}
type subMenuT = {
submenu: string
options: (menuOptionT | menuGroupT)[]
triggerCls?: string
contentCls?: string
}
type menuOptionsT = (menuOptionT | menuGroupT | subMenuT)[]
type menuInputOptionT = allowedPrimitiveT | optionT | (optionT & {
disabled?: boolean
})
type menuInputGroupT = {
group: string
options: menuInputOptionT[]
className?: string
groupLabelCls?: string
}
type inputSubMenuT = {
submenu: string
options: (menuInputOptionT | menuInputGroupT)[]
triggerCls?: string
contentCls?: string
}
type menuInputOptionsT = (menuInputOptionT | menuInputGroupT | inputSubMenuT)[]utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function isAllowedPrimitive(value: unknown): value is allowedPrimitiveT {
return ["string", "number", "boolean"].includes(typeof value)
}
export function parseAllowedPrimitive(value: allowedPrimitiveT): allowedPrimitiveT {
if (typeof value !== "string") return value
const trimmed = value.trim()
if (trimmed === "true") return true
if (trimmed === "false") return false
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed)
return trimmed
}
export function optionTypeChecker<T>(key: keyof T) {
return (option: any): option is T => !!option && typeof option === "object" && key in option
}
export const isSeparator = (item: any) => item === "---"
export const isOption = optionTypeChecker<optionT>("value")
export const isGroup = optionTypeChecker<groupT>("group")
export const getValue = (item: allowedPrimitiveT | optionT) => typeof item === "object" ? item.value : item
export const getLabel = (item: allowedPrimitiveT | optionT) => typeof item === "object" ? item.label : `${item}`
export function getKey(item: allowedPrimitiveT | optionT, i: number): string {
const val = getValue(item)
if (typeof val === "boolean") return `key-${val}`
if (val === "---") return `---${i}`
return `${val}`
}
export const isSubMenu = optionTypeChecker<subMenuT>("submenu")
export const isGroupMenu = optionTypeChecker<menuGroupT>("group")
export const isInputSubMenu = optionTypeChecker<inputSubMenuT>("submenu")
export const isInputGroupMenu = optionTypeChecker<menuInputGroupT>("group")menubar.tsx
"use client"
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className
)}
{...props}
/>
)
}
function MenubarMenu(props: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
}
function MenubarGroup(props: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
}
function MenubarPortal(props: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
}
function MenubarRadioGroup(props: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
)
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className
)}
{...props}
/>
)
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</MenubarPortal>
)
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function MenubarCheckboxItem({
className,
children,
checked,
indicatorAt = "right",
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem> & { indicatorAt?: indicatorAtT }) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
indicatorAt === "right" ? "pr-8 pl-2" : "pr-2 pl-8",
)}
checked={checked}
{...props}
>
<span className={cn("pointer-events-none absolute flex size-3.5 items-center justify-center", indicatorAt === "right" ? "right-2" : "left-2")}>
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
function MenubarRadioItem({
className,
children,
indicatorAt = "right",
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem> & { indicatorAt?: indicatorAtT }) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
indicatorAt === "right" ? "pr-8 pl-2" : "pr-2 pl-8",
)}
{...props}
>
<span className={cn("pointer-events-none absolute flex size-3.5 items-center justify-center", indicatorAt === "right" ? "right-2" : "left-2")}>
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
)
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function MenubarSub(props: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
)
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
}menubar-wrapper.tsx
"use client"
import { useState } from "react"
import {
cn, getKey, getLabel, getValue,
isSeparator,
isSubMenu,
isGroupMenu,
isInputSubMenu,
isInputGroupMenu,
parseAllowedPrimitive,
} from "@/lib/utils"
import {
Menubar,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarLabel,
MenubarItem,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
MenubarMenu,
} from "@/components/ui/menubar"
type commomClsT = {
itemCls?: string
groupCls?: string
groupLabelCls?: string
}
type commonCheckboxProps = {
checked?: allowedPrimitiveT[]
indicatorAt?: indicatorAtT
onCheckedChange?: (value: allowedPrimitiveT, checked: boolean) => void
}
type commonRadioProps = {
value?: allowedPrimitiveT
indicatorAt?: indicatorAtT
onValueChange?: (value: allowedPrimitiveT) => void
}
type commonInner = commomClsT & {
trigger: React.ReactNode
contentProps?: React.ComponentProps<typeof MenubarContent>
}
type menubarBaseT = commomClsT & {
key: string
trigger: React.ReactNode
contentProps?: React.ComponentProps<typeof MenubarContent>
}
type menubarOptionsT = (menubarBaseT & {
options: menuOptionsT
onSelect?: (value: allowedPrimitiveT) => void
})[]
type menubarInputOptionT = menubarBaseT & {
options: menuInputOptionsT
}
type menubarCheckboxOptionsT = (menubarInputOptionT & commonCheckboxProps)[]
type menubarRadioOptionsT = (menubarInputOptionT & commonRadioProps)[]
type commonWrapT = commomClsT & Omit<React.ComponentProps<typeof Menubar>, "children" | "asChild" | "value"> & {
contentProps?: React.ComponentProps<typeof MenubarContent>
}
// -------
type itemProps = {
option: menuOptionT
className?: string
onSelect?: () => void
}
function Item({
option,
className,
onSelect
}: itemProps) {
const value = getValue(option)
if (isSeparator(value)) return <MenubarSeparator className={className} />
const label = getLabel(option)
const opt: any = typeof option === "object" ? option : {}
const shortcut = opt?.shortcut
return (
<MenubarItem
{...opt}
onSelect={onSelect}
className={cn(className, opt?.className)}
>
{label}
{shortcut && <MenubarShortcut>{shortcut}</MenubarShortcut>}
</MenubarItem>
)
}
type checkboxItemProps = {
option: menuInputOptionT
className?: string
checked?: boolean
onCheckedChange?: (checked: boolean) => void
indicatorAt?: indicatorAtT
}
function CheckboxItem({
option,
className,
checked = false,
indicatorAt,
onCheckedChange = () => { }
}: checkboxItemProps) {
const value = getValue(option)
if (isSeparator(value)) return <MenubarSeparator className={className} />
const label = getLabel(option)
const disabled = (option as any)?.disabled
return (
<MenubarCheckboxItem
checked={checked}
disabled={disabled}
className={className}
indicatorAt={indicatorAt}
onCheckedChange={onCheckedChange}
>
{label}
</MenubarCheckboxItem>
)
}
type radioItemProps = {
option: menuInputOptionT
className?: string
indicatorAt?: indicatorAtT
}
function RadioItem({
option,
className,
indicatorAt
}: radioItemProps) {
const value = getValue(option)
if (isSeparator(value)) return <MenubarSeparator className={className} />
const label = getLabel(option)
const disabled = (option as any)?.disabled
return (
<MenubarRadioItem
value={`${value}`}
disabled={disabled}
className={className}
indicatorAt={indicatorAt}
>
{label}
</MenubarRadioItem>
)
}
type SubMenuProps = commomClsT & {
submenu: subMenuT
onSelect?: (value: allowedPrimitiveT) => void
}
function SubMenu({
submenu,
itemCls,
groupCls,
groupLabelCls,
onSelect
}: SubMenuProps) {
return (
<MenubarSub>
<MenubarSubTrigger className={submenu.triggerCls}>
{submenu.submenu}
</MenubarSubTrigger>
<MenubarSubContent className={submenu.contentCls}>
{submenu.options.map((option, i) => {
if (isGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => (
<Item
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
onSelect={() => onSelect?.(getValue(grOpt))}
/>
))}
</MenubarGroup>
)
}
if (isSubMenu(option)) {
return (
<SubMenu
key={getKey(option, i)}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
onSelect={onSelect}
/>
)
}
return (
<Item
key={getKey(option, i)}
option={option}
className={itemCls}
onSelect={() => onSelect?.(getValue(option))}
/>
)
})}
</MenubarSubContent>
</MenubarSub>
)
}
type CheckboxSubMenuProps = commomClsT & commonCheckboxProps & {
submenu: inputSubMenuT
}
function CheckboxSubMenu({
submenu,
itemCls,
groupCls,
groupLabelCls,
checked = [],
indicatorAt,
onCheckedChange = () => { }
}: CheckboxSubMenuProps) {
return (
<MenubarSub>
<MenubarSubTrigger className={submenu.triggerCls}>
{submenu.submenu}
</MenubarSubTrigger>
<MenubarSubContent className={submenu.contentCls}>
{submenu.options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => {
const v = getValue(grOpt)
return (
<CheckboxItem
key={getKey(grOpt, j)}
option={grOpt}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarGroup>
)
}
if (isInputSubMenu(option)) {
return (
<CheckboxSubMenu
key={option.submenu}
submenu={option}
checked={checked}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onCheckedChange={onCheckedChange}
/>
)
}
const v = getValue(option)
return (
<CheckboxItem
key={getKey(option, i)}
option={option}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarSubContent>
</MenubarSub>
)
}
type RadioSubMenuProps = commomClsT & commonRadioProps & {
submenu: inputSubMenuT
}
function RadioSubMenu({
submenu,
itemCls,
groupCls,
groupLabelCls,
value = "",
indicatorAt,
onValueChange = () => { }
}: RadioSubMenuProps) {
return (
<MenubarSub>
<MenubarSubTrigger className={submenu.triggerCls}>
{submenu.submenu}
</MenubarSubTrigger>
<MenubarSubContent className={submenu.contentCls}>
<MenubarRadioGroup value={`${value}`} onValueChange={onValueChange}>
{submenu.options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => (
<RadioItem
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
indicatorAt={indicatorAt}
/>
))}
</MenubarGroup>
)
}
if (isInputSubMenu(option)) {
return (
<RadioSubMenu
key={option.submenu}
value={value}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onValueChange={onValueChange}
/>
)
}
return (
<RadioItem
key={getKey(option, i)}
option={option}
className={itemCls}
indicatorAt={indicatorAt}
/>
)
})}
</MenubarRadioGroup>
</MenubarSubContent>
</MenubarSub>
)
}
type wrapperInner = commonInner & {
options: menuOptionsT
onSelect?: (value: allowedPrimitiveT) => void
}
function MenubarWrapperInner({
trigger,
options,
itemCls,
groupCls,
groupLabelCls,
contentProps,
onSelect
}: wrapperInner) {
return (
<MenubarMenu>
<MenubarTrigger asChild={typeof trigger !== "string"}>
{trigger}
</MenubarTrigger>
<MenubarContent {...contentProps}>
{options.map((option, i) => {
if (isGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => (
<Item
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
onSelect={() => onSelect?.(getValue(grOpt))}
/>
))}
</MenubarGroup>
)
}
if (isSubMenu(option)) {
return (
<SubMenu
key={option.submenu}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
groupLabelCls={groupLabelCls}
onSelect={onSelect}
/>
)
}
return (
<Item
key={getKey(option, i)}
option={option}
className={itemCls}
onSelect={() => onSelect?.(getValue(option))}
/>
)
})}
</MenubarContent>
</MenubarMenu>
)
}
type checkboxWrapperInner = commonInner & commonCheckboxProps & {
options: menuInputOptionsT
label?: string
}
function MenubarCheckboxWrapperInner({
trigger,
options,
label,
contentProps,
itemCls,
groupCls,
groupLabelCls,
checked: o_checked,
onCheckedChange: o_onCheckedChange,
indicatorAt,
}: checkboxWrapperInner) {
const [i_checked, setIChecked] = useState<allowedPrimitiveT[]>([])
function i_Checked(v: allowedPrimitiveT, c: boolean) {
setIChecked(prev => !c ? prev.filter(p => p !== v) : [...prev, v])
}
const checked = o_checked ?? i_checked
const onCheckedChange = o_onCheckedChange ?? i_Checked
return (
<MenubarMenu>
<MenubarTrigger asChild={typeof trigger !== "string"}>
{trigger}
</MenubarTrigger>
<MenubarContent {...contentProps}>
{label && (
<>
<MenubarLabel>{label}</MenubarLabel>
<MenubarSeparator />
</>
)}
{options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => {
const v = getValue(grOpt)
return (
<CheckboxItem
key={getKey(grOpt, j)}
option={grOpt}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarGroup>
)
}
if (isInputSubMenu(option)) {
return (
<CheckboxSubMenu
key={option.submenu}
submenu={option}
checked={checked}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onCheckedChange={onCheckedChange}
/>
)
}
const v = getValue(option)
return (
<CheckboxItem
key={getKey(option, i)}
option={option}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
/>
)
})}
</MenubarContent>
</MenubarMenu>
)
}
type radioWrapperInner = commonInner & commonRadioProps & {
options: menuInputOptionsT
label?: string
}
function MenubarRadioWrapperInner({
trigger,
options,
label,
itemCls,
groupCls,
groupLabelCls,
contentProps,
value: o_value,
onValueChange: o_onValueChange,
indicatorAt,
}: radioWrapperInner) {
const [i_value, setIValue] = useState<allowedPrimitiveT>("")
const value = o_value ?? i_value
const onValueChange = o_onValueChange ?? setIValue
return (
<MenubarMenu>
<MenubarTrigger asChild={typeof trigger !== "string"}>
{trigger}
</MenubarTrigger>
<MenubarContent {...contentProps}>
{label && (
<>
<MenubarLabel>{label}</MenubarLabel>
<MenubarSeparator />
</>
)}
<MenubarRadioGroup value={`${value}`} onValueChange={v => onValueChange(parseAllowedPrimitive(v))}>
{options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<MenubarGroup key={option.group} className={cn(groupCls, option.className)}>
<MenubarLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
{option.group}
</MenubarLabel>
{option.options.map((grOpt, j) => (
<RadioItem
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
indicatorAt={indicatorAt}
/>
))}
</MenubarGroup>
)
}
if (isInputSubMenu(option)) {
return (
<RadioSubMenu
key={option.submenu}
value={value}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onValueChange={v => onValueChange(parseAllowedPrimitive(v))}
/>
)
}
return (
<RadioItem
key={getKey(option, i)}
option={option}
className={itemCls}
indicatorAt={indicatorAt}
/>
)
})}
</MenubarRadioGroup>
</MenubarContent>
</MenubarMenu>
)
}
type wrap = commonWrapT & {
options: menubarOptionsT
onSelect?: (value: allowedPrimitiveT) => void
}
function MenubarWrapper({
options,
itemCls,
groupCls,
groupLabelCls,
contentProps,
onSelect,
...props
}: wrap) {
return (
<Menubar {...props}>
{
options.map(op => (
<MenubarWrapperInner
key={op.key}
trigger={op.trigger}
options={op.options}
itemCls={cn(itemCls, op.itemCls)}
groupCls={cn(groupCls, op.groupCls)}
groupLabelCls={cn(groupLabelCls, op.groupLabelCls)}
contentProps={{ ...contentProps, ...op?.contentProps }}
onSelect={op?.onSelect || onSelect}
/>
))
}
</Menubar>
)
}
type wrapCheckboxT = commonWrapT & commonCheckboxProps & {
options: menubarCheckboxOptionsT
}
function MenubarCheckboxWrapper({
options,
contentProps,
itemCls,
groupCls,
groupLabelCls,
checked,
onCheckedChange,
indicatorAt,
...props
}: wrapCheckboxT) {
return (
<Menubar {...props}>
{options.map(op => (
<MenubarCheckboxWrapperInner
key={op.key}
trigger={op.trigger}
options={op.options}
itemCls={cn(itemCls, op.itemCls)}
groupCls={cn(groupCls, op.groupCls)}
groupLabelCls={cn(groupLabelCls, op.groupLabelCls)}
contentProps={{ ...contentProps, ...op?.contentProps }}
onCheckedChange={op.onCheckedChange ?? onCheckedChange}
indicatorAt={op.indicatorAt ?? indicatorAt}
checked={op.checked ?? checked}
/>
))}
</Menubar>
)
}
type wrapRadioT = commonWrapT & commonRadioProps & {
options: menubarRadioOptionsT
}
function MenubarRadioWrapper({
options,
contentProps,
itemCls,
groupCls,
groupLabelCls,
value,
onValueChange,
indicatorAt,
...props
}: wrapRadioT) {
return (
<Menubar {...props}>
{options.map(op => (
<MenubarRadioWrapperInner
key={op.key}
trigger={op.trigger}
options={op.options}
itemCls={cn(itemCls, op.itemCls)}
groupCls={cn(groupCls, op.groupCls)}
groupLabelCls={cn(groupLabelCls, op.groupLabelCls)}
contentProps={{ ...contentProps, ...op?.contentProps }}
onValueChange={op.onValueChange ?? onValueChange}
indicatorAt={op.indicatorAt ?? indicatorAt}
value={op.value ?? value}
/>
))}
</Menubar>
)
}
export {
MenubarWrapper,
MenubarCheckboxWrapper,
MenubarRadioWrapper,
type menubarOptionsT,
type menubarCheckboxOptionsT,
type menubarRadioOptionsT,
}Done
You can now use MenubarCheckboxWrapper, MenubarRadioWrapper, and MenubarWrapper.
Usage
Basic
import { type menubarOptionsT, MenubarCheckboxWrapper, MenubarRadioWrapper, MenubarWrapper } from "@/components/ui/menubar-wrapper"
export function Basic() {
const opts: menubarOptionsT = [
{
key: "1",
trigger: "Option 1",
options: dropdownOptions
},
{
key: "2",
trigger: "Option 2",
options: dropdownOptions,
}
]
return (
<>
<MenubarWrapper
options={opts}
contentProps={{ align: "end" }}
/>
<MenubarCheckboxWrapper
options={opts}
/>
<MenubarRadioWrapper
options={opts}
/>
</>
)
}Controlled
import { useState } from "react"
import { type menubarOptionsT, MenubarCheckboxWrapper, MenubarRadioWrapper, MenubarWrapper } from "@/components/ui/menubar-wrapper"
import { Button } from "@/components/ui/button"
export function Controlled() {
const [checked, setChecked] = useState<allowedPrimitiveT[]>([])
const [val, setVal] = useState<allowedPrimitiveT>(true)
const opts: menubarOptionsT = [
{
key: "1",
trigger: "Option 1",
options: dropdownOptions
},
{
key: "2",
trigger: "Option 2",
options: dropdownOptions,
}
]
return (
<>
<MenubarWrapper
options={opts}
onSelect={v => console.log(v)}
/>
<MenubarCheckboxWrapper
checked={checked}
options={opts}
onCheckedChange={(value, isChecked) => {
setChecked((prev) =>
isChecked
? [...prev, value]
: prev.filter((x) => x !== value)
)
}}
/>
<MenubarRadioWrapper
value={val}
options={opts}
onValueChange={setVal}
/>
</>
)
}Nested Complex Data
const dropdownOptions = [
{ label: "New File", value: "new", shortcut: "Ctrl+N" },
"Save",
12,
{ label: <><Banana /> Banana</>, value: "banana" },
"---",
{
group: "Settings",
options: [
{ label: "Appearance", value: "appearance" },
22,
true
],
},
{
submenu: "More",
options: [
{ label: <><Apple /> Apple</>, value: "apple" },
{
group: "Tools",
options: [
{ label: "Formatter", value: "formatter" },
false,
],
},
],
},
]
export function Complex() {
const opts: menubarOptionsT = [
{
key: "1",
trigger: "Option 1",
options: dropdownOptions
}
]
return (
<MenubarWrapper
options={opts}
/>
)
}Reference
MenubarOption
Prop
Type
MenubarCheckboxOption
Prop
Type
MenubarRadioOption
Prop
Type
MenubarWrapper
Prop
Type
MenubarCheckboxWrapper
Prop
Type
MenubarRadioWrapper
Prop
Type