Files
spacedrive/packages/ui/src/ContextMenu.tsx
ameer2468 369d9fd848 [ENG-1750] Update context menu colors (#2369)
update context menu colors
2024-04-21 14:28:27 +00:00

229 lines
5.8 KiB
TypeScript

'use client';
import { CaretRight, Check, Icon, IconProps } from '@phosphor-icons/react';
import * as RadixCM from '@radix-ui/react-context-menu';
import { cva, VariantProps } from 'class-variance-authority';
import clsx from 'clsx';
import { ContextType, createContext, PropsWithChildren, Suspense, useContext } from 'react';
interface ContextMenuProps extends RadixCM.MenuContentProps {
trigger: React.ReactNode;
onOpenChange?: (open: boolean) => void;
disabled?: boolean;
}
export const contextMenuClassNames = clsx(
'z-50 max-h-[calc(100vh-20px)] overflow-y-auto',
'my-2 min-w-48 max-w-64 py-0.5',
'cool-shadow bg-menu/95 backdrop-blur-lg',
'border border-menu-line',
'cursor-default select-none rounded-md',
'animate-in fade-in'
);
const ContextMenuContext = createContext<boolean | null>(null);
export const useContextMenuContext = <T extends boolean>({ suspense }: { suspense?: T } = {}) => {
const ctx = useContext(ContextMenuContext);
if (suspense && ctx === null) throw new Error('ContextMenuContext.Provider not found!');
return ctx as T extends true
? NonNullable<ContextType<typeof ContextMenuContext>>
: NonNullable<ContextType<typeof ContextMenuContext>> | undefined;
};
const Root = ({
trigger,
children,
className,
onOpenChange,
disabled,
...props
}: ContextMenuProps) => {
return (
<RadixCM.Root onOpenChange={onOpenChange}>
<RadixCM.Trigger asChild onContextMenu={(e) => disabled && e.preventDefault()}>
{trigger}
</RadixCM.Trigger>
<RadixCM.Portal>
<RadixCM.Content className={clsx(contextMenuClassNames, className)} {...props}>
<ContextMenuContext.Provider value={true}>
{children}
</ContextMenuContext.Provider>
</RadixCM.Content>
</RadixCM.Portal>
</RadixCM.Root>
);
};
export const contextMenuSeparatorClassNames = 'border-b-menu-line mx-1 my-0.5 border-b';
const Separator = (props: { className?: string }) => (
<RadixCM.Separator className={clsx(contextMenuSeparatorClassNames, props.className)} />
);
const SubMenu = ({
label,
icon,
className,
...props
}: RadixCM.MenuSubContentProps & ContextMenuItemProps) => {
return (
<RadixCM.Sub>
<RadixCM.SubTrigger className={contextMenuItemClassNames}>
<ContextMenuDivItem rightArrow {...{ label, icon }} />
</RadixCM.SubTrigger>
<RadixCM.Portal>
<Suspense fallback={null}>
<RadixCM.SubContent
className={clsx(contextMenuClassNames, '-mt-2', className)}
{...props}
/>
</Suspense>
</RadixCM.Portal>
</RadixCM.Sub>
);
};
const contextMenuItemStyles = cva(
[
'flex max-h-fit min-h-[26px] items-center space-x-2 overflow-hidden rounded px-2',
'text-sm text-menu-ink',
'group-radix-highlighted:text-white',
'group-radix-disabled:pointer-events-none group-radix-disabled:text-menu-ink/50',
'group-radix-state-open:bg-accent group-radix-state-open:text-white'
],
{
variants: {
variant: {
default: 'group-radix-highlighted:bg-accent',
dull: 'group-radix-highlighted:bg-app-selected/50 group-radix-highlighted:!text-menu-ink group-radix-state-open:bg-app-selected/50 group-radix-state-open:!text-ink',
danger: [
'text-red-600 dark:text-red-400',
'group-radix-highlighted:text-white',
'group-radix-highlighted:bg-red-500'
]
}
},
defaultVariants: {
variant: 'default'
}
}
);
export interface ContextMenuItemProps
extends RadixCM.MenuItemProps,
VariantProps<typeof contextMenuItemStyles>,
Pick<ContextMenuInnerItemProps, 'label' | 'keybind' | 'icon' | 'iconProps'> {}
export const contextMenuItemClassNames = 'group py-0.5 outline-none px-1';
const Item = ({
icon,
label,
children,
keybind,
variant,
iconProps,
onClick,
...props
}: ContextMenuItemProps) => {
return (
<RadixCM.Item
{...props}
className={clsx(contextMenuItemClassNames, props.className)}
onClick={(e) => !props.disabled && onClick?.(e)}
>
<ContextMenuDivItem {...{ icon, iconProps, label, keybind, variant, children }} />
</RadixCM.Item>
);
};
export interface ContextMenuCheckboxItemProps
extends RadixCM.MenuCheckboxItemProps,
VariantProps<typeof contextMenuItemStyles>,
Pick<ContextMenuInnerItemProps, 'label' | 'keybind'> {}
const CheckboxItem = ({
variant,
className,
label,
keybind,
children,
...props
}: ContextMenuCheckboxItemProps) => {
return (
<RadixCM.CheckboxItem className={contextMenuItemClassNames} {...props}>
<ContextMenuDivItem variant={variant} className={className}>
<span className="flex size-3.5 items-center justify-center">
<RadixCM.ItemIndicator>
<Check weight="bold" />
</RadixCM.ItemIndicator>
</span>
<ItemInternals {...{ label, keybind, children }} />
</ContextMenuDivItem>
</RadixCM.CheckboxItem>
);
};
interface ContextMenuInnerItemProps {
icon?: Icon;
iconProps?: IconProps;
label?: string;
keybind?: string;
rightArrow?: boolean;
}
export const ContextMenuDivItem = ({
variant,
children,
className,
...props
}: ContextMenuInnerItemProps &
VariantProps<typeof contextMenuItemStyles> &
PropsWithChildren<{ className?: string }>) => (
<div className={contextMenuItemStyles({ variant, className })}>
{children || <ItemInternals {...props} />}
</div>
);
const ItemInternals = ({
icon,
label,
rightArrow,
keybind,
iconProps
}: ContextMenuInnerItemProps) => {
const ItemIcon = icon;
return (
<>
{ItemIcon && <ItemIcon size={18} {...iconProps} />}
{label && <span className="flex-1 truncate">{label}</span>}
{keybind && (
<span className="text-xs font-medium group-radix-highlighted:text-white">
{keybind}
</span>
)}
{rightArrow && (
<CaretRight
weight="fill"
size={12}
className="text-menu-faint group-radix-highlighted:text-white group-radix-state-open:text-white"
/>
)}
</>
);
};
export const ContextMenu = {
Root,
Item,
CheckboxItem,
Separator,
SubMenu
};