Skip to content

Commit 08be0aa

Browse files
committed
feat: add component select
1 parent b35a796 commit 08be0aa

23 files changed

+862
-3
lines changed

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@radix-ui/react-popover": "1.1.14",
4141
"@radix-ui/react-radio-group": "1.3.7",
4242
"@radix-ui/react-scroll-area": "1.2.9",
43+
"@radix-ui/react-select": "^2.2.5",
4344
"@radix-ui/react-separator": "1.1.7",
4445
"@radix-ui/react-slider": "1.3.5",
4546
"@radix-ui/react-slot": "1.2.3",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Root } from '@radix-ui/react-select';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import SelectContent from './SelectContent';
6+
import SelectOption from './SelectOption';
7+
import SelectTrigger from './SelectTrigger';
8+
import type { SelectProps } from './types';
9+
10+
const Select = forwardRef<ComponentRef<typeof SelectContent>, SelectProps>((props, ref) => {
11+
const { classNames, contentProps, indicatorIcon, items, size, triggerProps, ...rest } = props;
12+
13+
return (
14+
<Root
15+
data-slot="select-root"
16+
{...rest}
17+
>
18+
<SelectTrigger {...triggerProps} />
19+
20+
<SelectContent
21+
classNames={classNames}
22+
ref={ref}
23+
{...contentProps}
24+
>
25+
{items.map((item, index) => (
26+
<SelectOption
27+
classNames={classNames}
28+
indicatorIcon={indicatorIcon}
29+
item={item}
30+
key={String(index)}
31+
size={size}
32+
/>
33+
))}
34+
</SelectContent>
35+
</Root>
36+
);
37+
});
38+
39+
Select.displayName = 'Select';
40+
41+
export default Select;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Content, Portal, ScrollDownButton, ScrollUpButton, Viewport } from '@radix-ui/react-select';
2+
import { ChevronDown, ChevronUp } from 'lucide-react';
3+
import type { ComponentRef } from 'react';
4+
import { forwardRef } from 'react';
5+
6+
import { cn } from '@/lib/utils';
7+
8+
import { selectVariants } from './select-variants';
9+
import type { SelectContentProps } from './types';
10+
11+
const SelectContent = forwardRef<ComponentRef<typeof Content>, SelectContentProps>((props, ref) => {
12+
const {
13+
children,
14+
className,
15+
classNames,
16+
position = 'popper',
17+
scrollDownButton,
18+
scrollUpButton,
19+
size,
20+
...rest
21+
} = props;
22+
23+
const {
24+
content,
25+
scrollDownButton: scrollDownButtonCls,
26+
scrollUpButton: scrollUpButtonCls,
27+
viewport: viewportCls
28+
} = selectVariants({ position, size });
29+
30+
const mergedCls = {
31+
contentCls: cn(content(), className || classNames?.content),
32+
scrollDownButtonCls: cn(scrollDownButtonCls(), classNames?.scrollDownButton),
33+
scrollUpButtonCls: cn(scrollUpButtonCls(), classNames?.scrollUpButton),
34+
viewportCls: cn(viewportCls(), classNames?.viewport)
35+
};
36+
37+
return (
38+
<Portal>
39+
<Content
40+
{...rest}
41+
className={mergedCls.contentCls}
42+
data-slot="select-content"
43+
position={position}
44+
ref={ref}
45+
>
46+
<ScrollUpButton
47+
asChild
48+
className={mergedCls.scrollUpButtonCls}
49+
data-slot="scroll-up-button"
50+
>
51+
{scrollUpButton || <ChevronUp />}
52+
</ScrollUpButton>
53+
54+
<Viewport
55+
className={mergedCls.viewportCls}
56+
data-slot="viewport"
57+
>
58+
{children}
59+
</Viewport>
60+
61+
<ScrollDownButton
62+
asChild
63+
className={mergedCls.scrollDownButtonCls}
64+
data-slot="scroll-down-button"
65+
>
66+
{scrollDownButton || <ChevronDown />}
67+
</ScrollDownButton>
68+
</Content>
69+
</Portal>
70+
);
71+
});
72+
73+
SelectContent.displayName = Content.displayName;
74+
75+
export default SelectContent;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Item, ItemIndicator, ItemText } from '@radix-ui/react-select';
2+
import { Check } from 'lucide-react';
3+
import type { ComponentRef } from 'react';
4+
import { forwardRef } from 'react';
5+
6+
import { cn } from '@/lib/utils';
7+
8+
import { selectVariants } from './select-variants';
9+
import type { SelectItemProps } from './types';
10+
11+
const SelectItem = forwardRef<ComponentRef<typeof Item>, SelectItemProps>((props, ref) => {
12+
const { children, className, classNames, indicatorIcon, leading, size, trailing, ...rest } = props;
13+
14+
const { item, itemIndicator } = selectVariants({ size });
15+
16+
const mergedCls = {
17+
itemCls: cn(item(), className || classNames?.item),
18+
itemIndicatorCls: cn(itemIndicator(), classNames?.itemIndicator)
19+
};
20+
21+
return (
22+
<Item
23+
{...rest}
24+
className={mergedCls.itemCls}
25+
data-slot="select-item"
26+
ref={ref}
27+
>
28+
{leading}
29+
30+
<ItemText data-slot="select-item-text">{children}</ItemText>
31+
32+
{trailing}
33+
34+
<ItemIndicator
35+
asChild
36+
className={mergedCls.itemIndicatorCls}
37+
data-slot="select-item-indicator"
38+
>
39+
{indicatorIcon || <Check />}
40+
</ItemIndicator>
41+
</Item>
42+
);
43+
});
44+
45+
SelectItem.displayName = 'SelectItem';
46+
47+
export default SelectItem;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Label } from '@radix-ui/react-select';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
import { selectVariants } from './select-variants';
6+
import type { SelectLabelProps } from './types';
7+
8+
const SelectLabel = (props: SelectLabelProps) => {
9+
const { className, size, ...rest } = props;
10+
11+
const { groupLabel } = selectVariants({ size });
12+
13+
const mergedCls = cn(groupLabel(), className);
14+
15+
return (
16+
<Label
17+
className={mergedCls}
18+
data-slot="select-group-label"
19+
{...rest}
20+
/>
21+
);
22+
};
23+
24+
export default SelectLabel;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Group } from '@radix-ui/react-select';
2+
3+
import SelectItem from './SelectItem';
4+
import SelectLabel from './SelectLabel';
5+
import SelectSeparator from './SelectSeparator';
6+
import { isGroup, isSeparator } from './shared';
7+
import type { SelectOptionProps } from './types';
8+
9+
const SelectOption = (props: SelectOptionProps) => {
10+
const { classNames, indicatorIcon, item, size } = props;
11+
12+
if (isSeparator(item)) {
13+
return (
14+
<SelectSeparator
15+
className={classNames?.separator}
16+
size={size}
17+
{...item}
18+
/>
19+
);
20+
}
21+
22+
if (isGroup(item)) {
23+
const { children, label, ...rest } = item;
24+
return (
25+
<Group>
26+
<SelectLabel
27+
{...rest}
28+
className={classNames?.groupLabel}
29+
>
30+
{label}
31+
</SelectLabel>
32+
33+
{children.map(({ label: childLabel, ...childRest }) => (
34+
<SelectItem
35+
classNames={classNames}
36+
indicatorIcon={indicatorIcon}
37+
key={childRest.value}
38+
size={size}
39+
{...childRest}
40+
>
41+
{childLabel}
42+
</SelectItem>
43+
))}
44+
</Group>
45+
);
46+
}
47+
48+
const { label, ...rest } = item;
49+
50+
return (
51+
<SelectItem
52+
classNames={classNames}
53+
indicatorIcon={indicatorIcon}
54+
size={size}
55+
{...rest}
56+
>
57+
{label}
58+
</SelectItem>
59+
);
60+
};
61+
62+
export default SelectOption;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Separator } from '@radix-ui/react-select';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
import { selectVariants } from './select-variants';
6+
import type { SelectSeparatorProps } from './types';
7+
8+
const SelectSeparator = (props: SelectSeparatorProps) => {
9+
const { className, size, ...rest } = props;
10+
11+
const { separator } = selectVariants({ size });
12+
13+
const mergedCls = cn(separator(), className);
14+
15+
return (
16+
<Separator
17+
className={mergedCls}
18+
data-slot="select-separator"
19+
{...rest}
20+
/>
21+
);
22+
};
23+
24+
export default SelectSeparator;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Icon, Trigger, Value } from '@radix-ui/react-select';
2+
import { ChevronsUpDown } from 'lucide-react';
3+
4+
import { cn } from '@/lib/utils';
5+
6+
import { selectVariants } from './select-variants';
7+
import type { SelectTriggerProps } from './types';
8+
9+
const SelectTrigger = (props: SelectTriggerProps) => {
10+
const { children, className, classNames, leading, placeholder, size, trailing, triggerIcon, ...rest } = props;
11+
12+
const { trigger, triggerIcon: triggerIconCls } = selectVariants({ size });
13+
14+
const mergedCls = {
15+
triggerCls: cn(trigger(), className || classNames?.trigger),
16+
triggerIconCls: cn(triggerIconCls(), classNames?.triggerIcon)
17+
};
18+
19+
return (
20+
<Trigger
21+
{...rest}
22+
className={mergedCls.triggerCls}
23+
data-slot="select-trigger"
24+
>
25+
{leading}
26+
27+
<Value
28+
asChild={Boolean(children)}
29+
className={cn(classNames?.selectedValue)}
30+
data-slot="select-trigger-value"
31+
placeholder={placeholder}
32+
>
33+
{children}
34+
</Value>
35+
36+
{trailing}
37+
38+
<Icon
39+
asChild
40+
className={mergedCls.triggerIconCls}
41+
data-slot="select-trigger-icon"
42+
>
43+
{triggerIcon || <ChevronsUpDown />}
44+
</Icon>
45+
</Trigger>
46+
);
47+
};
48+
49+
export default SelectTrigger;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export { default as Select } from './Select';
2+
export { default as SelectContent } from './SelectContent';
3+
export { default as SelectItem } from './SelectItem';
4+
export { default as SelectLabel } from './SelectLabel';
5+
export { default as SelectOption } from './SelectOption';
6+
export { default as SelectSeparator } from './SelectSeparator';
7+
export { default as SelectTrigger } from './SelectTrigger';
8+
9+
export * from './types';

0 commit comments

Comments
 (0)