@mbao01/common 0.0.43 → 0.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/types/components/DatetimePicker/DatetimeGrid.d.ts +26 -0
  2. package/dist/types/components/DatetimePicker/DatetimePicker.d.ts +13 -0
  3. package/dist/types/components/DatetimePicker/constants.d.ts +15 -0
  4. package/dist/types/components/DatetimePicker/index.d.ts +1 -0
  5. package/dist/types/components/DatetimePicker/types.d.ts +25 -0
  6. package/dist/types/components/Form/DatetimeInput/DatetimeCalendar.d.ts +5 -0
  7. package/dist/types/components/Form/DatetimeInput/DatetimeInput.d.ts +83 -0
  8. package/dist/types/components/Form/DatetimeInput/DatetimeInputContext.d.ts +2 -0
  9. package/dist/types/components/Form/DatetimeInput/NaturalLanguageInput.d.ts +5 -0
  10. package/dist/types/components/Form/DatetimeInput/TimePicker.d.ts +1 -0
  11. package/dist/types/components/Form/DatetimeInput/constants.d.ts +24 -0
  12. package/dist/types/components/Form/DatetimeInput/helpers.d.ts +27 -0
  13. package/dist/types/components/Form/DatetimeInput/hooks/index.d.ts +1 -0
  14. package/dist/types/components/Form/DatetimeInput/hooks/useDateInput/index.d.ts +1 -0
  15. package/dist/types/components/Form/DatetimeInput/hooks/useDateInput/useDateInput.d.ts +1 -0
  16. package/dist/types/components/Form/DatetimeInput/index.d.ts +1 -0
  17. package/dist/types/components/Form/DatetimeInput/types.d.ts +31 -0
  18. package/dist/types/components/Form/MultiSelect/MultiSelect.d.ts +46 -0
  19. package/dist/types/components/Form/MultiSelect/MultiSelectContext.d.ts +2 -0
  20. package/dist/types/components/Form/MultiSelect/constants.d.ts +19 -0
  21. package/dist/types/components/Form/MultiSelect/hooks/index.d.ts +1 -0
  22. package/dist/types/components/Form/MultiSelect/hooks/useMultiSelect/index.d.ts +1 -0
  23. package/dist/types/components/Form/MultiSelect/hooks/useMultiSelect/useMultiSelect.d.ts +1 -0
  24. package/dist/types/components/Form/MultiSelect/index.d.ts +1 -0
  25. package/dist/types/components/Form/MultiSelect/types.d.ts +31 -0
  26. package/dist/types/components/Form/TagsInput/TagsInput.d.ts +13 -0
  27. package/dist/types/components/Form/TagsInput/constants.d.ts +16 -0
  28. package/dist/types/components/Form/TagsInput/index.d.ts +1 -0
  29. package/dist/types/components/Form/TagsInput/types.d.ts +9 -0
  30. package/dist/types/components/Form/index.d.ts +2 -0
  31. package/dist/types/index.d.ts +1 -0
  32. package/package.json +4 -2
  33. package/src/components/DatetimePicker/DatetimeGrid.tsx +59 -0
  34. package/src/components/DatetimePicker/DatetimePicker.tsx +59 -0
  35. package/src/components/DatetimePicker/constants.ts +102 -0
  36. package/src/components/DatetimePicker/index.ts +1 -0
  37. package/src/components/DatetimePicker/types.ts +36 -0
  38. package/src/components/Form/DatetimeInput/DatetimeCalendar.tsx +68 -0
  39. package/src/components/Form/DatetimeInput/DatetimeInput.tsx +90 -0
  40. package/src/components/Form/DatetimeInput/DatetimeInputContext.tsx +4 -0
  41. package/src/components/Form/DatetimeInput/NaturalLanguageInput.tsx +73 -0
  42. package/src/components/Form/DatetimeInput/TimePicker.tsx +202 -0
  43. package/src/components/Form/DatetimeInput/constants.ts +135 -0
  44. package/src/components/Form/DatetimeInput/helpers.ts +93 -0
  45. package/src/components/Form/DatetimeInput/hooks/index.ts +1 -0
  46. package/src/components/Form/DatetimeInput/hooks/useDateInput/index.ts +1 -0
  47. package/src/components/Form/DatetimeInput/hooks/useDateInput/useDateInput.ts +10 -0
  48. package/src/components/Form/DatetimeInput/index.ts +1 -0
  49. package/src/components/Form/DatetimeInput/types.ts +36 -0
  50. package/src/components/Form/MultiSelect/MultiSelect.tsx +348 -0
  51. package/src/components/Form/MultiSelect/MultiSelectContext.tsx +4 -0
  52. package/src/components/Form/MultiSelect/constants.ts +103 -0
  53. package/src/components/Form/MultiSelect/hooks/index.ts +1 -0
  54. package/src/components/Form/MultiSelect/hooks/useMultiSelect/index.ts +1 -0
  55. package/src/components/Form/MultiSelect/hooks/useMultiSelect/useMultiSelect.ts +10 -0
  56. package/src/components/Form/MultiSelect/index.ts +1 -0
  57. package/src/components/Form/MultiSelect/types.ts +46 -0
  58. package/src/components/Form/TagsInput/TagsInput.tsx +278 -0
  59. package/src/components/Form/TagsInput/constants.ts +87 -0
  60. package/src/components/Form/TagsInput/index.ts +1 -0
  61. package/src/components/Form/TagsInput/types.ts +10 -0
  62. package/src/components/Form/index.ts +2 -0
  63. package/src/index.ts +1 -0
@@ -0,0 +1,135 @@
1
+ import { cva } from "../../../libs";
2
+
3
+ export const DEFAULT_SIZE = 96;
4
+
5
+ export const getDatetimeInputContainerClasses = cva(
6
+ `h-10 flex items-center justify-between w-fit px-2 rounded-md transition-all gap-1
7
+ [&:has(input:focus)]:duration-100 [&:has(input:focus)]:outline [&:has(input:focus)]:outline-2 [&:has(input:focus)]:outline-offset-2 [&:has(input:focus)]:outline-base-content/20
8
+ [&:has(input:focus-within)]:duration-100 [&:has(input:focus-within)]:outline [&:has(input:focus-within)]:outline-2 [&:has(input:focus-within)]:outline-offset-2 [&:has(input:focus-within)]:outline-base-content/20
9
+ `,
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: "border-0",
14
+ accent: "border border-accent",
15
+ error: "border border-error",
16
+ ghost: "border border-ghost",
17
+ info: "border border-info",
18
+ primary: "border border-primary",
19
+ secondary: "border border-secondary",
20
+ success: "border border-success",
21
+ warning: "border border-warning",
22
+ },
23
+ outline: {
24
+ true: "border",
25
+ },
26
+ disabled: {
27
+ true: "border-base-300",
28
+ },
29
+ wide: {
30
+ true: "w-full",
31
+ },
32
+ size: {
33
+ xs: "h-6 leading-relaxed text-xs px-1",
34
+ sm: "h-8 leading-8 text-sm px-1",
35
+ md: "h-12 leading-loose text-sm px-2",
36
+ lg: "h-16 leading-loose text-lg px-3",
37
+ },
38
+ },
39
+ compoundVariants: [
40
+ {
41
+ size: undefined,
42
+ className: "min-h-fit h-10",
43
+ },
44
+ {
45
+ variant: undefined,
46
+ outline: true,
47
+ className: "border-neutral-content",
48
+ },
49
+ {
50
+ variant: "default",
51
+ outline: true,
52
+ className: "border-base-content",
53
+ },
54
+ ],
55
+ }
56
+ );
57
+
58
+ export const getDatetimeCalendarTriggerClasses = cva(
59
+ `size-8 rounded-md p-1 flex items-center justify-center font-normal
60
+ outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-base-content focus-visible:ring-inset
61
+ `,
62
+ {
63
+ variants: {
64
+ size: {
65
+ xs: "size-4 p-0 rounded-sm",
66
+ sm: "size-6",
67
+ md: "size-9",
68
+ lg: "size-10",
69
+ },
70
+ },
71
+ }
72
+ );
73
+
74
+ export const getDatetimeCalendarIconClasses = cva("", {
75
+ variants: {
76
+ size: {
77
+ xs: "size-3",
78
+ sm: "size-3",
79
+ md: "size-5",
80
+ lg: "size-6",
81
+ },
82
+ },
83
+ compoundVariants: [
84
+ {
85
+ size: undefined,
86
+ className: "size-4",
87
+ },
88
+ ],
89
+ });
90
+
91
+ export const getTimePickerClasses = cva(
92
+ "border border-neutral-content bg-base-100 shadow-sm cursor-pointer ring-0 py-2 h-8 px-3 w-full text-sm outline-0 inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-base-content disabled:pointer-events-none disabled:opacity-50 hover:bg-base-200",
93
+ {
94
+ variants: {
95
+ selected: {
96
+ true: "bg-primary border-primary text-primary-content hover:border-neutral-content hover:text-base-content",
97
+ },
98
+ suggested: {
99
+ true: "bg-info border-info text-info-content hover:border-neutral-content hover:text-base-content",
100
+ },
101
+ },
102
+ }
103
+ );
104
+
105
+ export const getTimePickerListClasses = cva(
106
+ "flex items-center flex-col gap-1 h-full max-h-56 w-28 px-1 py-0.5"
107
+ );
108
+
109
+ export const getTimePickerScrollAreaClasses = cva(
110
+ "h-[90%] w-full focus-visible:outline-0 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-0 py-0.5"
111
+ );
112
+
113
+ export const getNaturalLanguageInputClasses = cva(
114
+ "flex-1 border-none rounded bg-transparent outline-none ring-0 focus:outline-none focus:ring-0 focus-within:outline-none focus-within:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
115
+ {
116
+ variants: {
117
+ size: {
118
+ xs: "h-4 px-1",
119
+ sm: "h-6 px-1",
120
+ md: "h-9 px-2",
121
+ lg: "h-11 px-3",
122
+ },
123
+ },
124
+ compoundVariants: [
125
+ {
126
+ size: undefined,
127
+ className: "h-8 px-2",
128
+ },
129
+ ],
130
+ }
131
+ );
132
+
133
+ export const getDatetimeCalendarClasses = cva(
134
+ "peer flex justify-end bg-transparent focus:outline-none focus:ring-0 focus-within:outline-none focus-within:ring-0 sm:text-sm disabled:cursor-not-allowed disabled:opacity-50"
135
+ );
@@ -0,0 +1,93 @@
1
+ import { parseDate } from "chrono-node";
2
+ import { type TimeString } from "./types";
3
+
4
+ /**
5
+ * Utility function that parses dates.
6
+ * Parses a given date string using the `chrono-node` library.
7
+ *
8
+ * @param str - A string representation of a date and time.
9
+ * @returns A `Date` object representing the parsed date and time, or `null` if the string could not be parsed.
10
+ */
11
+ export const parseDateTime = (str: Date | string) => {
12
+ if (str instanceof Date) {
13
+ return str;
14
+ }
15
+
16
+ return parseDate(str) ?? undefined;
17
+ };
18
+
19
+ /**
20
+ * Converts a given timestamp or the current date and time to a string representation in the local time zone.
21
+ * format: `HH:mm`, adjusted for the local time zone.
22
+ *
23
+ * @param timestamp {Date | string}
24
+ * @returns A string representation of the timestamp
25
+ */
26
+ export const getDateTimeLocal = (timestamp?: Date): string => {
27
+ const d = timestamp ? new Date(timestamp) : new Date();
28
+ if (d.toString() === "Invalid Date") return "";
29
+ return new Date(d.getTime() - d.getTimezoneOffset() * 60000)
30
+ .toISOString()
31
+ .split(":")
32
+ .slice(0, 2)
33
+ .join(":");
34
+ };
35
+
36
+ /**
37
+ * Formats a given date and time object or string into a human-readable string representation.
38
+ * "MMM D, YYYY h:mm A" (e.g. "Jan 1, 2023 12:00 PM").
39
+ *
40
+ * @param date - {Date | string}
41
+ * @returns A string representation of the date and time
42
+ */
43
+ export const formatDateTime = (date: Date | string, locale?: Intl.LocalesArgument) => {
44
+ return new Date(date).toLocaleTimeString(locale, {
45
+ month: "short",
46
+ day: "numeric",
47
+ year: "numeric",
48
+ hour: "numeric",
49
+ minute: "numeric",
50
+ hour12: true,
51
+ });
52
+ };
53
+
54
+ export const getParsedTime = (date: Date): TimeString => {
55
+ if (date) {
56
+ const PM_AM = date.getHours() >= 12 ? "PM" : "AM";
57
+ //fix the time format for this value
58
+
59
+ const PM_AM_hour = date.getHours();
60
+ const minute = date.getMinutes();
61
+
62
+ const hour =
63
+ PM_AM_hour > 12 ? PM_AM_hour % 12 : PM_AM_hour === 0 || PM_AM_hour === 12 ? 12 : PM_AM_hour;
64
+
65
+ return `${hour}:${minute ? minute : "00"} ${PM_AM}` as TimeString;
66
+ }
67
+
68
+ return "";
69
+ };
70
+
71
+ export const setDateTime = (date: Date, time: TimeString) => {
72
+ if (!time) {
73
+ return date;
74
+ }
75
+
76
+ const [HHMM, PM_AM] = time.split(" ");
77
+ const [HH, MM] = HHMM.split(":").map((v) => parseInt(v) || 0);
78
+
79
+ let hour = HH;
80
+ if (HH === 12) {
81
+ if (PM_AM === "AM") {
82
+ hour = 0;
83
+ }
84
+ } else if (HH < 12) {
85
+ if (PM_AM === "PM") {
86
+ hour += 12;
87
+ }
88
+ }
89
+ const newDate = new Date(date);
90
+ newDate.setHours(hour, MM);
91
+
92
+ return newDate;
93
+ };
@@ -0,0 +1 @@
1
+ export { useDateInput } from "./useDateInput";
@@ -0,0 +1 @@
1
+ export { useDateInput } from "./useDateInput";
@@ -0,0 +1,10 @@
1
+ import { useContext } from "react";
2
+ import { DatetimeInputContext } from "../../DatetimeInputContext";
3
+
4
+ export const useDateInput = () => {
5
+ const context = useContext(DatetimeInputContext);
6
+ if (!context) {
7
+ throw new Error("useDateInput must be used within SmartDateInputProvider");
8
+ }
9
+ return context;
10
+ };
@@ -0,0 +1 @@
1
+ export { DatetimeInput } from "./DatetimeInput";
@@ -0,0 +1,36 @@
1
+ import { type VariantProps } from "../../../libs";
2
+ import { type CalendarProps } from "../../Calendar/types";
3
+ import { type InputProps } from "../Input/types";
4
+ import { getDatetimeInputContainerClasses } from "./constants";
5
+
6
+ type DisabledType = { disabled?: boolean };
7
+ type VariantType = VariantProps<typeof getDatetimeInputContainerClasses>;
8
+ type NaturalLanguageInputType = DisabledType & {
9
+ locale?: Intl.LocalesArgument;
10
+ } & Omit<InputProps, "type" | "ref" | "value" | "defaultValue" | "onBlur" | "disabled">;
11
+ type CalendarType = Omit<CalendarProps, "mode" | "disabled">;
12
+
13
+ export type DatetimeInputProps = NaturalLanguageInputType &
14
+ VariantType & {
15
+ date?: Date;
16
+ calendar?: CalendarType;
17
+ defaultDate?: Date;
18
+ onDateChange?: (date: Date | undefined) => void;
19
+ };
20
+
21
+ export type DatetimeInputContextProps = {
22
+ value?: Date;
23
+ onDateChange: (date: Date | undefined) => void;
24
+ time: TimeString;
25
+ onTimeChange: (time: TimeString) => void;
26
+ };
27
+
28
+ export type DatetimeCalendarProps = VariantType & CalendarType & DisabledType;
29
+
30
+ export type NaturalLanguageInputProps = NaturalLanguageInputType;
31
+
32
+ type Hours = `${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12}`;
33
+ type Minutes = `${0 | 1 | 2 | 3 | 4 | 5}${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;
34
+ type PM_AM = "AM" | "PM";
35
+
36
+ export type TimeString = `${Hours}:${Minutes} ${PM_AM}` | "";
@@ -0,0 +1,348 @@
1
+ "use client";
2
+
3
+ import type { ComponentRef, KeyboardEvent, MouseEvent, SyntheticEvent } from "react";
4
+ import { forwardRef, useCallback, useRef, useState } from "react";
5
+ import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons";
6
+ import { Command as CommandPrimitive } from "cmdk";
7
+ import type {
8
+ Item,
9
+ MultiSelectContentProps,
10
+ MultiSelectInputProps,
11
+ MultiSelectItemProps,
12
+ MultiSelectListProps,
13
+ MultiSelectProps,
14
+ MultiSelectTriggerProps,
15
+ } from "./types";
16
+ import { cn } from "../../../utilities";
17
+ import { Badge } from "../../Badge";
18
+ import { Command } from "../../Command";
19
+ import {
20
+ getMultiSelectClasses,
21
+ getMultiSelectItemClasses,
22
+ getMultiSelectListClasses,
23
+ getMultiSelectTagClasses,
24
+ getMultiSelectTriggerClasses,
25
+ } from "./constants";
26
+ import { useMultiSelect } from "./hooks";
27
+ import { MultiSelectContext } from "./MultiSelectContext";
28
+
29
+ // TODO : expose the visibility of the popup
30
+ const MultiSelect = ({
31
+ values,
32
+ onValuesChange,
33
+ loop = false,
34
+ className,
35
+ children,
36
+ dir,
37
+ ...props
38
+ }: MultiSelectProps) => {
39
+ const [inputValue, setInputValue] = useState("");
40
+ const [open, setOpen] = useState<boolean>(false);
41
+ const [activeIndex, setActiveIndex] = useState<number>(-1);
42
+ const inputRef = useRef<HTMLInputElement>(null);
43
+ const [isValueSelected, setIsValueSelected] = useState(false);
44
+ const [selectedValue, setSelectedValue] = useState("");
45
+ const [items, setItems] = useState<Item[]>(
46
+ (values ?? []).map((value) => ({ value, label: undefined }))
47
+ );
48
+
49
+ const onValueChangeHandler = useCallback(
50
+ (value: string, label?: string) => {
51
+ if (values.includes(value)) {
52
+ setItems(items.filter(({ value: val }) => val !== value));
53
+ onValuesChange(values.filter((val) => val !== value));
54
+ } else {
55
+ setItems([...items, { value, label }]);
56
+ onValuesChange([...values, value]);
57
+ }
58
+ },
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ [values]
61
+ );
62
+
63
+ const handleSelect = useCallback(
64
+ (e: SyntheticEvent<HTMLInputElement>) => {
65
+ const target = e.currentTarget;
66
+ const selection = target.value.substring(
67
+ target.selectionStart ?? 0,
68
+ target.selectionEnd ?? 0
69
+ );
70
+
71
+ setSelectedValue(selection);
72
+ setIsValueSelected(selection === inputValue);
73
+ },
74
+ [inputValue]
75
+ );
76
+
77
+ const handleKeyDown = useCallback(
78
+ (e: KeyboardEvent<HTMLDivElement>) => {
79
+ const target = inputRef.current;
80
+
81
+ if (!target) return;
82
+
83
+ if (["ArrowLeft", "ArrowRight", "Backspace", "Delete", "Escape", "Enter"].includes(e.key)) {
84
+ e.stopPropagation();
85
+
86
+ const moveNext = () => {
87
+ const nextIndex = activeIndex + 1;
88
+ setActiveIndex(nextIndex > values.length - 1 ? (loop ? 0 : -1) : nextIndex);
89
+ };
90
+
91
+ const movePrev = () => {
92
+ const prevIndex = activeIndex - 1;
93
+ setActiveIndex(prevIndex < 0 ? values.length - 1 : prevIndex);
94
+ };
95
+
96
+ const moveCurrent = () => {
97
+ const newIndex =
98
+ activeIndex - 1 <= 0 ? (values.length - 1 === 0 ? -1 : 0) : activeIndex - 1;
99
+ setActiveIndex(newIndex);
100
+ };
101
+
102
+ switch (e.key) {
103
+ case "ArrowLeft":
104
+ if (dir === "rtl") {
105
+ if (values.length > 0 && (activeIndex !== -1 || loop)) {
106
+ moveNext();
107
+ }
108
+ } else {
109
+ if (values.length > 0 && target.selectionStart === 0) {
110
+ movePrev();
111
+ }
112
+ }
113
+ break;
114
+
115
+ case "ArrowRight":
116
+ if (dir === "rtl") {
117
+ if (values.length > 0 && target.selectionStart === 0) {
118
+ movePrev();
119
+ }
120
+ } else {
121
+ if (values.length > 0 && (activeIndex !== -1 || loop)) {
122
+ moveNext();
123
+ }
124
+ }
125
+ break;
126
+
127
+ case "Backspace":
128
+ case "Delete":
129
+ if (values.length > 0) {
130
+ if (activeIndex !== -1 && activeIndex < values.length) {
131
+ onValueChangeHandler(values[activeIndex]);
132
+ moveCurrent();
133
+ } else {
134
+ if (target.selectionStart === 0) {
135
+ if (selectedValue === inputValue || isValueSelected) {
136
+ onValueChangeHandler(values[values.length - 1]);
137
+ }
138
+ }
139
+ }
140
+ }
141
+ break;
142
+
143
+ case "Enter":
144
+ setOpen(true);
145
+ break;
146
+
147
+ case "Escape":
148
+ if (activeIndex !== -1) {
149
+ setActiveIndex(-1);
150
+ } else if (open) {
151
+ setOpen(false);
152
+ }
153
+ break;
154
+ }
155
+ }
156
+ },
157
+ // eslint-disable-next-line react-hooks/exhaustive-deps
158
+ [dir, values, inputValue, activeIndex, loop]
159
+ );
160
+
161
+ return (
162
+ <MultiSelectContext.Provider
163
+ value={{
164
+ items,
165
+ values,
166
+ onValueChange: onValueChangeHandler,
167
+ open,
168
+ setOpen,
169
+ inputValue,
170
+ setInputValue,
171
+ activeIndex,
172
+ setActiveIndex,
173
+ ref: inputRef,
174
+ handleSelect,
175
+ }}
176
+ >
177
+ <Command
178
+ onKeyDown={handleKeyDown}
179
+ className={cn(getMultiSelectClasses(), className)}
180
+ dir={dir}
181
+ {...props}
182
+ >
183
+ {children}
184
+ </Command>
185
+ </MultiSelectContext.Provider>
186
+ );
187
+ };
188
+
189
+ export const MultiSelectTrigger = ({
190
+ className,
191
+ children,
192
+ size,
193
+ wide,
194
+ outline,
195
+ variant,
196
+ disabled,
197
+ ...props
198
+ }: MultiSelectTriggerProps) => {
199
+ const { items, onValueChange, activeIndex } = useMultiSelect();
200
+
201
+ const handleMouseDown = useCallback((e: MouseEvent) => {
202
+ e.preventDefault();
203
+ e.stopPropagation();
204
+ }, []);
205
+
206
+ return (
207
+ <div
208
+ className={cn(
209
+ getMultiSelectTriggerClasses({ size, wide, outline, variant, disabled }),
210
+ className
211
+ )}
212
+ {...props}
213
+ >
214
+ {items.map((item, index) => {
215
+ const label = item.label ?? item.value;
216
+
217
+ return (
218
+ <Badge
219
+ key={item.value}
220
+ className={cn(
221
+ getMultiSelectTagClasses({ size }),
222
+ "px-1 rounded-xl flex items-center gap-1",
223
+ activeIndex === index && "ring-2 ring-muted-foreground "
224
+ )}
225
+ size={size}
226
+ variant={variant === "default" ? "neutral" : variant}
227
+ >
228
+ <span className="text-xs">{label}</span>
229
+ <button
230
+ type="button"
231
+ aria-label={`Remove ${label} option`}
232
+ aria-roledescription="button to remove option"
233
+ onMouseDown={handleMouseDown}
234
+ onClick={() => onValueChange(item.value, item.label)}
235
+ disabled={disabled}
236
+ >
237
+ <span className="sr-only">Remove {label} option</span>
238
+ <Cross2Icon className="h-4 w-4 hover:stroke-destructive" />
239
+ </button>
240
+ </Badge>
241
+ );
242
+ })}
243
+ {children}
244
+ </div>
245
+ );
246
+ };
247
+
248
+ MultiSelectTrigger.displayName = "MultiSelectTrigger";
249
+
250
+ const MultiSelectInput = ({ className, ...props }: MultiSelectInputProps) => {
251
+ const {
252
+ setOpen,
253
+ inputValue,
254
+ setInputValue,
255
+ activeIndex,
256
+ setActiveIndex,
257
+ handleSelect,
258
+ ref: inputRef,
259
+ } = useMultiSelect();
260
+
261
+ return (
262
+ <CommandPrimitive.Input
263
+ {...props}
264
+ tabIndex={0}
265
+ ref={inputRef}
266
+ value={inputValue}
267
+ onValueChange={activeIndex === -1 ? setInputValue : undefined}
268
+ onSelect={handleSelect}
269
+ onBlur={() => setOpen(false)}
270
+ onFocus={() => setOpen(true)}
271
+ onClick={() => setActiveIndex(-1)}
272
+ className={cn(
273
+ "bg-transparent outline-none flex-1",
274
+ className,
275
+ activeIndex !== -1 && "caret-transparent"
276
+ )}
277
+ />
278
+ );
279
+ };
280
+
281
+ MultiSelectInput.displayName = "MultiSelectInput";
282
+
283
+ const MultiSelectContent = forwardRef<HTMLDivElement, MultiSelectContentProps>(
284
+ ({ children }, ref) => {
285
+ const { open } = useMultiSelect();
286
+ return (
287
+ <div ref={ref} className="relative">
288
+ {open && children}
289
+ </div>
290
+ );
291
+ }
292
+ );
293
+
294
+ MultiSelectContent.displayName = "MultiSelectContent";
295
+
296
+ const MultiSelectList = forwardRef<ComponentRef<typeof Command.List>, MultiSelectListProps>(
297
+ ({ className, size, children }, ref) => {
298
+ return (
299
+ <Command.List ref={ref} className={cn(getMultiSelectListClasses({ size }), className)}>
300
+ {children}
301
+ <Command.Empty>
302
+ <span className="text-muted-foreground">No results found</span>
303
+ </Command.Empty>
304
+ </Command.List>
305
+ );
306
+ }
307
+ );
308
+
309
+ MultiSelectList.displayName = "MultiSelectList";
310
+
311
+ const MultiSelectItem = forwardRef<ComponentRef<typeof Command.Item>, MultiSelectItemProps>(
312
+ ({ className, value, label, children, size, disabled, ...props }, ref) => {
313
+ const { values: options, onValueChange, setInputValue } = useMultiSelect();
314
+
315
+ const mousePreventDefault = useCallback((e: MouseEvent) => {
316
+ e.preventDefault();
317
+ e.stopPropagation();
318
+ }, []);
319
+
320
+ const included = options.includes(value);
321
+ return (
322
+ <Command.Item
323
+ ref={ref}
324
+ {...props}
325
+ disabled={disabled}
326
+ onSelect={() => {
327
+ onValueChange(value, label);
328
+ setInputValue("");
329
+ }}
330
+ className={cn(getMultiSelectItemClasses({ included, disabled, size }), className)}
331
+ onMouseDown={mousePreventDefault}
332
+ >
333
+ {children}
334
+ {included && <CheckIcon className="h-4 w-4" />}
335
+ </Command.Item>
336
+ );
337
+ }
338
+ );
339
+
340
+ MultiSelectItem.displayName = "MultiSelectItem";
341
+
342
+ MultiSelect.Trigger = MultiSelectTrigger;
343
+ MultiSelect.Input = MultiSelectInput;
344
+ MultiSelect.Content = MultiSelectContent;
345
+ MultiSelect.List = MultiSelectList;
346
+ MultiSelect.Item = MultiSelectItem;
347
+
348
+ export { MultiSelect };
@@ -0,0 +1,4 @@
1
+ import { createContext } from "react";
2
+ import { type MultiSelectContextProps } from "./types";
3
+
4
+ export const MultiSelectContext = createContext<MultiSelectContextProps | null>(null);