Skip to content

Commit 1c5eb8f

Browse files
committed
feat: add component dropdown menu
1 parent aecbb0f commit 1c5eb8f

23 files changed

+1039
-4
lines changed

packages/ui/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111
"main": "./dist/index.js",
1212
"module": "./dist/index.js",
1313
"types": "./dist/index.d.ts",
14-
"files": [
15-
"dist"
16-
],
14+
"files": ["dist"],
1715
"scripts": {
1816
"build": "pnpm run pgk:prod && pnpm run build:components && pnpm run registry",
1917
"build:components": "tsdown",
@@ -31,6 +29,7 @@
3129
"@radix-ui/react-checkbox": "1.3.2",
3230
"@radix-ui/react-collapsible": "^1.1.11",
3331
"@radix-ui/react-compose-refs": "1.1.2",
32+
"@radix-ui/react-dropdown-menu": "^2.1.15",
3433
"@radix-ui/react-label": "2.1.7",
3534
"@radix-ui/react-popover": "1.1.14",
3635
"@radix-ui/react-scroll-area": "1.2.9",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Root, Trigger } from '@radix-ui/react-dropdown-menu';
2+
3+
import DropdownMenuOption from './DropdownMenuOption';
4+
import DropdownMenuPortalContent from './DropdownMenuPortalContent';
5+
import type { DropdownMenuProps } from './types';
6+
7+
const DropdownMenu = (props: DropdownMenuProps) => {
8+
const { children, classNames, contentProps, defaultOpen, dir, items, modal, onOpenChange, open, size } = props;
9+
10+
return (
11+
<Root
12+
defaultOpen={defaultOpen}
13+
dir={dir}
14+
modal={modal}
15+
open={open}
16+
onOpenChange={onOpenChange}
17+
>
18+
<Trigger asChild>{children}</Trigger>
19+
20+
<DropdownMenuPortalContent {...contentProps}>
21+
{items.map((item, index) => {
22+
return (
23+
<DropdownMenuOption
24+
classNames={classNames}
25+
item={item}
26+
key={String(index)}
27+
size={size}
28+
/>
29+
);
30+
})}
31+
</DropdownMenuPortalContent>
32+
</Root>
33+
);
34+
};
35+
36+
export default DropdownMenu;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Arrow } from '@radix-ui/react-dropdown-menu';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib';
6+
7+
import { menuVariants } from './dropdown-menu-variants';
8+
import type { DropdownMenuArrowProps } from './types';
9+
10+
const DropdownMenuArrow = forwardRef<ComponentRef<typeof Arrow>, DropdownMenuArrowProps>((props, ref) => {
11+
const { className, ...rest } = props;
12+
13+
const { arrow } = menuVariants();
14+
15+
const mergedCls = cn(arrow(), className);
16+
17+
return (
18+
<Arrow
19+
className={mergedCls}
20+
ref={ref}
21+
{...rest}
22+
/>
23+
);
24+
});
25+
26+
DropdownMenuArrow.displayName = 'DropdownMenuArrow';
27+
28+
export default DropdownMenuArrow;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Content } from '@radix-ui/react-dropdown-menu';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib';
6+
7+
import { menuVariants } from './dropdown-menu-variants';
8+
import type { DropdownMenuContentProps } from './types';
9+
10+
const DropdownMenuContent = forwardRef<ComponentRef<typeof Content>, DropdownMenuContentProps>((props, ref) => {
11+
const { className, size, ...rest } = props;
12+
13+
const { content } = menuVariants({ size });
14+
15+
const mergedCls = cn(content(), className);
16+
17+
return (
18+
<Content
19+
className={mergedCls}
20+
ref={ref}
21+
{...rest}
22+
/>
23+
);
24+
});
25+
26+
DropdownMenuContent.displayName = 'DropdownMenuContent';
27+
28+
export default DropdownMenuContent;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Item } from '@radix-ui/react-dropdown-menu';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib';
6+
7+
import DropdownMenuShortcut from './DropdownMenuShortcut';
8+
import { menuVariants } from './dropdown-menu-variants';
9+
import type { DropdownMenuItemProps } from './types';
10+
11+
const DropdownMenuItem = forwardRef<ComponentRef<typeof Item>, DropdownMenuItemProps>((props, ref) => {
12+
const { children, className, classNames, leading, shortcut, size, trailing, ...rest } = props;
13+
14+
const { item } = menuVariants({ size });
15+
16+
const mergedCls = cn(item(), className);
17+
18+
return (
19+
<Item
20+
className={mergedCls}
21+
ref={ref}
22+
{...rest}
23+
>
24+
{leading}
25+
26+
<span>{children}</span>
27+
28+
{trailing}
29+
30+
{shortcut && (
31+
<DropdownMenuShortcut
32+
className={classNames?.shortcut}
33+
size={size}
34+
value={shortcut}
35+
/>
36+
)}
37+
</Item>
38+
);
39+
});
40+
41+
DropdownMenuItem.displayName = 'DropdownMenuItem';
42+
43+
export default DropdownMenuItem;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ItemIndicator } from '@radix-ui/react-dropdown-menu';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib';
6+
7+
import { menuVariants } from './dropdown-menu-variants';
8+
import type { DropdownMenuItemIndicatorProps } from './types';
9+
10+
const DropdownMenuItemIndicator = forwardRef<ComponentRef<typeof ItemIndicator>, DropdownMenuItemIndicatorProps>(
11+
(props, ref) => {
12+
const { className, size, ...rest } = props;
13+
14+
const { itemIndicator } = menuVariants({ size });
15+
16+
const mergedCls = cn(itemIndicator, className);
17+
18+
return (
19+
<ItemIndicator
20+
className={mergedCls}
21+
ref={ref}
22+
{...rest}
23+
/>
24+
);
25+
}
26+
);
27+
28+
DropdownMenuItemIndicator.displayName = 'DropdownMenuItemIndicator';
29+
30+
export default DropdownMenuItemIndicator;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Label } from '@radix-ui/react-dropdown-menu';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib';
6+
7+
import { menuVariants } from './dropdown-menu-variants';
8+
import type { DropdownMenuLabelProps } from './types';
9+
10+
const DropdownMenuLabel = forwardRef<ComponentRef<typeof Label>, DropdownMenuLabelProps>((props, ref) => {
11+
const { children, className, classNames, leading, size, trailing, ...rest } = props;
12+
13+
const { label } = menuVariants({ size });
14+
15+
const mergedCls = cn(label(), className || classNames?.label);
16+
17+
return (
18+
<Label
19+
className={mergedCls}
20+
ref={ref}
21+
{...rest}
22+
>
23+
{leading}
24+
25+
{children}
26+
27+
{trailing}
28+
</Label>
29+
);
30+
});
31+
32+
DropdownMenuLabel.displayName = 'DropdownMenuLabel';
33+
34+
export default DropdownMenuLabel;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Group, Portal, Sub } from '@radix-ui/react-dropdown-menu';
2+
3+
import { cn } from '@/lib';
4+
5+
import DropdownMenuItem from './DropdownMenuItem';
6+
import DropdownMenuLabel from './DropdownMenuLabel';
7+
import DropdownMenuSeparator from './DropdownMenuSeparator';
8+
import DropdownMenuSubContent from './DropdownMenuSubContent';
9+
import DropdownMenuSubTrigger from './DropdownMenuSubTrigger';
10+
import type {
11+
DropdownMenuLabelOption,
12+
DropdownMenuOptionData,
13+
DropdownMenuOptionProps,
14+
DropdownMenuSeparatorOption,
15+
DropdownMenuSubOption
16+
} from './types';
17+
18+
function isLabel(opt: DropdownMenuOptionData): opt is DropdownMenuLabelOption {
19+
return opt.type === 'label';
20+
}
21+
function isSeparator(opt: DropdownMenuOptionData): opt is DropdownMenuSeparatorOption {
22+
return opt.type === 'separator';
23+
}
24+
25+
function isSub(opt: DropdownMenuOptionData): opt is DropdownMenuSubOption {
26+
return opt.type === 'sub';
27+
}
28+
29+
const DropdownMenuOption = (props: DropdownMenuOptionProps) => {
30+
const { classNames, item, size } = props;
31+
32+
if (isSeparator(item)) {
33+
return (
34+
<DropdownMenuSeparator
35+
{...item}
36+
className={classNames?.separator}
37+
size={size}
38+
/>
39+
);
40+
}
41+
42+
if (isLabel(item)) {
43+
return (
44+
<DropdownMenuLabel
45+
classNames={classNames}
46+
size={size}
47+
{...item}
48+
>
49+
{item.label}
50+
</DropdownMenuLabel>
51+
);
52+
}
53+
54+
if (isSub(item)) {
55+
const { label, subContentProps, subProps, ...subTriggerProps } = item;
56+
return (
57+
<Sub {...subProps}>
58+
<DropdownMenuSubTrigger {...subTriggerProps}>{label}</DropdownMenuSubTrigger>
59+
60+
<Portal>
61+
<DropdownMenuSubContent
62+
{...subContentProps}
63+
className={classNames?.subContent}
64+
size={size}
65+
>
66+
<Group className={cn(classNames?.subContent)}>
67+
{item.children.map((child, index) => {
68+
return (
69+
<DropdownMenuOption
70+
item={child}
71+
key={String(index)}
72+
/>
73+
);
74+
})}
75+
</Group>
76+
</DropdownMenuSubContent>
77+
</Portal>
78+
</Sub>
79+
);
80+
}
81+
82+
return (
83+
<DropdownMenuItem
84+
classNames={classNames}
85+
size={size}
86+
{...item}
87+
>
88+
{item.label}
89+
</DropdownMenuItem>
90+
);
91+
};
92+
93+
DropdownMenuOption.displayName = 'DropdownMenuOption';
94+
95+
export default DropdownMenuOption;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Portal } from '@radix-ui/react-dropdown-menu';
2+
3+
import DropdownMenuArrow from './DropdownMenuArrow';
4+
import DropdownMenuContent from './DropdownMenuContent';
5+
import type { DropdownMenuPortalContentProps } from './types';
6+
7+
const DropdownMenuPortalContent = (props: DropdownMenuPortalContentProps) => {
8+
const { children, container, forceMountContent, forceMountPortal, showArrow, ...rest } = props;
9+
10+
return (
11+
<Portal
12+
container={container}
13+
forceMount={forceMountPortal}
14+
>
15+
<DropdownMenuContent
16+
forceMount={forceMountContent}
17+
{...rest}
18+
>
19+
{children}
20+
21+
{showArrow && <DropdownMenuArrow />}
22+
</DropdownMenuContent>
23+
</Portal>
24+
);
25+
};
26+
27+
DropdownMenuPortalContent.displayName = 'DropdownMenuPortalContent';
28+
29+
export default DropdownMenuPortalContent;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Separator } from '@radix-ui/react-dropdown-menu';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib';
6+
7+
import { menuVariants } from './dropdown-menu-variants';
8+
import type { DropdownMenuSeparatorProps } from './types';
9+
10+
const DropdownMenuSeparator = forwardRef<ComponentRef<typeof Separator>, DropdownMenuSeparatorProps>((props, ref) => {
11+
const { className, size, ...rest } = props;
12+
13+
const { separator } = menuVariants({ size });
14+
15+
const mergedCls = cn(separator(), className);
16+
17+
return (
18+
<Separator
19+
className={mergedCls}
20+
ref={ref}
21+
{...rest}
22+
/>
23+
);
24+
});
25+
26+
DropdownMenuSeparator.displayName = 'DropdownMenuSeparator';
27+
28+
export default DropdownMenuSeparator;

0 commit comments

Comments
 (0)