-
Notifications
You must be signed in to change notification settings - Fork 2
/
combo-box.component.tsx
143 lines (135 loc) · 4.61 KB
/
combo-box.component.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { ReactNode, useState } from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from '@/utils/shadcn-ui.util';
import { Button, ButtonProps } from '@/components/shadcn-ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/shadcn-ui/popover';
import {
Command,
CommandEmpty,
CommandInput,
CommandItem,
CommandList,
} from '@/components/shadcn-ui/command';
import { PopoverProps } from '@radix-ui/react-popover';
export type ComboBoxLabelOption = { label: string };
export type ComboBoxOption = string | number | ComboBoxLabelOption;
export type ComboBoxProps<T> = {
/** Optional unique identifier */
id?: string;
/** Text label title for combobox */
/** List of available options for the dropdown menu */
options?: readonly T[];
/** Additional css classes to help with unique styling of the combo box */
className?: string;
/**
* The selected value(s) that the combo box currently holds. Must be shallow equal to one or more
* of the options entries.
*/
value?: T;
/** Triggers when content of textfield is changed */
onChange?: (newValue: T) => void;
/** Used to determine the string value for a given option. */
getOptionLabel?: (option: ComboBoxOption) => string;
/** Icon to be displayed on the trigger */
icon?: ReactNode;
/** Text displayed on button if `value` is undefined */
buttonPlaceholder?: string;
/** Placeholder text for text field */
textPlaceholder?: string;
/** Text to display when no options match input */
commandEmptyMessage?: string;
/** Variant of button */
buttonVariant?: ButtonProps['variant'];
/** Control how the popover menu should be aligned. Defaults to start */
alignDropDown?: 'start' | 'center' | 'end';
/** Text direction ltr or rtl */
dir?: Direction;
/** Optional boolean to set if trigger should be disabled */
isDisabled?: boolean;
} & PopoverProps;
type Direction = 'ltr' | 'rtl';
function getOptionLabelDefault(option: ComboBoxOption): string {
if (typeof option === 'string') {
return option;
}
if (typeof option === 'number') {
return option.toString();
}
return option.label;
}
/**
* Autocomplete input and command palette with a list of suggestions.
*
* Thanks to Shadcn for heavy inspiration and documentation
* https://ui.shadcn.com/docs/components/combobox
*/
function ComboBox<T extends ComboBoxOption = ComboBoxOption>({
id,
options = [],
className,
value,
onChange = () => {},
getOptionLabel = getOptionLabelDefault,
icon = undefined,
buttonPlaceholder = '',
textPlaceholder = '',
commandEmptyMessage = 'No option found',
buttonVariant = 'outline',
alignDropDown = 'start',
dir = 'ltr',
isDisabled = false,
...props
}: ComboBoxProps<T>) {
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen} {...props}>
<PopoverTrigger asChild>
<Button
variant={buttonVariant}
role="combobox"
aria-expanded={open}
id={id}
className={cn(
'tw-flex tw-w-[200px] tw-items-center tw-justify-between tw-overflow-hidden',
className,
)}
disabled={isDisabled}
>
<div className="tw-flex tw-flex-1 tw-items-center tw-overflow-hidden">
{icon && <div className="tw-pr-2">{icon}</div>}
<span className="tw-overflow-hidden tw-text-ellipsis tw-whitespace-nowrap">
{value ? getOptionLabel(value) : buttonPlaceholder}
</span>
</div>
<ChevronsUpDown className="tw-ms-2 tw-h-4 tw-w-4 tw-shrink-0 tw-opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align={alignDropDown} className="tw-w-[200px] tw-p-0" dir={dir}>
<Command>
<CommandInput dir={dir} placeholder={textPlaceholder} className="tw-text-inherit" />
<CommandEmpty>{commandEmptyMessage}</CommandEmpty>
<CommandList>
{options.map((option) => (
<CommandItem
key={getOptionLabel(option)}
value={getOptionLabel(option)}
onSelect={() => {
onChange(option);
setOpen(false);
}}
>
<Check
className={cn('tw-me-2 tw-h-4 tw-w-4', {
'tw-opacity-0': !value || getOptionLabel(value) !== getOptionLabel(option),
})}
/>
{getOptionLabel(option)}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
export default ComboBox;