@arolariu/components 1.0.0 → 1.1.0
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.
- package/CHANGELOG.md +57 -0
- package/EXAMPLES.md +2510 -0
- package/dist/components/ui/alert-dialog.d.ts +4 -16
- package/dist/components/ui/alert-dialog.d.ts.map +1 -1
- package/dist/components/ui/alert-dialog.js +18 -14
- package/dist/components/ui/alert-dialog.js.map +1 -1
- package/dist/components/ui/avatar.d.ts +3 -12
- package/dist/components/ui/avatar.d.ts.map +1 -1
- package/dist/components/ui/avatar.js +18 -15
- package/dist/components/ui/avatar.js.map +1 -1
- package/dist/components/ui/button-group.d.ts +1 -1
- package/dist/components/ui/button-group.d.ts.map +1 -1
- package/dist/components/ui/calendar.d.ts +1 -4
- package/dist/components/ui/calendar.d.ts.map +1 -1
- package/dist/components/ui/calendar.js +7 -7
- package/dist/components/ui/calendar.js.map +1 -1
- package/dist/components/ui/carousel.d.ts.map +1 -1
- package/dist/components/ui/carousel.js.map +1 -1
- package/dist/components/ui/chart.d.ts.map +1 -1
- package/dist/components/ui/chart.js +125 -59
- package/dist/components/ui/chart.js.map +1 -1
- package/dist/components/ui/checkbox-group.d.ts +2 -6
- package/dist/components/ui/checkbox-group.d.ts.map +1 -1
- package/dist/components/ui/checkbox-group.js +8 -7
- package/dist/components/ui/checkbox-group.js.map +1 -1
- package/dist/components/ui/checkbox.d.ts +3 -1
- package/dist/components/ui/checkbox.d.ts.map +1 -1
- package/dist/components/ui/checkbox.js +4 -1
- package/dist/components/ui/checkbox.js.map +1 -1
- package/dist/components/ui/collapsible.d.ts.map +1 -1
- package/dist/components/ui/collapsible.js.map +1 -1
- package/dist/components/ui/combobox.d.ts +335 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +206 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/combobox.module.js +23 -0
- package/dist/components/ui/combobox.module.js.map +1 -0
- package/dist/components/ui/combobox_module.css +142 -0
- package/dist/components/ui/combobox_module.css.map +1 -0
- package/dist/components/ui/command.d.ts.map +1 -1
- package/dist/components/ui/command.js +25 -16
- package/dist/components/ui/command.js.map +1 -1
- package/dist/components/ui/context-menu.d.ts.map +1 -1
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/components/ui/drawer.d.ts.map +1 -1
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.js.map +1 -1
- package/dist/components/ui/dropdrawer.d.ts +10 -16
- package/dist/components/ui/dropdrawer.d.ts.map +1 -1
- package/dist/components/ui/dropdrawer.js +28 -20
- package/dist/components/ui/dropdrawer.js.map +1 -1
- package/dist/components/ui/item.d.ts +1 -1
- package/dist/components/ui/item.d.ts.map +1 -1
- package/dist/components/ui/menubar.d.ts +11 -13
- package/dist/components/ui/menubar.d.ts.map +1 -1
- package/dist/components/ui/menubar.js.map +1 -1
- package/dist/components/ui/meter.d.ts +8 -24
- package/dist/components/ui/meter.d.ts.map +1 -1
- package/dist/components/ui/meter.js +23 -19
- package/dist/components/ui/meter.js.map +1 -1
- package/dist/components/ui/navigation-menu.d.ts +3 -12
- package/dist/components/ui/navigation-menu.d.ts.map +1 -1
- package/dist/components/ui/navigation-menu.js +14 -11
- package/dist/components/ui/navigation-menu.js.map +1 -1
- package/dist/components/ui/number-field.d.ts +6 -12
- package/dist/components/ui/number-field.d.ts.map +1 -1
- package/dist/components/ui/number-field.js.map +1 -1
- package/dist/components/ui/progress.d.ts +1 -4
- package/dist/components/ui/progress.d.ts.map +1 -1
- package/dist/components/ui/progress.js +10 -9
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/radio-group.d.ts +2 -4
- package/dist/components/ui/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group.js.map +1 -1
- package/dist/components/ui/resizable.d.ts +3 -3
- package/dist/components/ui/resizable.d.ts.map +1 -1
- package/dist/components/ui/resizable.js.map +1 -1
- package/dist/components/ui/scratcher.d.ts +1 -1
- package/dist/components/ui/scratcher.d.ts.map +1 -1
- package/dist/components/ui/scratcher.js +5 -4
- package/dist/components/ui/scratcher.js.map +1 -1
- package/dist/components/ui/scroll-area.d.ts +2 -4
- package/dist/components/ui/scroll-area.d.ts.map +1 -1
- package/dist/components/ui/scroll-area.js.map +1 -1
- package/dist/components/ui/separator.d.ts +1 -4
- package/dist/components/ui/separator.d.ts.map +1 -1
- package/dist/components/ui/separator.js +9 -8
- package/dist/components/ui/separator.js.map +1 -1
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet.js.map +1 -1
- package/dist/components/ui/sidebar.d.ts +1 -1
- package/dist/components/ui/sidebar.d.ts.map +1 -1
- package/dist/components/ui/sidebar.js.map +1 -1
- package/dist/components/ui/sonner.d.ts +5 -4
- package/dist/components/ui/sonner.d.ts.map +1 -1
- package/dist/components/ui/sonner.js +7 -6
- package/dist/components/ui/sonner.js.map +1 -1
- package/dist/components/ui/toggle-group.d.ts +2 -8
- package/dist/components/ui/toggle-group.d.ts.map +1 -1
- package/dist/components/ui/toggle-group.js +12 -10
- package/dist/components/ui/toggle-group.js.map +1 -1
- package/dist/components/ui/toolbar.d.ts +10 -30
- package/dist/components/ui/toolbar.d.ts.map +1 -1
- package/dist/components/ui/toolbar.js +28 -23
- package/dist/components/ui/toolbar.js.map +1 -1
- package/dist/hooks/useClipboard.d.ts +77 -0
- package/dist/hooks/useClipboard.d.ts.map +1 -0
- package/dist/hooks/useClipboard.js +42 -0
- package/dist/hooks/useClipboard.js.map +1 -0
- package/dist/hooks/useControllableState.d.ts +54 -0
- package/dist/hooks/useControllableState.d.ts.map +1 -0
- package/dist/hooks/useControllableState.js +29 -0
- package/dist/hooks/useControllableState.js.map +1 -0
- package/dist/hooks/useDebounce.d.ts +33 -0
- package/dist/hooks/useDebounce.d.ts.map +1 -0
- package/dist/hooks/useDebounce.js +20 -0
- package/dist/hooks/useDebounce.js.map +1 -0
- package/dist/hooks/useEventCallback.d.ts +34 -0
- package/dist/hooks/useEventCallback.d.ts.map +1 -0
- package/dist/hooks/useEventCallback.js +12 -0
- package/dist/hooks/useEventCallback.js.map +1 -0
- package/dist/hooks/useId.d.ts +30 -0
- package/dist/hooks/useId.d.ts.map +1 -0
- package/dist/hooks/useId.js +9 -0
- package/dist/hooks/useId.js.map +1 -0
- package/dist/hooks/useIntersectionObserver.d.ts +51 -0
- package/dist/hooks/useIntersectionObserver.d.ts.map +1 -0
- package/dist/hooks/useIntersectionObserver.js +25 -0
- package/dist/hooks/useIntersectionObserver.js.map +1 -0
- package/dist/hooks/useInterval.d.ts +55 -0
- package/dist/hooks/useInterval.d.ts.map +1 -0
- package/dist/hooks/useInterval.js +24 -0
- package/dist/hooks/useInterval.js.map +1 -0
- package/dist/hooks/useLocalStorage.d.ts +43 -0
- package/dist/hooks/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/useLocalStorage.js +53 -0
- package/dist/hooks/useLocalStorage.js.map +1 -0
- package/dist/hooks/useMergedRefs.d.ts +27 -0
- package/dist/hooks/useMergedRefs.d.ts.map +1 -0
- package/dist/hooks/useMergedRefs.js +11 -0
- package/dist/hooks/useMergedRefs.js.map +1 -0
- package/dist/hooks/useOnClickOutside.d.ts +32 -0
- package/dist/hooks/useOnClickOutside.d.ts.map +1 -0
- package/dist/hooks/useOnClickOutside.js +23 -0
- package/dist/hooks/useOnClickOutside.js.map +1 -0
- package/dist/hooks/usePrevious.d.ts +33 -0
- package/dist/hooks/usePrevious.d.ts.map +1 -0
- package/dist/hooks/usePrevious.js +14 -0
- package/dist/hooks/usePrevious.js.map +1 -0
- package/dist/hooks/useThrottle.d.ts +37 -0
- package/dist/hooks/useThrottle.d.ts.map +1 -0
- package/dist/hooks/useThrottle.js +34 -0
- package/dist/hooks/useThrottle.js.map +1 -0
- package/dist/hooks/useTimeout.d.ts +28 -0
- package/dist/hooks/useTimeout.d.ts.map +1 -0
- package/dist/hooks/useTimeout.js +24 -0
- package/dist/hooks/useTimeout.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/lib/utilities.d.ts +2 -3
- package/dist/lib/utilities.d.ts.map +1 -1
- package/dist/lib/utilities.js.map +1 -1
- package/dist/motion/tokens.js +5 -5
- package/dist/motion/tokens.js.map +1 -1
- package/dist/rslib-runtime.js +39 -0
- package/dist/rslib-runtime.js.map +1 -0
- package/package.json +82 -3
- package/src/components/ui/alert-dialog.tsx +15 -8
- package/src/components/ui/avatar.tsx +9 -6
- package/src/components/ui/calendar.tsx +7 -13
- package/src/components/ui/carousel.tsx +2 -0
- package/src/components/ui/chart.tsx +63 -60
- package/src/components/ui/checkbox-group.tsx +4 -5
- package/src/components/ui/checkbox.tsx +10 -2
- package/src/components/ui/collapsible.tsx +1 -0
- package/src/components/ui/combobox.module.css +158 -0
- package/src/components/ui/combobox.tsx +569 -0
- package/src/components/ui/command.tsx +31 -15
- package/src/components/ui/context-menu.tsx +3 -0
- package/src/components/ui/drawer.tsx +2 -0
- package/src/components/ui/dropdown-menu.tsx +3 -0
- package/src/components/ui/dropdrawer.tsx +80 -62
- package/src/components/ui/menubar.tsx +9 -10
- package/src/components/ui/meter.tsx +16 -17
- package/src/components/ui/navigation-menu.tsx +41 -33
- package/src/components/ui/number-field.tsx +6 -13
- package/src/components/ui/progress.tsx +3 -2
- package/src/components/ui/radio-group.tsx +2 -5
- package/src/components/ui/resizable.tsx +2 -2
- package/src/components/ui/scratcher.tsx +6 -10
- package/src/components/ui/scroll-area.tsx +2 -5
- package/src/components/ui/separator.tsx +4 -3
- package/src/components/ui/sheet.tsx +3 -0
- package/src/components/ui/sidebar.tsx +1 -0
- package/src/components/ui/sonner.tsx +20 -12
- package/src/components/ui/toggle-group.tsx +6 -4
- package/src/components/ui/toolbar.tsx +20 -21
- package/src/hooks/useClipboard.tsx +137 -0
- package/src/hooks/useControllableState.tsx +81 -0
- package/src/hooks/useDebounce.tsx +50 -0
- package/src/hooks/useEventCallback.tsx +47 -0
- package/src/hooks/useId.tsx +36 -0
- package/src/hooks/useIntersectionObserver.tsx +81 -0
- package/src/hooks/useInterval.tsx +80 -0
- package/src/hooks/useLocalStorage.tsx +111 -0
- package/src/hooks/useMergedRefs.tsx +48 -0
- package/src/hooks/useOnClickOutside.tsx +55 -0
- package/src/hooks/usePrevious.tsx +44 -0
- package/src/hooks/useThrottle.tsx +78 -0
- package/src/hooks/useTimeout.tsx +51 -0
- package/src/index.ts +23 -0
- package/src/lib/utilities.ts +4 -4
- package/src/motion/tokens.ts +4 -4
- package/src/stories/DesignPrinciples.mdx +48 -0
- package/src/stories/GettingStarted.mdx +92 -0
- package/src/stories/Welcome.mdx +44 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {Check, ChevronsUpDown} from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import {useControllableState} from "@/hooks/useControllableState";
|
|
7
|
+
import {cn} from "@/lib/utilities";
|
|
8
|
+
|
|
9
|
+
import {Button} from "./button";
|
|
10
|
+
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator} from "./command";
|
|
11
|
+
import {Popover, PopoverContent, PopoverTrigger} from "./popover";
|
|
12
|
+
|
|
13
|
+
import styles from "./combobox.module.css";
|
|
14
|
+
|
|
15
|
+
interface ComboboxContextValue {
|
|
16
|
+
value: string;
|
|
17
|
+
onValueChange: (value: string) => void;
|
|
18
|
+
open: boolean;
|
|
19
|
+
setOpen: (open: boolean) => void;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
searchPlaceholder?: string;
|
|
22
|
+
emptyMessage?: string;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
itemLabels: Map<string, string>;
|
|
25
|
+
registerItem: (value: string, label: string) => void;
|
|
26
|
+
unregisterItem: (value: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ComboboxContext = React.createContext<ComboboxContextValue | null>(null);
|
|
30
|
+
|
|
31
|
+
function useComboboxContext(componentName: string): ComboboxContextValue {
|
|
32
|
+
const context = React.useContext(ComboboxContext);
|
|
33
|
+
|
|
34
|
+
if (!context) {
|
|
35
|
+
throw new Error(`${componentName} must be used within Combobox.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return context;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ComboboxProps {
|
|
42
|
+
/**
|
|
43
|
+
* The controlled selected value.
|
|
44
|
+
* @default undefined
|
|
45
|
+
*/
|
|
46
|
+
value?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The default value when uncontrolled.
|
|
49
|
+
* @default ""
|
|
50
|
+
*/
|
|
51
|
+
defaultValue?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Callback fired when the selected value changes.
|
|
54
|
+
* @default undefined
|
|
55
|
+
*/
|
|
56
|
+
onValueChange?: (value: string) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Whether the popover is controlled open state.
|
|
59
|
+
* @default undefined
|
|
60
|
+
*/
|
|
61
|
+
open?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the popover is open by default (uncontrolled).
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
defaultOpen?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Callback fired when the open state changes.
|
|
69
|
+
* @default undefined
|
|
70
|
+
*/
|
|
71
|
+
onOpenChange?: (open: boolean) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Placeholder text shown when no value is selected.
|
|
74
|
+
* @default "Select an item..."
|
|
75
|
+
*/
|
|
76
|
+
placeholder?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Placeholder text shown in the search input.
|
|
79
|
+
* @default "Search..."
|
|
80
|
+
*/
|
|
81
|
+
searchPlaceholder?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Message shown when no items match the search.
|
|
84
|
+
* @default "No items found."
|
|
85
|
+
*/
|
|
86
|
+
emptyMessage?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Whether the combobox is disabled.
|
|
89
|
+
* @default false
|
|
90
|
+
*/
|
|
91
|
+
disabled?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Additional CSS classes merged with the combobox styles.
|
|
94
|
+
* @default undefined
|
|
95
|
+
*/
|
|
96
|
+
className?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Combobox content and items.
|
|
99
|
+
* @default undefined
|
|
100
|
+
*/
|
|
101
|
+
children?: React.ReactNode;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface ComboboxTriggerProps {
|
|
105
|
+
/**
|
|
106
|
+
* Additional CSS classes merged with the trigger styles.
|
|
107
|
+
* @default undefined
|
|
108
|
+
*/
|
|
109
|
+
className?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Trigger content. If not provided, shows selected item label or placeholder.
|
|
112
|
+
* @default undefined
|
|
113
|
+
*/
|
|
114
|
+
children?: React.ReactNode;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface ComboboxContentProps {
|
|
118
|
+
/**
|
|
119
|
+
* Additional CSS classes merged with the content styles.
|
|
120
|
+
* @default undefined
|
|
121
|
+
*/
|
|
122
|
+
className?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Content children (typically ComboboxItem components).
|
|
125
|
+
* @default undefined
|
|
126
|
+
*/
|
|
127
|
+
children?: React.ReactNode;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface ComboboxItemProps {
|
|
131
|
+
/**
|
|
132
|
+
* The value associated with this item.
|
|
133
|
+
*/
|
|
134
|
+
value: string;
|
|
135
|
+
/**
|
|
136
|
+
* Additional CSS classes merged with the item styles.
|
|
137
|
+
* @default undefined
|
|
138
|
+
*/
|
|
139
|
+
className?: string;
|
|
140
|
+
/**
|
|
141
|
+
* Item content (label).
|
|
142
|
+
* @default undefined
|
|
143
|
+
*/
|
|
144
|
+
children?: React.ReactNode;
|
|
145
|
+
/**
|
|
146
|
+
* Whether the item is disabled.
|
|
147
|
+
* @default false
|
|
148
|
+
*/
|
|
149
|
+
disabled?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* Callback fired when the item is selected.
|
|
152
|
+
* @default undefined
|
|
153
|
+
*/
|
|
154
|
+
onSelect?: (value: string) => void;
|
|
155
|
+
/**
|
|
156
|
+
* Additional search keywords for filtering.
|
|
157
|
+
* @default []
|
|
158
|
+
*/
|
|
159
|
+
keywords?: string[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface ComboboxEmptyProps {
|
|
163
|
+
/**
|
|
164
|
+
* Additional CSS classes merged with the empty state styles.
|
|
165
|
+
* @default undefined
|
|
166
|
+
*/
|
|
167
|
+
className?: string;
|
|
168
|
+
/**
|
|
169
|
+
* Content shown when no items match. Defaults to context emptyMessage.
|
|
170
|
+
* @default undefined
|
|
171
|
+
*/
|
|
172
|
+
children?: React.ReactNode;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface ComboboxGroupProps {
|
|
176
|
+
/**
|
|
177
|
+
* Group heading text.
|
|
178
|
+
* @default undefined
|
|
179
|
+
*/
|
|
180
|
+
heading?: string;
|
|
181
|
+
/**
|
|
182
|
+
* Additional CSS classes merged with the group styles.
|
|
183
|
+
* @default undefined
|
|
184
|
+
*/
|
|
185
|
+
className?: string;
|
|
186
|
+
/**
|
|
187
|
+
* Group items.
|
|
188
|
+
* @default undefined
|
|
189
|
+
*/
|
|
190
|
+
children?: React.ReactNode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface ComboboxSeparatorProps {
|
|
194
|
+
/**
|
|
195
|
+
* Additional CSS classes merged with the separator styles.
|
|
196
|
+
* @default undefined
|
|
197
|
+
*/
|
|
198
|
+
className?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A searchable select component combining Command, Popover, and Button.
|
|
203
|
+
*
|
|
204
|
+
* @remarks
|
|
205
|
+
* - Composes Command (search), Popover (positioning), and Button (trigger)
|
|
206
|
+
* - Supports both controlled and uncontrolled modes
|
|
207
|
+
* - Provides keyboard navigation and filtering
|
|
208
|
+
* - Built with Base UI primitives and CSS Modules
|
|
209
|
+
*
|
|
210
|
+
* @example Basic usage
|
|
211
|
+
* ```tsx
|
|
212
|
+
* <Combobox value={value} onValueChange={setValue}>
|
|
213
|
+
* <ComboboxTrigger />
|
|
214
|
+
* <ComboboxContent>
|
|
215
|
+
* <ComboboxItem value="apple">Apple</ComboboxItem>
|
|
216
|
+
* <ComboboxItem value="banana">Banana</ComboboxItem>
|
|
217
|
+
* </ComboboxContent>
|
|
218
|
+
* </Combobox>
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* @example With groups
|
|
222
|
+
* ```tsx
|
|
223
|
+
* <Combobox>
|
|
224
|
+
* <ComboboxTrigger />
|
|
225
|
+
* <ComboboxContent>
|
|
226
|
+
* <ComboboxGroup heading="Fruits">
|
|
227
|
+
* <ComboboxItem value="apple">Apple</ComboboxItem>
|
|
228
|
+
* </ComboboxGroup>
|
|
229
|
+
* <ComboboxSeparator />
|
|
230
|
+
* <ComboboxGroup heading="Vegetables">
|
|
231
|
+
* <ComboboxItem value="carrot">Carrot</ComboboxItem>
|
|
232
|
+
* </ComboboxGroup>
|
|
233
|
+
* </ComboboxContent>
|
|
234
|
+
* </Combobox>
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
function Combobox(props: Readonly<Combobox.Props>): React.ReactElement {
|
|
238
|
+
const {
|
|
239
|
+
value: controlledValue,
|
|
240
|
+
defaultValue = "",
|
|
241
|
+
onValueChange,
|
|
242
|
+
open: controlledOpen,
|
|
243
|
+
defaultOpen = false,
|
|
244
|
+
onOpenChange,
|
|
245
|
+
placeholder = "Select an item...",
|
|
246
|
+
searchPlaceholder = "Search...",
|
|
247
|
+
emptyMessage = "No items found.",
|
|
248
|
+
disabled = false,
|
|
249
|
+
className,
|
|
250
|
+
children,
|
|
251
|
+
} = props;
|
|
252
|
+
|
|
253
|
+
const [value, setValue] = useControllableState({
|
|
254
|
+
controlled: controlledValue,
|
|
255
|
+
defaultValue,
|
|
256
|
+
onChange: onValueChange,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const [open, setOpen] = useControllableState({
|
|
260
|
+
controlled: controlledOpen,
|
|
261
|
+
defaultValue: defaultOpen,
|
|
262
|
+
onChange: onOpenChange,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const itemLabelsRef = React.useRef(new Map<string, string>());
|
|
266
|
+
|
|
267
|
+
const registerItem = React.useCallback((itemValue: string, label: string) => {
|
|
268
|
+
itemLabelsRef.current.set(itemValue, label);
|
|
269
|
+
}, []);
|
|
270
|
+
|
|
271
|
+
const unregisterItem = React.useCallback((itemValue: string) => {
|
|
272
|
+
itemLabelsRef.current.delete(itemValue);
|
|
273
|
+
}, []);
|
|
274
|
+
|
|
275
|
+
const contextValue = React.useMemo<ComboboxContextValue>(
|
|
276
|
+
() => ({
|
|
277
|
+
value,
|
|
278
|
+
onValueChange: setValue,
|
|
279
|
+
open,
|
|
280
|
+
setOpen,
|
|
281
|
+
placeholder,
|
|
282
|
+
searchPlaceholder,
|
|
283
|
+
emptyMessage,
|
|
284
|
+
disabled,
|
|
285
|
+
itemLabels: itemLabelsRef.current,
|
|
286
|
+
registerItem,
|
|
287
|
+
unregisterItem,
|
|
288
|
+
}),
|
|
289
|
+
[value, setValue, open, setOpen, placeholder, searchPlaceholder, emptyMessage, disabled, registerItem, unregisterItem],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<ComboboxContext.Provider value={contextValue}>
|
|
294
|
+
<Popover
|
|
295
|
+
open={open}
|
|
296
|
+
onOpenChange={setOpen}>
|
|
297
|
+
<div className={cn(styles.combobox, className)}>{children}</div>
|
|
298
|
+
</Popover>
|
|
299
|
+
</ComboboxContext.Provider>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
Combobox.displayName = "Combobox";
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Button that opens and closes the combobox popover.
|
|
306
|
+
*
|
|
307
|
+
* @remarks
|
|
308
|
+
* - Renders as a Button with trigger behavior
|
|
309
|
+
* - Shows selected item label or placeholder
|
|
310
|
+
* - Supports custom children or auto-display
|
|
311
|
+
*
|
|
312
|
+
* @example Basic usage
|
|
313
|
+
* ```tsx
|
|
314
|
+
* <ComboboxTrigger />
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* @example Custom content
|
|
318
|
+
* ```tsx
|
|
319
|
+
* <ComboboxTrigger>
|
|
320
|
+
* {selectedLabel || "Choose..."}
|
|
321
|
+
* </ComboboxTrigger>
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
const ComboboxTrigger = React.forwardRef<HTMLButtonElement, ComboboxTrigger.Props>(
|
|
325
|
+
(props: Readonly<ComboboxTrigger.Props>, ref): React.ReactElement => {
|
|
326
|
+
const {className, children} = props;
|
|
327
|
+
const {open, setOpen, value, placeholder, disabled, itemLabels} = useComboboxContext("ComboboxTrigger");
|
|
328
|
+
|
|
329
|
+
// Force re-render when value changes to update the selected label
|
|
330
|
+
const [, forceUpdate] = React.useReducer((x: number) => x + 1, 0);
|
|
331
|
+
|
|
332
|
+
React.useEffect(() => {
|
|
333
|
+
forceUpdate();
|
|
334
|
+
}, [value]);
|
|
335
|
+
|
|
336
|
+
const selectedLabel = itemLabels.get(value) || "";
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<PopoverTrigger asChild>
|
|
340
|
+
<Button
|
|
341
|
+
ref={ref}
|
|
342
|
+
variant='outline'
|
|
343
|
+
role='combobox'
|
|
344
|
+
aria-expanded={open}
|
|
345
|
+
disabled={disabled}
|
|
346
|
+
className={cn(styles.trigger, className)}
|
|
347
|
+
onClick={() => setOpen(!open)}>
|
|
348
|
+
{children ?? (
|
|
349
|
+
<>
|
|
350
|
+
<span className={cn(styles.triggerValue, !selectedLabel && styles.triggerPlaceholder)}>{selectedLabel || placeholder}</span>
|
|
351
|
+
<ChevronsUpDown className={styles.triggerIcon} />
|
|
352
|
+
</>
|
|
353
|
+
)}
|
|
354
|
+
</Button>
|
|
355
|
+
</PopoverTrigger>
|
|
356
|
+
);
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
ComboboxTrigger.displayName = "ComboboxTrigger";
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The popover content containing the searchable command list.
|
|
363
|
+
*
|
|
364
|
+
* @remarks
|
|
365
|
+
* - Wraps Command with Popover positioning
|
|
366
|
+
* - Includes search input and items list
|
|
367
|
+
* - Automatically closes on item selection
|
|
368
|
+
*
|
|
369
|
+
* @example Basic usage
|
|
370
|
+
* ```tsx
|
|
371
|
+
* <ComboboxContent>
|
|
372
|
+
* <ComboboxItem value="item1">Item 1</ComboboxItem>
|
|
373
|
+
* </ComboboxContent>
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
const ComboboxContent = React.forwardRef<HTMLDivElement, ComboboxContent.Props>(
|
|
377
|
+
(props: Readonly<ComboboxContent.Props>, ref): React.ReactElement => {
|
|
378
|
+
const {className, children} = props;
|
|
379
|
+
const {searchPlaceholder} = useComboboxContext("ComboboxContent");
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<PopoverContent
|
|
383
|
+
ref={ref}
|
|
384
|
+
className={cn(styles.content, className)}
|
|
385
|
+
sideOffset={4}>
|
|
386
|
+
<Command className={styles.command}>
|
|
387
|
+
<CommandInput
|
|
388
|
+
placeholder={searchPlaceholder}
|
|
389
|
+
className={styles.commandInput}
|
|
390
|
+
/>
|
|
391
|
+
<CommandList className={styles.commandList}>
|
|
392
|
+
<ComboboxEmpty />
|
|
393
|
+
{children}
|
|
394
|
+
</CommandList>
|
|
395
|
+
</Command>
|
|
396
|
+
</PopoverContent>
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
ComboboxContent.displayName = "ComboboxContent";
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* A selectable item within the combobox.
|
|
404
|
+
*
|
|
405
|
+
* @remarks
|
|
406
|
+
* - Uses CommandItem internally
|
|
407
|
+
* - Shows check icon when selected
|
|
408
|
+
* - Closes popover on selection
|
|
409
|
+
*
|
|
410
|
+
* @example Basic usage
|
|
411
|
+
* ```tsx
|
|
412
|
+
* <ComboboxItem value="apple">Apple</ComboboxItem>
|
|
413
|
+
* ```
|
|
414
|
+
*
|
|
415
|
+
* @example With custom select handler
|
|
416
|
+
* ```tsx
|
|
417
|
+
* <ComboboxItem
|
|
418
|
+
* value="apple"
|
|
419
|
+
* onSelect={(value) => console.log("Selected:", value)}
|
|
420
|
+
* >
|
|
421
|
+
* Apple
|
|
422
|
+
* </ComboboxItem>
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
function ComboboxItem(props: Readonly<ComboboxItem.Props>): React.ReactElement {
|
|
426
|
+
const {value: itemValue, className, children, disabled = false, onSelect, keywords = []} = props;
|
|
427
|
+
const {value: selectedValue, onValueChange, setOpen, registerItem, unregisterItem} = useComboboxContext("ComboboxItem");
|
|
428
|
+
|
|
429
|
+
const isSelected = selectedValue === itemValue;
|
|
430
|
+
const label = typeof children === "string" ? children : itemValue;
|
|
431
|
+
|
|
432
|
+
// Register this item's label when mounted
|
|
433
|
+
React.useEffect(() => {
|
|
434
|
+
registerItem(itemValue, label);
|
|
435
|
+
return () => {
|
|
436
|
+
unregisterItem(itemValue);
|
|
437
|
+
};
|
|
438
|
+
}, [itemValue, label, registerItem, unregisterItem]);
|
|
439
|
+
|
|
440
|
+
const handleSelect = React.useCallback(
|
|
441
|
+
(currentValue: string) => {
|
|
442
|
+
const newValue = currentValue === selectedValue ? "" : currentValue;
|
|
443
|
+
onValueChange(newValue);
|
|
444
|
+
setOpen(false);
|
|
445
|
+
onSelect?.(newValue);
|
|
446
|
+
},
|
|
447
|
+
[selectedValue, onValueChange, setOpen, onSelect],
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<CommandItem
|
|
452
|
+
value={itemValue}
|
|
453
|
+
disabled={disabled}
|
|
454
|
+
onSelect={handleSelect}
|
|
455
|
+
keywords={keywords}
|
|
456
|
+
className={cn(styles.item, isSelected && styles.itemSelected, className)}>
|
|
457
|
+
<Check className={cn(styles.itemCheck, isSelected && styles.itemCheckVisible)} />
|
|
458
|
+
<span className={styles.itemLabel}>{children}</span>
|
|
459
|
+
</CommandItem>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
ComboboxItem.displayName = "ComboboxItem";
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Message shown when search returns no results.
|
|
466
|
+
*
|
|
467
|
+
* @remarks
|
|
468
|
+
* - Uses CommandEmpty internally
|
|
469
|
+
* - Defaults to context emptyMessage
|
|
470
|
+
*
|
|
471
|
+
* @example Basic usage
|
|
472
|
+
* ```tsx
|
|
473
|
+
* <ComboboxEmpty />
|
|
474
|
+
* ```
|
|
475
|
+
*
|
|
476
|
+
* @example Custom message
|
|
477
|
+
* ```tsx
|
|
478
|
+
* <ComboboxEmpty>Nothing found</ComboboxEmpty>
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
function ComboboxEmpty(props: Readonly<ComboboxEmpty.Props>): React.ReactElement {
|
|
482
|
+
const {className, children} = props;
|
|
483
|
+
const {emptyMessage} = useComboboxContext("ComboboxEmpty");
|
|
484
|
+
|
|
485
|
+
return <CommandEmpty className={cn(styles.empty, className)}>{children ?? emptyMessage}</CommandEmpty>;
|
|
486
|
+
}
|
|
487
|
+
ComboboxEmpty.displayName = "ComboboxEmpty";
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Groups related combobox items with an optional heading.
|
|
491
|
+
*
|
|
492
|
+
* @remarks
|
|
493
|
+
* - Uses CommandGroup internally
|
|
494
|
+
* - Supports visual grouping with headings
|
|
495
|
+
*
|
|
496
|
+
* @example Basic usage
|
|
497
|
+
* ```tsx
|
|
498
|
+
* <ComboboxGroup heading="Fruits">
|
|
499
|
+
* <ComboboxItem value="apple">Apple</ComboboxItem>
|
|
500
|
+
* </ComboboxGroup>
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
function ComboboxGroup(props: Readonly<ComboboxGroup.Props>): React.ReactElement {
|
|
504
|
+
const {heading, className, children} = props;
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
<CommandGroup
|
|
508
|
+
heading={heading}
|
|
509
|
+
className={cn(styles.group, className)}>
|
|
510
|
+
{children}
|
|
511
|
+
</CommandGroup>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
ComboboxGroup.displayName = "ComboboxGroup";
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Visual separator between combobox groups.
|
|
518
|
+
*
|
|
519
|
+
* @remarks
|
|
520
|
+
* - Uses CommandSeparator internally
|
|
521
|
+
*
|
|
522
|
+
* @example Basic usage
|
|
523
|
+
* ```tsx
|
|
524
|
+
* <ComboboxSeparator />
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
function ComboboxSeparator(props: Readonly<ComboboxSeparator.Props>): React.ReactElement {
|
|
528
|
+
const {className} = props;
|
|
529
|
+
|
|
530
|
+
return <CommandSeparator className={cn(styles.separator, className)} />;
|
|
531
|
+
}
|
|
532
|
+
ComboboxSeparator.displayName = "ComboboxSeparator";
|
|
533
|
+
|
|
534
|
+
// eslint-disable-next-line no-redeclare -- required for the canonical component namespace typing API
|
|
535
|
+
namespace Combobox {
|
|
536
|
+
export type Props = ComboboxProps;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// eslint-disable-next-line no-redeclare -- required for the canonical component namespace typing API
|
|
540
|
+
namespace ComboboxTrigger {
|
|
541
|
+
export type Props = ComboboxTriggerProps;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// eslint-disable-next-line no-redeclare -- required for the canonical component namespace typing API
|
|
545
|
+
namespace ComboboxContent {
|
|
546
|
+
export type Props = ComboboxContentProps;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// eslint-disable-next-line no-redeclare -- required for the canonical component namespace typing API
|
|
550
|
+
namespace ComboboxItem {
|
|
551
|
+
export type Props = ComboboxItemProps;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// eslint-disable-next-line no-redeclare -- required for the canonical component namespace typing API
|
|
555
|
+
namespace ComboboxEmpty {
|
|
556
|
+
export type Props = ComboboxEmptyProps;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// eslint-disable-next-line no-redeclare -- required for the canonical component namespace typing API
|
|
560
|
+
namespace ComboboxGroup {
|
|
561
|
+
export type Props = ComboboxGroupProps;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// eslint-disable-next-line no-redeclare -- required for the canonical component namespace typing API
|
|
565
|
+
namespace ComboboxSeparator {
|
|
566
|
+
export type Props = ComboboxSeparatorProps;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export {Combobox, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxItem, ComboboxSeparator, ComboboxTrigger};
|
|
@@ -285,16 +285,16 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
|
|
|
285
285
|
) => {
|
|
286
286
|
const [activeItemId, setActiveItemId] = React.useState<string | null>(null);
|
|
287
287
|
const [search, setSearch] = React.useState("");
|
|
288
|
-
const
|
|
289
|
-
const
|
|
288
|
+
const itemOrderRef = React.useRef(0);
|
|
289
|
+
const itemsRef = React.useRef(new Map<string, CommandRegisteredItem>());
|
|
290
290
|
const [itemsVersion, setItemsVersion] = React.useState(0);
|
|
291
291
|
const listId = React.useId();
|
|
292
292
|
|
|
293
293
|
const registerItem = React.useCallback((item: Omit<CommandRegisteredItem, "order">): void => {
|
|
294
|
-
const existingItem =
|
|
294
|
+
const existingItem = itemsRef.current.get(item.id);
|
|
295
295
|
const nextItem: CommandRegisteredItem = {
|
|
296
296
|
...item,
|
|
297
|
-
order: existingItem?.order ??
|
|
297
|
+
order: existingItem?.order ?? itemOrderRef.current++,
|
|
298
298
|
};
|
|
299
299
|
|
|
300
300
|
const hasChanged =
|
|
@@ -311,12 +311,12 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
|
|
|
311
311
|
return;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
|
|
314
|
+
itemsRef.current.set(item.id, nextItem);
|
|
315
315
|
setItemsVersion((currentVersion) => currentVersion + 1);
|
|
316
316
|
}, []);
|
|
317
317
|
|
|
318
318
|
const unregisterItem = React.useCallback((itemId: string): void => {
|
|
319
|
-
if (!
|
|
319
|
+
if (!itemsRef.current.delete(itemId)) {
|
|
320
320
|
return;
|
|
321
321
|
}
|
|
322
322
|
|
|
@@ -324,14 +324,15 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
|
|
|
324
324
|
}, []);
|
|
325
325
|
|
|
326
326
|
const items = React.useMemo(() => {
|
|
327
|
-
return [...
|
|
327
|
+
return [...itemsRef.current.values()].toSorted((firstItem, secondItem) => firstItem.order - secondItem.order);
|
|
328
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- itemsVersion is an intentional change counter
|
|
328
329
|
}, [itemsVersion]);
|
|
329
330
|
|
|
330
331
|
const isFiltering = shouldFilter && search.trim().length > 0;
|
|
331
332
|
|
|
332
333
|
const isItemVisible = React.useCallback(
|
|
333
334
|
(itemId: string): boolean => {
|
|
334
|
-
const item =
|
|
335
|
+
const item = itemsRef.current.get(itemId);
|
|
335
336
|
|
|
336
337
|
if (!item) {
|
|
337
338
|
return false;
|
|
@@ -355,11 +356,13 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
|
|
|
355
356
|
|
|
356
357
|
React.useEffect(() => {
|
|
357
358
|
if (selectableItems.length === 0) {
|
|
359
|
+
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
|
358
360
|
setActiveItemId(null);
|
|
359
361
|
return;
|
|
360
362
|
}
|
|
361
363
|
|
|
362
364
|
if (!activeItemId || !selectableItems.some((item) => item.id === activeItemId)) {
|
|
365
|
+
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
|
363
366
|
setActiveItemId(selectableItems[0].id);
|
|
364
367
|
}
|
|
365
368
|
}, [activeItemId, selectableItems]);
|
|
@@ -369,7 +372,7 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
|
|
|
369
372
|
return;
|
|
370
373
|
}
|
|
371
374
|
|
|
372
|
-
|
|
375
|
+
itemsRef.current.get(activeItemId)?.ref.current?.scrollIntoView({
|
|
373
376
|
block: "nearest",
|
|
374
377
|
});
|
|
375
378
|
}, [activeItemId]);
|
|
@@ -427,7 +430,7 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
|
|
|
427
430
|
return;
|
|
428
431
|
}
|
|
429
432
|
|
|
430
|
-
|
|
433
|
+
itemsRef.current.get(activeItemId)?.ref.current?.click();
|
|
431
434
|
}, [activeItemId]);
|
|
432
435
|
|
|
433
436
|
const hasVisibleItemsInGroup = React.useCallback(
|
|
@@ -484,6 +487,7 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
|
|
|
484
487
|
ref={ref}
|
|
485
488
|
aria-label={label}
|
|
486
489
|
className={cn(styles.command, className)}
|
|
490
|
+
role='toolbar'
|
|
487
491
|
onKeyDown={(event) => {
|
|
488
492
|
onKeyDown?.(event);
|
|
489
493
|
|
|
@@ -796,11 +800,11 @@ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
|
|
|
796
800
|
useCommandContext("CommandItem");
|
|
797
801
|
const groupId = React.useContext(CommandGroupContext);
|
|
798
802
|
const generatedId = React.useId();
|
|
799
|
-
const
|
|
803
|
+
const itemRef = React.useRef<HTMLDivElement | null>(null);
|
|
800
804
|
const keywordSignature = React.useMemo(() => keywords.join("\u0000"), [keywords]);
|
|
801
805
|
|
|
802
806
|
React.useLayoutEffect(() => {
|
|
803
|
-
const textValue = value ??
|
|
807
|
+
const textValue = value ?? itemRef.current?.textContent?.trim() ?? "";
|
|
804
808
|
|
|
805
809
|
registerItem({
|
|
806
810
|
disabled,
|
|
@@ -808,7 +812,7 @@ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
|
|
|
808
812
|
groupId,
|
|
809
813
|
id: generatedId,
|
|
810
814
|
keywords,
|
|
811
|
-
ref:
|
|
815
|
+
ref: itemRef,
|
|
812
816
|
textValue,
|
|
813
817
|
value,
|
|
814
818
|
});
|
|
@@ -832,7 +836,7 @@ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
|
|
|
832
836
|
<div
|
|
833
837
|
{...props}
|
|
834
838
|
ref={(node) => {
|
|
835
|
-
|
|
839
|
+
itemRef.current = node;
|
|
836
840
|
assignRef(ref, node);
|
|
837
841
|
}}
|
|
838
842
|
aria-disabled={disabled || undefined}
|
|
@@ -848,9 +852,21 @@ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
|
|
|
848
852
|
}
|
|
849
853
|
|
|
850
854
|
selectSpecificItem(generatedId);
|
|
851
|
-
onSelect?.(value ??
|
|
855
|
+
onSelect?.(value ?? itemRef.current?.textContent?.trim() ?? "");
|
|
852
856
|
onClick?.(event);
|
|
853
857
|
}}
|
|
858
|
+
onKeyDown={(event) => {
|
|
859
|
+
if (disabled) {
|
|
860
|
+
event.preventDefault();
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
865
|
+
event.preventDefault();
|
|
866
|
+
selectSpecificItem(generatedId);
|
|
867
|
+
onSelect?.(value ?? itemRef.current?.textContent?.trim() ?? "");
|
|
868
|
+
}
|
|
869
|
+
}}
|
|
854
870
|
onFocus={() => {
|
|
855
871
|
if (disabled) {
|
|
856
872
|
return;
|