@neynar/ui 0.1.1 → 0.1.2
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/dist/components/ui/accordion.d.ts +1 -25
- package/dist/components/ui/accordion.d.ts.map +1 -1
- package/dist/components/ui/alert-dialog.d.ts +240 -46
- package/dist/components/ui/alert-dialog.d.ts.map +1 -1
- package/dist/components/ui/alert.d.ts +73 -11
- package/dist/components/ui/alert.d.ts.map +1 -1
- package/dist/components/ui/aspect-ratio.d.ts +44 -10
- package/dist/components/ui/aspect-ratio.d.ts.map +1 -1
- package/dist/components/ui/avatar.d.ts +117 -33
- package/dist/components/ui/avatar.d.ts.map +1 -1
- package/dist/components/ui/badge.d.ts +50 -71
- package/dist/components/ui/badge.d.ts.map +1 -1
- package/dist/components/ui/breadcrumb.d.ts +231 -49
- package/dist/components/ui/breadcrumb.d.ts.map +1 -1
- package/dist/components/ui/button.d.ts +189 -71
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/calendar.d.ts +197 -40
- package/dist/components/ui/calendar.d.ts.map +1 -1
- package/dist/components/ui/card.d.ts +7 -22
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/carousel.d.ts +369 -99
- package/dist/components/ui/carousel.d.ts.map +1 -1
- package/dist/components/ui/chart.d.ts.map +1 -1
- package/dist/components/ui/checkbox.d.ts +110 -38
- package/dist/components/ui/checkbox.d.ts.map +1 -1
- package/dist/components/ui/collapsible.d.ts +246 -61
- package/dist/components/ui/collapsible.d.ts.map +1 -1
- package/dist/components/ui/combobox.d.ts +207 -159
- package/dist/components/ui/combobox.d.ts.map +1 -1
- package/dist/components/ui/command.d.ts +336 -67
- package/dist/components/ui/command.d.ts.map +1 -1
- package/dist/components/ui/container.d.ts +159 -64
- package/dist/components/ui/container.d.ts.map +1 -1
- package/dist/components/ui/context-menu.d.ts +321 -39
- package/dist/components/ui/context-menu.d.ts.map +1 -1
- package/dist/components/ui/date-picker.d.ts +113 -86
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/dialog.d.ts +106 -25
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/drawer.d.ts +388 -59
- package/dist/components/ui/drawer.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts +521 -74
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/empty-state.d.ts +148 -76
- package/dist/components/ui/empty-state.d.ts.map +1 -1
- package/dist/components/ui/hover-card.d.ts +253 -34
- package/dist/components/ui/hover-card.d.ts.map +1 -1
- package/dist/components/ui/input.d.ts +143 -44
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/label.d.ts +0 -8
- package/dist/components/ui/label.d.ts.map +1 -1
- package/dist/components/ui/menubar.d.ts +288 -46
- package/dist/components/ui/menubar.d.ts.map +1 -1
- package/dist/components/ui/navigation-menu.d.ts +444 -127
- package/dist/components/ui/navigation-menu.d.ts.map +1 -1
- package/dist/components/ui/pagination.d.ts +342 -66
- package/dist/components/ui/pagination.d.ts.map +1 -1
- package/dist/components/ui/popover.d.ts +0 -8
- package/dist/components/ui/popover.d.ts.map +1 -1
- package/dist/components/ui/progress.d.ts +88 -30
- package/dist/components/ui/progress.d.ts.map +1 -1
- package/dist/components/ui/radio-group.d.ts +189 -45
- package/dist/components/ui/radio-group.d.ts.map +1 -1
- package/dist/components/ui/resizable.d.ts +178 -62
- package/dist/components/ui/resizable.d.ts.map +1 -1
- package/dist/components/ui/scroll-area.d.ts +180 -21
- package/dist/components/ui/scroll-area.d.ts.map +1 -1
- package/dist/components/ui/select.d.ts +382 -60
- package/dist/components/ui/select.d.ts.map +1 -1
- package/dist/components/ui/separator.d.ts +52 -39
- package/dist/components/ui/separator.d.ts.map +1 -1
- package/dist/components/ui/sheet.d.ts +144 -27
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sidebar.d.ts +81 -31
- package/dist/components/ui/sidebar.d.ts.map +1 -1
- package/dist/components/ui/skeleton.d.ts +94 -32
- package/dist/components/ui/skeleton.d.ts.map +1 -1
- package/dist/components/ui/slider.d.ts +37 -31
- package/dist/components/ui/slider.d.ts.map +1 -1
- package/dist/components/ui/sonner.d.ts +280 -46
- package/dist/components/ui/sonner.d.ts.map +1 -1
- package/dist/components/ui/stack.d.ts +289 -148
- package/dist/components/ui/stack.d.ts.map +1 -1
- package/dist/components/ui/stories/aspect-ratio.stories.d.ts +1 -2
- package/dist/components/ui/stories/aspect-ratio.stories.d.ts.map +1 -1
- package/dist/components/ui/stories/container.stories.d.ts +2 -3
- package/dist/components/ui/stories/container.stories.d.ts.map +1 -1
- package/dist/components/ui/stories/empty-state.stories.d.ts +2 -2
- package/dist/components/ui/stories/scroll-area.stories.d.ts +1 -2
- package/dist/components/ui/stories/scroll-area.stories.d.ts.map +1 -1
- package/dist/components/ui/stories/stack.stories.d.ts +1 -1
- package/dist/components/ui/stories/text-field.stories.d.ts +7 -1
- package/dist/components/ui/stories/text-field.stories.d.ts.map +1 -1
- package/dist/components/ui/switch.d.ts +44 -38
- package/dist/components/ui/switch.d.ts.map +1 -1
- package/dist/components/ui/table.d.ts +33 -0
- package/dist/components/ui/table.d.ts.map +1 -1
- package/dist/components/ui/tabs.d.ts +4 -22
- package/dist/components/ui/tabs.d.ts.map +1 -1
- package/dist/components/ui/text-field.d.ts +170 -84
- package/dist/components/ui/text-field.d.ts.map +1 -1
- package/dist/components/ui/textarea.d.ts +106 -29
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/theme-toggle.d.ts +190 -65
- package/dist/components/ui/theme-toggle.d.ts.map +1 -1
- package/dist/components/ui/theme.d.ts +107 -23
- package/dist/components/ui/theme.d.ts.map +1 -1
- package/dist/components/ui/toggle-group.d.ts +143 -67
- package/dist/components/ui/toggle-group.d.ts.map +1 -1
- package/dist/components/ui/toggle.d.ts +118 -30
- package/dist/components/ui/toggle.d.ts.map +1 -1
- package/dist/components/ui/tooltip.d.ts +152 -28
- package/dist/components/ui/tooltip.d.ts.map +1 -1
- package/dist/components/ui/typography.d.ts +452 -134
- package/dist/components/ui/typography.d.ts.map +1 -1
- package/dist/index.js +9388 -8281
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/llms.txt +173 -3
- package/package.json +5 -2
- package/src/components/ui/accordion.tsx +112 -27
- package/src/components/ui/alert-dialog.tsx +401 -46
- package/src/components/ui/alert.tsx +114 -11
- package/src/components/ui/aspect-ratio.tsx +69 -14
- package/src/components/ui/avatar.tsx +179 -33
- package/src/components/ui/badge.tsx +74 -75
- package/src/components/ui/breadcrumb.tsx +335 -50
- package/src/components/ui/button.tsx +198 -90
- package/src/components/ui/calendar.tsx +867 -43
- package/src/components/ui/card.tsx +140 -33
- package/src/components/ui/carousel.tsx +529 -98
- package/src/components/ui/chart.tsx +222 -1
- package/src/components/ui/checkbox.tsx +176 -38
- package/src/components/ui/collapsible.tsx +321 -67
- package/src/components/ui/combobox.tsx +284 -83
- package/src/components/ui/command.tsx +527 -67
- package/src/components/ui/container.tsx +217 -65
- package/src/components/ui/context-menu.tsx +716 -51
- package/src/components/ui/date-picker.tsx +228 -38
- package/src/components/ui/dialog.tsx +270 -33
- package/src/components/ui/drawer.tsx +546 -67
- package/src/components/ui/dropdown-menu.tsx +657 -74
- package/src/components/ui/empty-state.tsx +241 -82
- package/src/components/ui/hover-card.tsx +328 -39
- package/src/components/ui/input.tsx +207 -44
- package/src/components/ui/label.tsx +98 -8
- package/src/components/ui/menubar.tsx +587 -54
- package/src/components/ui/navigation-menu.tsx +557 -128
- package/src/components/ui/pagination.tsx +561 -79
- package/src/components/ui/popover.tsx +119 -8
- package/src/components/ui/progress.tsx +131 -29
- package/src/components/ui/radio-group.tsx +260 -51
- package/src/components/ui/resizable.tsx +289 -63
- package/src/components/ui/scroll-area.tsx +377 -66
- package/src/components/ui/select.tsx +545 -60
- package/src/components/ui/separator.tsx +146 -40
- package/src/components/ui/sheet.tsx +348 -31
- package/src/components/ui/sidebar.tsx +471 -29
- package/src/components/ui/skeleton.tsx +114 -32
- package/src/components/ui/slider.tsx +77 -31
- package/src/components/ui/sonner.tsx +574 -46
- package/src/components/ui/stack.tsx +423 -101
- package/src/components/ui/switch.tsx +78 -39
- package/src/components/ui/table.tsx +170 -4
- package/src/components/ui/tabs.tsx +108 -22
- package/src/components/ui/text-field.tsx +226 -81
- package/src/components/ui/textarea.tsx +180 -29
- package/src/components/ui/theme-toggle.tsx +313 -65
- package/src/components/ui/theme.tsx +117 -23
- package/src/components/ui/toggle-group.tsx +280 -69
- package/src/components/ui/toggle.tsx +124 -35
- package/src/components/ui/tooltip.tsx +239 -29
- package/src/components/ui/typography.tsx +1115 -165
|
@@ -53,27 +53,49 @@ export type ComboboxOption = {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
56
|
+
* Props for Combobox component (Documentation only - NOT used in component implementation)
|
|
57
57
|
*
|
|
58
58
|
* Comprehensive configuration options for customizing combobox behavior,
|
|
59
|
-
* appearance, and interaction patterns.
|
|
59
|
+
* appearance, and interaction patterns. Built on top of Radix UI Popover
|
|
60
|
+
* primitives and CMDK command menu for robust accessibility and functionality.
|
|
61
|
+
*
|
|
62
|
+
* These types are for documentation generation and should not replace CVA/Radix inferred types.
|
|
60
63
|
*
|
|
61
64
|
* @since 1.0.0
|
|
62
65
|
*/
|
|
63
|
-
|
|
66
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
67
|
+
type ComboboxDocsProps = {
|
|
64
68
|
/**
|
|
65
69
|
* Array of options to display in the combobox dropdown
|
|
66
70
|
*
|
|
67
71
|
* Each option should have a unique value and descriptive label.
|
|
68
72
|
* Options can be disabled to prevent selection while remaining visible.
|
|
73
|
+
* The array is filtered in real-time as users type in the search input.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```tsx
|
|
77
|
+
* const options = [
|
|
78
|
+
* { value: "nextjs", label: "Next.js" },
|
|
79
|
+
* { value: "remix", label: "Remix", disabled: false },
|
|
80
|
+
* { value: "sveltekit", label: "SvelteKit" },
|
|
81
|
+
* { value: "nuxt", label: "Nuxt.js", disabled: true }, // Disabled option
|
|
82
|
+
* ]
|
|
83
|
+
* ```
|
|
69
84
|
*/
|
|
70
85
|
options: ComboboxOption[];
|
|
71
86
|
|
|
72
87
|
/**
|
|
73
88
|
* Currently selected value from the options array
|
|
74
89
|
*
|
|
75
|
-
* Should match the value property of one of the provided options.
|
|
76
|
-
* Use with onValueChange for controlled component behavior.
|
|
90
|
+
* Should match the `value` property of one of the provided options.
|
|
91
|
+
* Use with `onValueChange` for controlled component behavior.
|
|
92
|
+
* When undefined or empty, no option is selected and placeholder is shown.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```tsx
|
|
96
|
+
* const [framework, setFramework] = useState("") // No selection
|
|
97
|
+
* const [language, setLanguage] = useState("typescript") // Pre-selected
|
|
98
|
+
* ```
|
|
77
99
|
*/
|
|
78
100
|
value?: string;
|
|
79
101
|
|
|
@@ -82,8 +104,22 @@ export type ComboboxProps = {
|
|
|
82
104
|
*
|
|
83
105
|
* Receives the new selected value as a string. If the same option
|
|
84
106
|
* is clicked again, an empty string is passed to allow deselection.
|
|
107
|
+
* This enables toggle behavior where clicking selected items deselects them.
|
|
85
108
|
*
|
|
86
109
|
* @param value - The newly selected option value or empty string for deselection
|
|
110
|
+
* @example
|
|
111
|
+
* ```tsx
|
|
112
|
+
* // Basic controlled usage
|
|
113
|
+
* onValueChange={(value) => setSelectedValue(value)}
|
|
114
|
+
*
|
|
115
|
+
* // With validation and side effects
|
|
116
|
+
* onValueChange={(value) => {
|
|
117
|
+
* setSelectedValue(value)
|
|
118
|
+
* if (value) {
|
|
119
|
+
* console.log('Selected:', options.find(opt => opt.value === value)?.label)
|
|
120
|
+
* }
|
|
121
|
+
* }}
|
|
122
|
+
* ```
|
|
87
123
|
*/
|
|
88
124
|
onValueChange?: (value: string) => void;
|
|
89
125
|
|
|
@@ -91,8 +127,10 @@ export type ComboboxProps = {
|
|
|
91
127
|
* Placeholder text displayed when no option is selected
|
|
92
128
|
*
|
|
93
129
|
* Should be descriptive and help users understand what they're selecting.
|
|
130
|
+
* Displayed in muted text color to distinguish from actual selections.
|
|
94
131
|
*
|
|
95
132
|
* @default "Select option..."
|
|
133
|
+
* @example "Choose a framework...", "Select your country..."
|
|
96
134
|
*/
|
|
97
135
|
placeholder?: string;
|
|
98
136
|
|
|
@@ -100,8 +138,10 @@ export type ComboboxProps = {
|
|
|
100
138
|
* Text displayed when search query returns no matching options
|
|
101
139
|
*
|
|
102
140
|
* Customize this message to match your application's tone and context.
|
|
141
|
+
* Shown in the dropdown when no options match the current search input.
|
|
103
142
|
*
|
|
104
143
|
* @default "No option found."
|
|
144
|
+
* @example "No frameworks found.", "Try a different search term."
|
|
105
145
|
*/
|
|
106
146
|
emptyText?: string;
|
|
107
147
|
|
|
@@ -109,15 +149,20 @@ export type ComboboxProps = {
|
|
|
109
149
|
* Placeholder text for the search input field
|
|
110
150
|
*
|
|
111
151
|
* Should guide users on how to search effectively within your option set.
|
|
152
|
+
* Displayed inside the search input when it's empty.
|
|
112
153
|
*
|
|
113
154
|
* @default "Search options..."
|
|
155
|
+
* @example "Search frameworks...", "Type to filter countries..."
|
|
114
156
|
*/
|
|
115
157
|
searchPlaceholder?: string;
|
|
116
158
|
|
|
117
159
|
/**
|
|
118
160
|
* Additional CSS classes for the root container element
|
|
119
161
|
*
|
|
120
|
-
* Use for custom spacing,
|
|
162
|
+
* Applied to the outermost div wrapper. Use for custom spacing,
|
|
163
|
+
* positioning, or layout adjustments that don't affect internal components.
|
|
164
|
+
*
|
|
165
|
+
* @example "mb-4", "w-full max-w-sm"
|
|
121
166
|
*/
|
|
122
167
|
className?: string;
|
|
123
168
|
|
|
@@ -125,10 +170,11 @@ export type ComboboxProps = {
|
|
|
125
170
|
* Whether the entire combobox is disabled
|
|
126
171
|
*
|
|
127
172
|
* When disabled, the trigger button cannot be clicked and the
|
|
128
|
-
* dropdown cannot be opened. Use individual option.disabled for
|
|
129
|
-
* granular control.
|
|
173
|
+
* dropdown cannot be opened. Use individual `option.disabled` for
|
|
174
|
+
* granular control over specific options.
|
|
130
175
|
*
|
|
131
176
|
* @default false
|
|
177
|
+
* @example disabled={isLoading || !hasPermission}
|
|
132
178
|
*/
|
|
133
179
|
disabled?: boolean;
|
|
134
180
|
|
|
@@ -136,37 +182,59 @@ export type ComboboxProps = {
|
|
|
136
182
|
* Additional CSS classes for the trigger button
|
|
137
183
|
*
|
|
138
184
|
* Commonly used to control width (e.g., "w-[300px]"), styling variants,
|
|
139
|
-
* or responsive behavior.
|
|
185
|
+
* or responsive behavior. The button uses the "outline" variant by default.
|
|
186
|
+
* For best UX, consider matching `popoverClassName` width.
|
|
140
187
|
*
|
|
141
188
|
* @example "w-full sm:w-[280px] border-primary/50"
|
|
189
|
+
* @example "min-w-[200px] font-medium"
|
|
142
190
|
*/
|
|
143
191
|
buttonClassName?: string;
|
|
144
192
|
|
|
145
193
|
/**
|
|
146
194
|
* Additional CSS classes for the popover dropdown content
|
|
147
195
|
*
|
|
148
|
-
* Should typically match buttonClassName width for consistent alignment.
|
|
149
|
-
* Use to control positioning, width, and styling of the dropdown.
|
|
196
|
+
* Should typically match `buttonClassName` width for consistent alignment.
|
|
197
|
+
* Use to control positioning, width, and styling of the dropdown container.
|
|
198
|
+
* The popover automatically handles z-index and positioning.
|
|
150
199
|
*
|
|
151
200
|
* @example "w-full sm:w-[280px] border-primary/50"
|
|
201
|
+
* @example "max-h-[300px] border-2"
|
|
152
202
|
*/
|
|
153
203
|
popoverClassName?: string;
|
|
154
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Inherited props from Radix UI Popover.Root
|
|
206
|
+
*
|
|
207
|
+
* Advanced props for controlling popover behavior. These are automatically
|
|
208
|
+
* passed through to the underlying Popover.Root component.
|
|
209
|
+
*
|
|
210
|
+
* @see {@link https://www.radix-ui.com/primitives/docs/components/popover#root} Radix Popover.Root API
|
|
211
|
+
*/
|
|
212
|
+
} & Omit<React.ComponentProps<typeof Popover>, "children">;
|
|
155
213
|
|
|
156
214
|
/**
|
|
157
215
|
* Searchable dropdown selection component with typeahead functionality
|
|
158
216
|
*
|
|
159
217
|
* A versatile combobox that combines a button trigger with a searchable dropdown list.
|
|
160
218
|
* Ideal for selecting from moderate to large lists of options where search functionality
|
|
161
|
-
* improves user experience. Built on
|
|
219
|
+
* improves user experience. Built on Radix UI Popover primitives for accessibility and
|
|
220
|
+
* CMDK for powerful command menu functionality with real-time filtering.
|
|
221
|
+
*
|
|
222
|
+
* **Technical Architecture:**
|
|
223
|
+
* - **Popover Container**: Radix UI Popover.Root provides modal/non-modal behavior, focus management, and positioning
|
|
224
|
+
* - **Trigger Button**: Uses Button component with proper ARIA attributes and visual states
|
|
225
|
+
* - **Command Menu**: CMDK provides keyboard navigation, filtering, and accessibility features
|
|
226
|
+
* - **State Management**: Controlled/uncontrolled modes with proper React patterns
|
|
162
227
|
*
|
|
163
|
-
* Use
|
|
164
|
-
* -
|
|
165
|
-
* -
|
|
166
|
-
* -
|
|
167
|
-
* -
|
|
228
|
+
* **Use Cases:**
|
|
229
|
+
* - Selecting from 10+ options that benefit from real-time filtering
|
|
230
|
+
* - User interfaces requiring quick option discovery through search
|
|
231
|
+
* - Forms needing better UX than basic select dropdowns
|
|
232
|
+
* - Applications with dynamic or large option sets
|
|
168
233
|
*
|
|
169
|
-
*
|
|
234
|
+
* **When Not to Use:**
|
|
235
|
+
* - Simple selection from few options (use Select component instead)
|
|
236
|
+
* - Multi-select scenarios (use Checkbox group or specialized multi-select)
|
|
237
|
+
* - Tree or hierarchical data (use TreeSelect or nested menus)
|
|
170
238
|
*
|
|
171
239
|
* @component
|
|
172
240
|
* @example
|
|
@@ -175,9 +243,9 @@ export type ComboboxProps = {
|
|
|
175
243
|
* const [framework, setFramework] = useState("")
|
|
176
244
|
*
|
|
177
245
|
* const frameworks = [
|
|
178
|
-
* { value: "
|
|
246
|
+
* { value: "nextjs", label: "Next.js" },
|
|
179
247
|
* { value: "remix", label: "Remix" },
|
|
180
|
-
* { value: "
|
|
248
|
+
* { value: "sveltekit", label: "SvelteKit" },
|
|
181
249
|
* { value: "nuxt", label: "Nuxt.js" },
|
|
182
250
|
* ]
|
|
183
251
|
*
|
|
@@ -188,11 +256,12 @@ export type ComboboxProps = {
|
|
|
188
256
|
* placeholder="Select framework..."
|
|
189
257
|
* searchPlaceholder="Search frameworks..."
|
|
190
258
|
* buttonClassName="w-[300px]"
|
|
259
|
+
* popoverClassName="w-[300px]"
|
|
191
260
|
* />
|
|
192
261
|
* ```
|
|
193
262
|
*
|
|
194
263
|
* @example
|
|
195
|
-
*
|
|
264
|
+
* Advanced combobox with disabled options and custom styling
|
|
196
265
|
* ```tsx
|
|
197
266
|
* const statusOptions = [
|
|
198
267
|
* { value: "active", label: "Active" },
|
|
@@ -204,103 +273,217 @@ export type ComboboxProps = {
|
|
|
204
273
|
* <Combobox
|
|
205
274
|
* options={statusOptions}
|
|
206
275
|
* value={status}
|
|
207
|
-
* onValueChange={
|
|
276
|
+
* onValueChange={(value) => {
|
|
277
|
+
* setStatus(value)
|
|
278
|
+
* // Track selection analytics
|
|
279
|
+
* analytics.track('status_selected', { value })
|
|
280
|
+
* }}
|
|
208
281
|
* placeholder="Select status..."
|
|
209
|
-
* emptyText="No status found."
|
|
282
|
+
* emptyText="No matching status found."
|
|
283
|
+
* searchPlaceholder="Filter statuses..."
|
|
210
284
|
* buttonClassName="w-[250px] border-primary/50"
|
|
211
285
|
* popoverClassName="w-[250px]"
|
|
286
|
+
* disabled={isLoading}
|
|
212
287
|
* />
|
|
213
288
|
* ```
|
|
214
289
|
*
|
|
215
290
|
* @example
|
|
216
|
-
* Form integration with validation and error
|
|
291
|
+
* Form integration with validation and error handling
|
|
217
292
|
* ```tsx
|
|
218
293
|
* import { Label } from "@/components/ui/label"
|
|
294
|
+
* import { useFormContext } from "react-hook-form"
|
|
219
295
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
296
|
+
* function CountryField() {
|
|
297
|
+
* const { register, setValue, watch, formState: { errors } } = useFormContext()
|
|
298
|
+
* const selectedCountry = watch("country")
|
|
299
|
+
*
|
|
300
|
+
* return (
|
|
301
|
+
* <div className="space-y-2">
|
|
302
|
+
* <Label htmlFor="country" className={errors.country ? "text-destructive" : ""}>
|
|
303
|
+
* Country {errors.country && "*"}
|
|
304
|
+
* </Label>
|
|
305
|
+
* <Combobox
|
|
306
|
+
* options={countries}
|
|
307
|
+
* value={selectedCountry}
|
|
308
|
+
* onValueChange={(value) => setValue("country", value, { shouldValidate: true })}
|
|
309
|
+
* placeholder="Choose your country..."
|
|
310
|
+
* searchPlaceholder="Type to search countries..."
|
|
311
|
+
* emptyText="Country not found. Try a different search."
|
|
312
|
+
* buttonClassName={cn(
|
|
313
|
+
* "w-full",
|
|
314
|
+
* errors.country && "border-destructive focus-visible:ring-destructive"
|
|
315
|
+
* )}
|
|
316
|
+
* popoverClassName="w-full"
|
|
317
|
+
* disabled={isSubmitting}
|
|
318
|
+
* />
|
|
319
|
+
* {errors.country && (
|
|
320
|
+
* <p className="text-sm text-destructive" role="alert">
|
|
321
|
+
* {errors.country.message}
|
|
322
|
+
* </p>
|
|
323
|
+
* )}
|
|
324
|
+
* </div>
|
|
325
|
+
* )
|
|
326
|
+
* }
|
|
240
327
|
* ```
|
|
241
328
|
*
|
|
242
329
|
* @example
|
|
243
|
-
* Responsive
|
|
330
|
+
* Responsive design with mobile optimization
|
|
244
331
|
* ```tsx
|
|
245
332
|
* <Combobox
|
|
246
|
-
* options={
|
|
247
|
-
* value={
|
|
248
|
-
* onValueChange={
|
|
249
|
-
* placeholder="
|
|
250
|
-
* searchPlaceholder="Search
|
|
251
|
-
*
|
|
252
|
-
*
|
|
333
|
+
* options={teamMembers}
|
|
334
|
+
* value={assignedTo}
|
|
335
|
+
* onValueChange={setAssignedTo}
|
|
336
|
+
* placeholder="Assign to team member..."
|
|
337
|
+
* searchPlaceholder="Search team members..."
|
|
338
|
+
* emptyText="No team members found."
|
|
339
|
+
* buttonClassName="w-full sm:w-[280px] md:w-[320px]"
|
|
340
|
+
* popoverClassName="w-full sm:w-[280px] md:w-[320px]"
|
|
341
|
+
* // Mobile: full width, Desktop: fixed width for consistent layout
|
|
253
342
|
* />
|
|
254
343
|
* ```
|
|
255
344
|
*
|
|
345
|
+
* @example
|
|
346
|
+
* Async data loading with loading states
|
|
347
|
+
* ```tsx
|
|
348
|
+
* function AsyncCombobox() {
|
|
349
|
+
* const [options, setOptions] = useState([])
|
|
350
|
+
* const [loading, setLoading] = useState(false)
|
|
351
|
+
* const [searchTerm, setSearchTerm] = useState("")
|
|
352
|
+
*
|
|
353
|
+
* // Debounced search effect
|
|
354
|
+
* useEffect(() => {
|
|
355
|
+
* if (!searchTerm) return
|
|
356
|
+
*
|
|
357
|
+
* const timeoutId = setTimeout(async () => {
|
|
358
|
+
* setLoading(true)
|
|
359
|
+
* try {
|
|
360
|
+
* const results = await searchAPI(searchTerm)
|
|
361
|
+
* setOptions(results)
|
|
362
|
+
* } finally {
|
|
363
|
+
* setLoading(false)
|
|
364
|
+
* }
|
|
365
|
+
* }, 300)
|
|
366
|
+
*
|
|
367
|
+
* return () => clearTimeout(timeoutId)
|
|
368
|
+
* }, [searchTerm])
|
|
369
|
+
*
|
|
370
|
+
* return (
|
|
371
|
+
* <Combobox
|
|
372
|
+
* options={options}
|
|
373
|
+
* value={selectedValue}
|
|
374
|
+
* onValueChange={setSelectedValue}
|
|
375
|
+
* placeholder={loading ? "Searching..." : "Search items..."}
|
|
376
|
+
* emptyText={loading ? "Loading..." : "No results found."}
|
|
377
|
+
* disabled={loading}
|
|
378
|
+
* />
|
|
379
|
+
* )
|
|
380
|
+
* }
|
|
381
|
+
* ```
|
|
382
|
+
*
|
|
256
383
|
* @accessibility
|
|
257
384
|
*
|
|
258
|
-
* **ARIA Implementation:**
|
|
259
|
-
* -
|
|
260
|
-
* -
|
|
261
|
-
* -
|
|
262
|
-
* -
|
|
385
|
+
* **ARIA Implementation (WCAG 2.1 Level AA Compliant):**
|
|
386
|
+
* - **Combobox Role**: Trigger button uses `role="combobox"` with proper `aria-expanded` state
|
|
387
|
+
* - **Popup Association**: `aria-controls` links trigger to popup when visible
|
|
388
|
+
* - **Active Descendant**: `aria-activedescendant` manages focus within dropdown
|
|
389
|
+
* - **Popup Type**: `aria-haspopup="listbox"` indicates the nature of the popup
|
|
390
|
+
* - **Selection State**: `aria-selected` attributes on options indicate current selection
|
|
391
|
+
* - **Disabled State**: Proper `aria-disabled` attributes for unavailable options
|
|
263
392
|
*
|
|
264
|
-
* **Keyboard Navigation (W3C ARIA 1.2
|
|
265
|
-
* - **Tab**: Moves focus to/from combobox in
|
|
266
|
-
* - **
|
|
393
|
+
* **Keyboard Navigation (W3C ARIA 1.2 Combobox Pattern):**
|
|
394
|
+
* - **Tab**: Moves focus to/from combobox in natural tab order
|
|
395
|
+
* - **Space/Enter**: Opens/closes dropdown, selects focused option
|
|
396
|
+
* - **Down Arrow**: Opens dropdown (if closed) or moves to next option
|
|
267
397
|
* - **Up Arrow**: Moves to previous option (when dropdown is open)
|
|
268
|
-
* - **
|
|
269
|
-
* - **
|
|
270
|
-
* - **
|
|
398
|
+
* - **Home**: Moves to first option in list
|
|
399
|
+
* - **End**: Moves to last option in list
|
|
400
|
+
* - **Escape**: Closes dropdown and returns focus to trigger button
|
|
271
401
|
* - **Type-ahead**: Real-time filtering as user types in search input
|
|
402
|
+
* - **Character Keys**: When dropdown is closed, opens and starts filtering
|
|
272
403
|
*
|
|
273
404
|
* **Screen Reader Support:**
|
|
274
|
-
* -
|
|
275
|
-
* -
|
|
276
|
-
* -
|
|
277
|
-
* -
|
|
405
|
+
* - **Selection Announcements**: Current selection state announced on focus
|
|
406
|
+
* - **Option Count**: Number of available options announced when dropdown opens
|
|
407
|
+
* - **Filter Results**: Live announcements of filtered results count
|
|
408
|
+
* - **State Changes**: Open/close state changes announced appropriately
|
|
409
|
+
* - **Empty State**: Clear messaging when no options match search
|
|
410
|
+
* - **Disabled Feedback**: Disabled options announced and properly skipped
|
|
278
411
|
*
|
|
279
412
|
* **Focus Management:**
|
|
280
|
-
* - DOM focus remains on combobox trigger for screen reader compatibility
|
|
281
|
-
* - Visual
|
|
282
|
-
* - Search input automatically receives focus when dropdown opens
|
|
283
|
-
* - Focus returns to trigger when dropdown closes via Escape or selection
|
|
413
|
+
* - **Programmatic Focus**: DOM focus remains on combobox trigger for screen reader compatibility
|
|
414
|
+
* - **Visual Focus**: Options highlighted using `aria-activedescendant` pattern
|
|
415
|
+
* - **Auto Focus**: Search input automatically receives focus when dropdown opens
|
|
416
|
+
* - **Focus Return**: Focus returns to trigger when dropdown closes via Escape or selection
|
|
417
|
+
* - **Focus Trap**: Focus contained within popover when modal behavior is desired
|
|
284
418
|
*
|
|
285
419
|
* **Visual Accessibility:**
|
|
286
|
-
* -
|
|
287
|
-
* -
|
|
288
|
-
* -
|
|
289
|
-
* -
|
|
420
|
+
* - **Color Contrast**: All states meet WCAG AA contrast requirements (4.5:1)
|
|
421
|
+
* - **Focus Indicators**: Clear visual focus indicators for all interactive elements
|
|
422
|
+
* - **Hover States**: Distinct hover states for better interaction feedback
|
|
423
|
+
* - **Disabled State**: Visually distinct disabled options with reduced opacity
|
|
424
|
+
* - **Selection Indicator**: Check icon provides clear visual selection feedback
|
|
425
|
+
* - **High Contrast Mode**: Supports Windows High Contrast Mode
|
|
426
|
+
*
|
|
427
|
+
* **Touch and Mobile Accessibility:**
|
|
428
|
+
* - **Touch Targets**: Minimum 44px touch target size for mobile interactions
|
|
429
|
+
* - **Scroll Behavior**: Proper scroll support for long option lists
|
|
430
|
+
* - **Responsive Design**: Adapts to different screen sizes and orientations
|
|
431
|
+
* - **Mobile Navigation**: Touch-optimized selection and scrolling
|
|
290
432
|
*
|
|
291
433
|
* @performance
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* -
|
|
434
|
+
*
|
|
435
|
+
* **Optimization Strategies:**
|
|
436
|
+
* - **Efficient Filtering**: Single-pass option filtering with memoized results
|
|
437
|
+
* - **Minimal Re-renders**: Optimized state management prevents unnecessary renders
|
|
438
|
+
* - **Virtual Scrolling**: Built-in support for large datasets via CMDK virtualization
|
|
439
|
+
* - **Debounced Search**: Search input changes debounced to prevent excessive API calls
|
|
440
|
+
* - **Lazy Loading**: Options can be loaded asynchronously as needed
|
|
441
|
+
*
|
|
442
|
+
* **Memory Management:**
|
|
443
|
+
* - **Event Cleanup**: Proper cleanup of event listeners and timers
|
|
444
|
+
* - **Reference Management**: No memory leaks through proper ref handling
|
|
445
|
+
* - **Option Caching**: Previously loaded options cached for better performance
|
|
446
|
+
*
|
|
447
|
+
* @technical
|
|
448
|
+
*
|
|
449
|
+
* **Component Composition:**
|
|
450
|
+
* ```
|
|
451
|
+
* Combobox
|
|
452
|
+
* ├── Popover (Radix UI)
|
|
453
|
+
* │ ├── PopoverTrigger
|
|
454
|
+
* │ │ └── Button (outline variant)
|
|
455
|
+
* │ └── PopoverContent
|
|
456
|
+
* │ └── Command (CMDK)
|
|
457
|
+
* │ ├── CommandInput (search)
|
|
458
|
+
* │ └── CommandList
|
|
459
|
+
* │ ├── CommandEmpty
|
|
460
|
+
* │ └── CommandGroup
|
|
461
|
+
* │ └── CommandItem(s)
|
|
462
|
+
* ```
|
|
463
|
+
*
|
|
464
|
+
* **State Flow:**
|
|
465
|
+
* 1. User clicks trigger → Popover opens → Command input focuses
|
|
466
|
+
* 2. User types → CMDK filters options → Results update
|
|
467
|
+
* 3. User navigates → aria-activedescendant updates → Visual focus moves
|
|
468
|
+
* 4. User selects → onValueChange fires → Popover closes → Focus returns
|
|
469
|
+
*
|
|
470
|
+
* **Event Handling:**
|
|
471
|
+
* - Popover manages open/close state and positioning
|
|
472
|
+
* - CMDK handles keyboard navigation and filtering
|
|
473
|
+
* - Button handles trigger interactions and ARIA states
|
|
474
|
+
* - Custom logic manages selection and deselection behavior
|
|
295
475
|
*
|
|
296
476
|
* @see {@link https://ui.shadcn.com/docs/components/combobox} shadcn/ui Combobox documentation
|
|
297
477
|
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/combobox/} W3C ARIA Combobox Pattern
|
|
478
|
+
* @see {@link https://www.radix-ui.com/primitives/docs/components/popover} Radix UI Popover API
|
|
479
|
+
* @see {@link https://cmdk.paco.me/} CMDK Command Menu documentation
|
|
298
480
|
* @see {@link Select} For simpler dropdowns without search functionality
|
|
299
481
|
* @see {@link Command} The underlying command menu component
|
|
300
482
|
* @see {@link Popover} The popover container component
|
|
483
|
+
* @see {@link Button} The trigger button component
|
|
301
484
|
* @since 1.0.0
|
|
302
485
|
*/
|
|
303
|
-
|
|
486
|
+
function Combobox({
|
|
304
487
|
options,
|
|
305
488
|
value,
|
|
306
489
|
onValueChange,
|
|
@@ -311,7 +494,19 @@ export function Combobox({
|
|
|
311
494
|
disabled = false,
|
|
312
495
|
buttonClassName,
|
|
313
496
|
popoverClassName,
|
|
314
|
-
|
|
497
|
+
...popoverProps
|
|
498
|
+
}: {
|
|
499
|
+
options: ComboboxOption[];
|
|
500
|
+
value?: string;
|
|
501
|
+
onValueChange?: (value: string) => void;
|
|
502
|
+
placeholder?: string;
|
|
503
|
+
emptyText?: string;
|
|
504
|
+
searchPlaceholder?: string;
|
|
505
|
+
className?: string;
|
|
506
|
+
disabled?: boolean;
|
|
507
|
+
buttonClassName?: string;
|
|
508
|
+
popoverClassName?: string;
|
|
509
|
+
} & Omit<React.ComponentProps<typeof Popover>, "children">) {
|
|
315
510
|
const [open, setOpen] = React.useState(false);
|
|
316
511
|
|
|
317
512
|
const selectedOption = options.find((option) => option.value === value);
|
|
@@ -324,12 +519,14 @@ export function Combobox({
|
|
|
324
519
|
|
|
325
520
|
return (
|
|
326
521
|
<div className={className}>
|
|
327
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
522
|
+
<Popover open={open} onOpenChange={setOpen} {...popoverProps}>
|
|
328
523
|
<PopoverTrigger asChild>
|
|
329
524
|
<Button
|
|
330
525
|
variant="outline"
|
|
331
526
|
role="combobox"
|
|
332
527
|
aria-expanded={open}
|
|
528
|
+
aria-haspopup="listbox"
|
|
529
|
+
aria-controls={open ? "combobox-options" : undefined}
|
|
333
530
|
disabled={disabled}
|
|
334
531
|
className={cn(
|
|
335
532
|
"w-full justify-between",
|
|
@@ -344,7 +541,7 @@ export function Combobox({
|
|
|
344
541
|
<PopoverContent className={cn("w-full p-0", popoverClassName)}>
|
|
345
542
|
<Command>
|
|
346
543
|
<CommandInput placeholder={searchPlaceholder} className="h-9" />
|
|
347
|
-
<CommandList>
|
|
544
|
+
<CommandList id="combobox-options">
|
|
348
545
|
<CommandEmpty>{emptyText}</CommandEmpty>
|
|
349
546
|
<CommandGroup>
|
|
350
547
|
{options.map((option) => (
|
|
@@ -353,12 +550,14 @@ export function Combobox({
|
|
|
353
550
|
value={option.value}
|
|
354
551
|
disabled={option.disabled}
|
|
355
552
|
onSelect={handleSelect}
|
|
553
|
+
aria-selected={value === option.value}
|
|
356
554
|
>
|
|
357
555
|
<Check
|
|
358
556
|
className={cn(
|
|
359
557
|
"mr-2 h-4 w-4",
|
|
360
558
|
value === option.value ? "opacity-100" : "opacity-0",
|
|
361
559
|
)}
|
|
560
|
+
aria-hidden="true"
|
|
362
561
|
/>
|
|
363
562
|
{option.label}
|
|
364
563
|
</CommandItem>
|
|
@@ -371,3 +570,5 @@ export function Combobox({
|
|
|
371
570
|
</div>
|
|
372
571
|
);
|
|
373
572
|
}
|
|
573
|
+
|
|
574
|
+
export { Combobox };
|