@adamosuiteservices/ui 1.7.7 → 1.7.9
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/combobox.cjs +2 -2
- package/dist/combobox.js +271 -305
- package/dist/components/ui/combobox/combobox.d.ts +0 -20
- package/dist/components/ui/combobox/combobox.stories.d.ts +2 -5
- package/dist/custom-layered-styles.css +1 -1
- package/dist/styles.css +1 -1
- package/docs/components/ui/combobox.md +536 -654
- package/package.json +1 -1
|
@@ -1,654 +1,536 @@
|
|
|
1
|
-
# Combobox
|
|
2
|
-
|
|
3
|
-
Componente de selección avanzado que combina Popover + Command para búsqueda y selección. Soporta selección simple/múltiple, búsqueda filtrable, opciones deshabilitadas, feedback visual con checkbox o check, placeholder persistente, iconos personalizados, y renderizado completamente personalizable mediante render props.
|
|
4
|
-
|
|
5
|
-
## Importación
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
import { Combobox } from "@adamosuiteservices/ui/combobox";
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Anatomía
|
|
12
|
-
|
|
13
|
-
```tsx
|
|
14
|
-
<Combobox
|
|
15
|
-
options={[
|
|
16
|
-
{ value: "option1", label: "Option 1" },
|
|
17
|
-
{ value: "option2", label: "Option 2", disabled: true },
|
|
18
|
-
]}
|
|
19
|
-
value={selectedValue}
|
|
20
|
-
onValueChange={setSelectedValue}
|
|
21
|
-
/>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
**Componente único**: Internamente usa Popover, Command, Button, Checkbox
|
|
25
|
-
|
|
26
|
-
## Props Principales
|
|
27
|
-
|
|
28
|
-
| Prop | Tipo | Default | Descripción |
|
|
29
|
-
| ----------------------- | --------------------------------------- | ------------ | ----------------------------------------------------------- |
|
|
30
|
-
| `options` | `ComboboxOption[]` | **required** | Array de opciones `{ value, label, disabled? }` |
|
|
31
|
-
| `value` | `string \| string[]` | - | Valor controlado (string para single, array para multiple) |
|
|
32
|
-
| `onValueChange` | `(value: string \| string[]) => void` | - | Callback al cambiar selección |
|
|
33
|
-
| `searchable` | `boolean` | `false` | Habilita input de búsqueda |
|
|
34
|
-
| `multiple` | `boolean` | `false` | Permite selección múltiple |
|
|
35
|
-
| `selectedFeedback` | `"checkbox" \| "check"` | `"checkbox"` | Tipo de indicador visual |
|
|
36
|
-
| `icon` | `ComponentType<{ className?: string }>` | - | Componente de icono opcional para mostrar en el trigger |
|
|
37
|
-
| `alwaysShowPlaceholder` | `boolean` | `false` | Mantiene el placeholder visible junto al valor seleccionado |
|
|
38
|
-
| `valuePosition` | `"left" \| "right"` | `"right"` | Posición del valor cuando alwaysShowPlaceholder es true |
|
|
39
|
-
| `labels` | `ComboboxLabels` | - | Textos personalizables (ver tabla abajo) |
|
|
40
|
-
| `classNames` | `ComboboxClassNames` | - | Clases CSS por sección (ver tabla abajo) |
|
|
41
|
-
| `renders` | `ComboboxRenderProps` | - | Funciones de renderizado personalizadas (ver tabla abajo) |
|
|
42
|
-
|
|
43
|
-
### ComboboxLabels
|
|
44
|
-
|
|
45
|
-
| Prop | Tipo | Default | Descripción |
|
|
46
|
-
| ------------------- | --------------------------- | ---------------------- | ----------------------------------------------- |
|
|
47
|
-
| `placeholder` | `string` | `"Select options..."` | Texto cuando no hay selección |
|
|
48
|
-
| `searchPlaceholder` | `string` | `"Search options..."` | Placeholder del input de búsqueda |
|
|
49
|
-
| `noItemsFound` | `string` | `"No options found."` | Mensaje cuando búsqueda no encuentra resultados |
|
|
50
|
-
| `multipleSelected` | `(count: number) => string` | `"X options selected"` | Función para texto de múltiples seleccionados |
|
|
51
|
-
|
|
52
|
-
### ComboboxClassNames
|
|
53
|
-
|
|
54
|
-
| Prop | Descripción |
|
|
55
|
-
| ---------- | -------------------------------------------------------- |
|
|
56
|
-
| `trigger` | Button trigger del popover |
|
|
57
|
-
| `popover` | Contenedor del popover |
|
|
58
|
-
| `command` | Componente Command root |
|
|
59
|
-
| `input` | Input de búsqueda |
|
|
60
|
-
| `list` | Lista de opciones |
|
|
61
|
-
| `empty` | Mensaje de no resultados |
|
|
62
|
-
| `group` | Grupo de items |
|
|
63
|
-
| `item` | Cada opción individual |
|
|
64
|
-
| `checkbox` | Checkbox de selección (si `selectedFeedback="checkbox"`) |
|
|
65
|
-
| `check` | Icono de check (si `selectedFeedback="check"`) |
|
|
66
|
-
|
|
67
|
-
### ComboboxRenderProps
|
|
68
|
-
|
|
69
|
-
Funciones de renderizado personalizadas para control total sobre la UI. Todas son opcionales y reciben props relevantes.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{ value: "
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
{ value: "
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
</
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
<
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
###
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
{values.length}
|
|
538
|
-
</span>
|
|
539
|
-
)}
|
|
540
|
-
</div>
|
|
541
|
-
),
|
|
542
|
-
// Custom completo sin Original
|
|
543
|
-
optionLabel: ({ option }) => (
|
|
544
|
-
<div className="flex items-center gap-2">
|
|
545
|
-
<span>🚀</span>
|
|
546
|
-
<span className="font-medium">{option.label}</span>
|
|
547
|
-
</div>
|
|
548
|
-
),
|
|
549
|
-
// Mejora el Original
|
|
550
|
-
displayValue: ({ Original, text }) => (
|
|
551
|
-
<div className="flex items-center gap-2">
|
|
552
|
-
<Original />
|
|
553
|
-
{text && <span className="text-xs opacity-50">selected</span>}
|
|
554
|
-
</div>
|
|
555
|
-
),
|
|
556
|
-
}}
|
|
557
|
-
/>
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
## Modos de Operación
|
|
561
|
-
|
|
562
|
-
### Single Selection
|
|
563
|
-
|
|
564
|
-
- `multiple={false}` (default)
|
|
565
|
-
- `value` es `string`
|
|
566
|
-
- Click en opción cierra el popover automáticamente
|
|
567
|
-
- Seleccionar opción ya seleccionada la deselecciona
|
|
568
|
-
|
|
569
|
-
### Multiple Selection
|
|
570
|
-
|
|
571
|
-
- `multiple={true}`
|
|
572
|
-
- `value` es `string[]`
|
|
573
|
-
- Click en opción NO cierra el popover
|
|
574
|
-
- Checkboxes permiten toggle de múltiples opciones
|
|
575
|
-
- Display muestra contador cuando >1 seleccionado
|
|
576
|
-
|
|
577
|
-
## Casos de Uso Comunes
|
|
578
|
-
|
|
579
|
-
**Framework selector**: Selección de tecnología con búsqueda
|
|
580
|
-
**Country/City picker**: Selección geográfica con opciones deshabilitadas
|
|
581
|
-
**Tags/Categories**: Selección múltiple de categorías
|
|
582
|
-
**Settings filters**: Filtros multi-selección en dashboards
|
|
583
|
-
**User assignment**: Asignar múltiples usuarios a tarea
|
|
584
|
-
**Language selector**: Selección de idioma con búsqueda
|
|
585
|
-
**Status selector con icono**: Usar `icon` + `alwaysShowPlaceholder` para selectores etiquetados
|
|
586
|
-
**Rich options**: Usar `renders.optionLabel` para mostrar avatares, badges, etc.
|
|
587
|
-
**Custom UI**: Usar `renders.trigger` para triggers completamente personalizados
|
|
588
|
-
|
|
589
|
-
## Estados Internos
|
|
590
|
-
|
|
591
|
-
### Display Text
|
|
592
|
-
|
|
593
|
-
- **Sin selección**: Muestra `labels.placeholder`
|
|
594
|
-
- **Single selection**: Muestra `label` de la opción seleccionada
|
|
595
|
-
- **Single selection + alwaysShowPlaceholder**: Muestra `placeholder: label` separados
|
|
596
|
-
- **Multiple (1 item)**: Muestra `label` del único item seleccionado
|
|
597
|
-
- **Multiple (1 item) + alwaysShowPlaceholder**: Muestra `placeholder: label` separados
|
|
598
|
-
- **Multiple (>1 items)**: Muestra resultado de `labels.multipleSelected(count)`
|
|
599
|
-
- **Multiple (>1 items) + alwaysShowPlaceholder**: Muestra `placeholder: X items selected` separados
|
|
600
|
-
|
|
601
|
-
### Popover State
|
|
602
|
-
|
|
603
|
-
- Controlado internamente con `useState`
|
|
604
|
-
- Se cierra automáticamente en single selection
|
|
605
|
-
- Permanece abierto en multiple selection
|
|
606
|
-
|
|
607
|
-
## Accesibilidad
|
|
608
|
-
|
|
609
|
-
- ✅ **ARIA**: Button tiene `role="combobox"` y `aria-expanded`
|
|
610
|
-
- ✅ **Navegación teclado**: Arrow keys para navegar opciones, Enter para seleccionar, Escape para cerrar
|
|
611
|
-
- ✅ **Screen readers**: Anuncia opciones y estado seleccionado
|
|
612
|
-
- ✅ **Focus management**: Focus automático en input cuando searchable
|
|
613
|
-
- ✅ **Disabled options**: No seleccionables, estilo visual diferente
|
|
614
|
-
|
|
615
|
-
## Notas de Implementación
|
|
616
|
-
|
|
617
|
-
- **Composición**: Usa Popover + Command + Button + Checkbox internamente
|
|
618
|
-
- **Búsqueda**: Command component maneja fuzzy search automáticamente
|
|
619
|
-
- **Estado interno**: Soporta modo controlado y no controlado
|
|
620
|
-
- **Tipo de value**: Cambia según `multiple` (string vs string[])
|
|
621
|
-
- **selectedFeedback**:
|
|
622
|
-
- `"checkbox"`: Muestra Checkbox a la izquierda de cada opción
|
|
623
|
-
- `"check"`: Muestra CheckIcon a la derecha (agrega `pr-8` automáticamente)
|
|
624
|
-
- **Popover alignment**: `align="start"` por defecto para alinear con trigger
|
|
625
|
-
- **ChevronDownIcon**: Se muestra en trigger con `opacity-50` (puede sobrescribirse con `renders.triggerIcon`)
|
|
626
|
-
- **No animation**: Selección instantánea, sin delays
|
|
627
|
-
- **alwaysShowPlaceholder**: Renderiza placeholder y valor como elementos separados con gap
|
|
628
|
-
- **valuePosition**: Controla donde aparece el valor cuando `alwaysShowPlaceholder` es true ("left" o "right", default "right")
|
|
629
|
-
- **icon**: Se muestra con `opacity-50` por defecto cuando se proporciona el prop `icon`
|
|
630
|
-
- **Render props**: Tienen prioridad sobre renderizado por defecto, permiten personalización completa
|
|
631
|
-
- **Composición de renders**: Puedes combinar múltiples render props para personalización granular
|
|
632
|
-
- **Original component**: Cada render prop recibe un componente `Original` para renderizar la implementación por defecto, permitiendo envolver o mejorar sin reimplementar
|
|
633
|
-
|
|
634
|
-
## Troubleshooting
|
|
635
|
-
|
|
636
|
-
**Value no cambia**: Verifica que uses `onValueChange` en modo controlado
|
|
637
|
-
**Búsqueda no funciona**: Asegúrate de pasar `searchable={true}`
|
|
638
|
-
**Multiple selection cierra popover**: Verifica que `multiple={true}` esté activo
|
|
639
|
-
**Labels incorrectas en multiple**: Usa función `multipleSelected` para pluralización
|
|
640
|
-
**Tipo de value incorrecto**: Single usa `string`, Multiple usa `string[]`
|
|
641
|
-
**Checkboxes no aparecen**: Solo aparecen cuando `selectedFeedback="checkbox"` (default)
|
|
642
|
-
**Check icon no visible**: Solo aparece cuando `selectedFeedback="check"`
|
|
643
|
-
**Icon no se muestra**: Asegúrate de pasar el prop `icon` con un componente válido
|
|
644
|
-
**Placeholder desaparece al seleccionar**: Si quieres mantenerlo visible, usa `alwaysShowPlaceholder={true}`
|
|
645
|
-
**Render prop no funciona**: Verifica que el render prop retorne un `ReactNode` válido
|
|
646
|
-
**Custom trigger pierde funcionalidad**: Cuando usas `renders.trigger`, debes manejar el click manualmente o envolver en `PopoverTrigger`
|
|
647
|
-
**Original component no renderiza**: Asegúrate de usar `<Original />` como componente JSX, no como función
|
|
648
|
-
**Quiero envolver sin reimplementar**: Usa el componente `Original` que recibe cada render prop para mantener la funcionalidad por defecto
|
|
649
|
-
|
|
650
|
-
## Referencias
|
|
651
|
-
|
|
652
|
-
- **Radix UI Popover**: <https://www.radix-ui.com/primitives/docs/components/popover>
|
|
653
|
-
- **Command Component**: Ver documentación de Command
|
|
654
|
-
- **ARIA Combobox**: <https://www.w3.org/WAI/ARIA/apg/patterns/combobox/>
|
|
1
|
+
# Combobox
|
|
2
|
+
|
|
3
|
+
Componente de selección avanzado que combina Popover + Command para búsqueda y selección. Soporta selección simple/múltiple, búsqueda filtrable, opciones deshabilitadas, feedback visual con checkbox o check, placeholder persistente, iconos personalizados, y renderizado completamente personalizable mediante render props.
|
|
4
|
+
|
|
5
|
+
## Importación
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Combobox } from "@adamosuiteservices/ui/combobox";
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Anatomía
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Combobox
|
|
15
|
+
options={[
|
|
16
|
+
{ value: "option1", label: "Option 1" },
|
|
17
|
+
{ value: "option2", label: "Option 2", disabled: true },
|
|
18
|
+
]}
|
|
19
|
+
value={selectedValue}
|
|
20
|
+
onValueChange={setSelectedValue}
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Componente único**: Internamente usa Popover, Command, Button, Checkbox
|
|
25
|
+
|
|
26
|
+
## Props Principales
|
|
27
|
+
|
|
28
|
+
| Prop | Tipo | Default | Descripción |
|
|
29
|
+
| ----------------------- | --------------------------------------- | ------------ | ----------------------------------------------------------- |
|
|
30
|
+
| `options` | `ComboboxOption[]` | **required** | Array de opciones `{ value, label, disabled? }` |
|
|
31
|
+
| `value` | `string \| string[]` | - | Valor controlado (string para single, array para multiple) |
|
|
32
|
+
| `onValueChange` | `(value: string \| string[]) => void` | - | Callback al cambiar selección |
|
|
33
|
+
| `searchable` | `boolean` | `false` | Habilita input de búsqueda |
|
|
34
|
+
| `multiple` | `boolean` | `false` | Permite selección múltiple |
|
|
35
|
+
| `selectedFeedback` | `"checkbox" \| "check"` | `"checkbox"` | Tipo de indicador visual |
|
|
36
|
+
| `icon` | `ComponentType<{ className?: string }>` | - | Componente de icono opcional para mostrar en el trigger |
|
|
37
|
+
| `alwaysShowPlaceholder` | `boolean` | `false` | Mantiene el placeholder visible junto al valor seleccionado |
|
|
38
|
+
| `valuePosition` | `"left" \| "right"` | `"right"` | Posición del valor cuando alwaysShowPlaceholder es true |
|
|
39
|
+
| `labels` | `ComboboxLabels` | - | Textos personalizables (ver tabla abajo) |
|
|
40
|
+
| `classNames` | `ComboboxClassNames` | - | Clases CSS por sección (ver tabla abajo) |
|
|
41
|
+
| `renders` | `ComboboxRenderProps` | - | Funciones de renderizado personalizadas (ver tabla abajo) |
|
|
42
|
+
|
|
43
|
+
### ComboboxLabels
|
|
44
|
+
|
|
45
|
+
| Prop | Tipo | Default | Descripción |
|
|
46
|
+
| ------------------- | --------------------------- | ---------------------- | ----------------------------------------------- |
|
|
47
|
+
| `placeholder` | `string` | `"Select options..."` | Texto cuando no hay selección |
|
|
48
|
+
| `searchPlaceholder` | `string` | `"Search options..."` | Placeholder del input de búsqueda |
|
|
49
|
+
| `noItemsFound` | `string` | `"No options found."` | Mensaje cuando búsqueda no encuentra resultados |
|
|
50
|
+
| `multipleSelected` | `(count: number) => string` | `"X options selected"` | Función para texto de múltiples seleccionados |
|
|
51
|
+
|
|
52
|
+
### ComboboxClassNames
|
|
53
|
+
|
|
54
|
+
| Prop | Descripción |
|
|
55
|
+
| ---------- | -------------------------------------------------------- |
|
|
56
|
+
| `trigger` | Button trigger del popover |
|
|
57
|
+
| `popover` | Contenedor del popover |
|
|
58
|
+
| `command` | Componente Command root |
|
|
59
|
+
| `input` | Input de búsqueda |
|
|
60
|
+
| `list` | Lista de opciones |
|
|
61
|
+
| `empty` | Mensaje de no resultados |
|
|
62
|
+
| `group` | Grupo de items |
|
|
63
|
+
| `item` | Cada opción individual |
|
|
64
|
+
| `checkbox` | Checkbox de selección (si `selectedFeedback="checkbox"`) |
|
|
65
|
+
| `check` | Icono de check (si `selectedFeedback="check"`) |
|
|
66
|
+
|
|
67
|
+
### ComboboxRenderProps
|
|
68
|
+
|
|
69
|
+
Funciones de renderizado personalizadas para control total sobre la UI. Todas son opcionales y reciben props relevantes.
|
|
70
|
+
|
|
71
|
+
| Prop | Parámetros | Descripción |
|
|
72
|
+
| ------------------ | ----------------------------------------------------------- | --------------------------------------- |
|
|
73
|
+
| `trigger` | `{ open, value, displayText, placeholder, hasValue, icon }` | Renderiza el botón trigger completo |
|
|
74
|
+
| `triggerIcon` | `{ open }` | Renderiza el icono del dropdown |
|
|
75
|
+
| `placeholder` | `{ text, hasValue }` | Renderiza el texto del placeholder |
|
|
76
|
+
| `displayValue` | `{ text, value }` | Renderiza el valor seleccionado |
|
|
77
|
+
| `option` | `{ option, isSelected, selectedFeedback }` | Renderiza una opción completa |
|
|
78
|
+
| `optionLabel` | `{ option }` | Renderiza solo la etiqueta de la opción |
|
|
79
|
+
| `selectedFeedback` | `{ option, isSelected, type }` | Renderiza el indicador de selección |
|
|
80
|
+
| `empty` | `{ text }` | Renderiza el estado vacío |
|
|
81
|
+
| `searchInput` | `{ placeholder }` | Renderiza el input de búsqueda |
|
|
82
|
+
|
|
83
|
+
## Patrones de Uso
|
|
84
|
+
|
|
85
|
+
### Básico (Sin Búsqueda)
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const frameworks = [
|
|
89
|
+
{ value: "next", label: "Next.js" },
|
|
90
|
+
{ value: "remix", label: "Remix" },
|
|
91
|
+
{ value: "astro", label: "Astro" },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
<Combobox
|
|
95
|
+
options={frameworks}
|
|
96
|
+
labels={{ placeholder: "Select framework..." }}
|
|
97
|
+
/>;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Con Icono
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { CalendarIcon } from "lucide-react";
|
|
104
|
+
|
|
105
|
+
<Combobox
|
|
106
|
+
searchable
|
|
107
|
+
icon={CalendarIcon}
|
|
108
|
+
options={frameworks}
|
|
109
|
+
labels={{ placeholder: "Select framework..." }}
|
|
110
|
+
/>;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Placeholder Persistente
|
|
114
|
+
|
|
115
|
+
Útil para usar el placeholder como etiqueta que permanece visible.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<Combobox
|
|
119
|
+
searchable
|
|
120
|
+
alwaysShowPlaceholder
|
|
121
|
+
valuePosition="right"
|
|
122
|
+
options={frameworks}
|
|
123
|
+
value={value}
|
|
124
|
+
onValueChange={setValue}
|
|
125
|
+
labels={{ placeholder: "Framework" }}
|
|
126
|
+
/>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Comportamiento**:
|
|
130
|
+
|
|
131
|
+
- Sin valor: Muestra solo "Framework"
|
|
132
|
+
- Con valor y `valuePosition="right"`: Muestra "Framework" a la izquierda y el valor a la derecha
|
|
133
|
+
- Con valor y `valuePosition="left"`: Muestra "Framework" y el valor ambos a la izquierda
|
|
134
|
+
|
|
135
|
+
### Posición del Valor
|
|
136
|
+
|
|
137
|
+
Cuando `alwaysShowPlaceholder` es true, puedes controlar dónde aparece el valor seleccionado:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// Valor a la derecha (default)
|
|
141
|
+
<Combobox
|
|
142
|
+
alwaysShowPlaceholder
|
|
143
|
+
valuePosition="right"
|
|
144
|
+
options={frameworks}
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
// Valor a la izquierda junto al placeholder
|
|
148
|
+
<Combobox
|
|
149
|
+
alwaysShowPlaceholder
|
|
150
|
+
valuePosition="left"
|
|
151
|
+
options={frameworks}
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Layouts resultantes**:
|
|
156
|
+
|
|
157
|
+
- `valuePosition="right"`: `[Icon] [Placeholder]` ... `[Value] [Chevron]`
|
|
158
|
+
- `valuePosition="left"`: `[Icon] [Placeholder] [Value]` ... `[Chevron]`
|
|
159
|
+
|
|
160
|
+
### Icono + Placeholder Persistente
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<Combobox
|
|
164
|
+
searchable
|
|
165
|
+
alwaysShowPlaceholder
|
|
166
|
+
icon={CalendarIcon}
|
|
167
|
+
options={frameworks}
|
|
168
|
+
value={value}
|
|
169
|
+
onValueChange={setValue}
|
|
170
|
+
labels={{ placeholder: "Framework" }}
|
|
171
|
+
/>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Con Búsqueda
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<Combobox
|
|
178
|
+
searchable
|
|
179
|
+
options={frameworks}
|
|
180
|
+
labels={{
|
|
181
|
+
placeholder: "Select framework...",
|
|
182
|
+
searchPlaceholder: "Search frameworks...",
|
|
183
|
+
noItemsFound: "No framework found.",
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Selección Simple Controlada
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
const [value, setValue] = useState("");
|
|
192
|
+
|
|
193
|
+
<Combobox
|
|
194
|
+
searchable
|
|
195
|
+
options={frameworks}
|
|
196
|
+
value={value}
|
|
197
|
+
onValueChange={(newValue) => setValue(newValue as string)}
|
|
198
|
+
labels={{ placeholder: "Select framework..." }}
|
|
199
|
+
/>;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Selección Múltiple
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
const [values, setValues] = useState<string[]>([]);
|
|
206
|
+
|
|
207
|
+
<Combobox
|
|
208
|
+
searchable
|
|
209
|
+
multiple
|
|
210
|
+
options={frameworks}
|
|
211
|
+
value={values}
|
|
212
|
+
onValueChange={(newValues) => setValues(newValues as string[])}
|
|
213
|
+
labels={{
|
|
214
|
+
placeholder: "Select frameworks...",
|
|
215
|
+
multipleSelected: (count) => `${count} frameworks selected`,
|
|
216
|
+
}}
|
|
217
|
+
/>;
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Con Feedback de Check
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
<Combobox
|
|
224
|
+
searchable
|
|
225
|
+
options={frameworks}
|
|
226
|
+
selectedFeedback="check"
|
|
227
|
+
labels={{ placeholder: "Select option..." }}
|
|
228
|
+
/>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Diferencia**: `"checkbox"` muestra checkboxes a la izquierda, `"check"` muestra ícono check a la derecha.
|
|
232
|
+
|
|
233
|
+
### Opciones Deshabilitadas
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
const options = [
|
|
237
|
+
{ value: "us", label: "United States" },
|
|
238
|
+
{ value: "ca", label: "Canada" },
|
|
239
|
+
{ value: "mx", label: "Mexico", disabled: true },
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
<Combobox
|
|
243
|
+
searchable
|
|
244
|
+
options={options}
|
|
245
|
+
labels={{ placeholder: "Select country..." }}
|
|
246
|
+
/>;
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Labels Personalizadas
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
<Combobox
|
|
253
|
+
searchable
|
|
254
|
+
multiple
|
|
255
|
+
options={frameworks}
|
|
256
|
+
labels={{
|
|
257
|
+
placeholder: "Choose your stack...",
|
|
258
|
+
searchPlaceholder: "Type to filter...",
|
|
259
|
+
noItemsFound: "No matching technologies found.",
|
|
260
|
+
multipleSelected: (count) =>
|
|
261
|
+
`${count} ${count === 1 ? "tech" : "techs"} selected`,
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Styling Personalizado
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
<Combobox
|
|
270
|
+
searchable
|
|
271
|
+
options={frameworks}
|
|
272
|
+
classNames={{
|
|
273
|
+
trigger: "border-2 border-blue-500 w-64",
|
|
274
|
+
popover: "border-blue-200 shadow-lg",
|
|
275
|
+
item: "rounded px-3 py-2 hover:bg-blue-50",
|
|
276
|
+
check: "text-blue-600",
|
|
277
|
+
}}
|
|
278
|
+
/>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### En Formulario
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { Field, FieldLabel } from "@adamosuiteservices/ui/field";
|
|
285
|
+
|
|
286
|
+
function FormExample() {
|
|
287
|
+
const [framework, setFramework] = useState("");
|
|
288
|
+
const [country, setCountry] = useState("");
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<form className="space-y-4">
|
|
292
|
+
<Field>
|
|
293
|
+
<FieldLabel>Framework</FieldLabel>
|
|
294
|
+
<Combobox
|
|
295
|
+
searchable
|
|
296
|
+
options={frameworks}
|
|
297
|
+
value={framework}
|
|
298
|
+
onValueChange={(value) => setFramework(value as string)}
|
|
299
|
+
labels={{ placeholder: "Select framework..." }}
|
|
300
|
+
/>
|
|
301
|
+
</Field>
|
|
302
|
+
|
|
303
|
+
<Field>
|
|
304
|
+
<FieldLabel>Country</FieldLabel>
|
|
305
|
+
<Combobox
|
|
306
|
+
searchable
|
|
307
|
+
options={countries}
|
|
308
|
+
value={country}
|
|
309
|
+
onValueChange={(value) => setCountry(value as string)}
|
|
310
|
+
selectedFeedback="check"
|
|
311
|
+
labels={{ placeholder: "Select country..." }}
|
|
312
|
+
/>
|
|
313
|
+
</Field>
|
|
314
|
+
|
|
315
|
+
<button type="submit">Submit</button>
|
|
316
|
+
</form>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Multiple con Clear All
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
const [values, setValues] = useState<string[]>([]);
|
|
325
|
+
|
|
326
|
+
<div className="space-y-2">
|
|
327
|
+
<Combobox
|
|
328
|
+
searchable
|
|
329
|
+
multiple
|
|
330
|
+
options={frameworks}
|
|
331
|
+
value={values}
|
|
332
|
+
onValueChange={(newValues) => setValues(newValues as string[])}
|
|
333
|
+
/>
|
|
334
|
+
|
|
335
|
+
{values.length > 0 && (
|
|
336
|
+
<button
|
|
337
|
+
onClick={() => setValues([])}
|
|
338
|
+
className="text-xs text-blue-600 hover:text-blue-800 underline"
|
|
339
|
+
>
|
|
340
|
+
Clear all
|
|
341
|
+
</button>
|
|
342
|
+
)}
|
|
343
|
+
</div>;
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Renderizado Personalizado
|
|
347
|
+
|
|
348
|
+
El prop `renders` permite personalizar completamente la UI usando el patrón render prop. Todas las funciones son opcionales y permiten reemplazar completamente el renderizado por defecto de cada parte del componente.
|
|
349
|
+
|
|
350
|
+
### Custom Trigger
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
<Combobox
|
|
354
|
+
searchable
|
|
355
|
+
options={frameworks}
|
|
356
|
+
value={value}
|
|
357
|
+
onValueChange={setValue}
|
|
358
|
+
renders={{
|
|
359
|
+
trigger: ({ open, displayText, placeholder, hasValue }) => (
|
|
360
|
+
<button
|
|
361
|
+
type="button"
|
|
362
|
+
className="flex items-center justify-between w-full px-4 py-2 text-sm bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-lg shadow-md hover:shadow-lg transition-all"
|
|
363
|
+
>
|
|
364
|
+
<span>{hasValue ? displayText : placeholder}</span>
|
|
365
|
+
<span className={`transition-transform ${open ? "rotate-180" : ""}`}>
|
|
366
|
+
▼
|
|
367
|
+
</span>
|
|
368
|
+
</button>
|
|
369
|
+
),
|
|
370
|
+
}}
|
|
371
|
+
/>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Mejorar Display Value con Badge
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Custom Display Value con Badge
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
<Combobox
|
|
384
|
+
searchable
|
|
385
|
+
options={frameworks}
|
|
386
|
+
value={value}
|
|
387
|
+
onValueChange={setValue}
|
|
388
|
+
renders={{
|
|
389
|
+
displayValue: ({ text }) => (
|
|
390
|
+
<div className="flex items-center gap-2">
|
|
391
|
+
<span>{text}</span>
|
|
392
|
+
{text && (
|
|
393
|
+
<span className="px-1.5 py-0.5 text-xs bg-green-100 text-green-700 rounded font-medium">
|
|
394
|
+
Active
|
|
395
|
+
</span>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
),
|
|
399
|
+
}}
|
|
400
|
+
/>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Custom Option Label
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
<Combobox
|
|
407
|
+
searchable
|
|
408
|
+
options={frameworks}
|
|
409
|
+
value={value}
|
|
410
|
+
onValueChange={setValue}
|
|
411
|
+
renders={{
|
|
412
|
+
optionLabel: ({ option }) => (
|
|
413
|
+
<div className="flex items-center gap-2">
|
|
414
|
+
<span className="font-semibold text-blue-600">🚀</span>
|
|
415
|
+
<span>{option.label}</span>
|
|
416
|
+
</div>
|
|
417
|
+
),
|
|
418
|
+
}}
|
|
419
|
+
/>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Custom Empty State
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
<Combobox
|
|
426
|
+
searchable
|
|
427
|
+
options={[]}
|
|
428
|
+
labels={{
|
|
429
|
+
noItemsFound: "No items found.",
|
|
430
|
+
}}
|
|
431
|
+
renders={{
|
|
432
|
+
empty: ({ text }) => (
|
|
433
|
+
<div className="py-8 text-center">
|
|
434
|
+
<div className="text-4xl mb-2">🔍</div>
|
|
435
|
+
<p className="text-sm font-medium text-muted-foreground">{text}</p>
|
|
436
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
437
|
+
Try adjusting your search criteria
|
|
438
|
+
</p>
|
|
439
|
+
</div>
|
|
440
|
+
),
|
|
441
|
+
}}
|
|
442
|
+
/>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Modos de Operación
|
|
446
|
+
|
|
447
|
+
### Single Selection
|
|
448
|
+
|
|
449
|
+
- `multiple={false}` (default)
|
|
450
|
+
- `value` es `string`
|
|
451
|
+
- Click en opción cierra el popover automáticamente
|
|
452
|
+
- Seleccionar opción ya seleccionada la deselecciona
|
|
453
|
+
|
|
454
|
+
### Multiple Selection
|
|
455
|
+
|
|
456
|
+
- `multiple={true}`
|
|
457
|
+
- `value` es `string[]`
|
|
458
|
+
- Click en opción NO cierra el popover
|
|
459
|
+
- Checkboxes permiten toggle de múltiples opciones
|
|
460
|
+
- Display muestra contador cuando >1 seleccionado
|
|
461
|
+
|
|
462
|
+
## Casos de Uso Comunes
|
|
463
|
+
|
|
464
|
+
**Framework selector**: Selección de tecnología con búsqueda
|
|
465
|
+
**Country/City picker**: Selección geográfica con opciones deshabilitadas
|
|
466
|
+
**Tags/Categories**: Selección múltiple de categorías
|
|
467
|
+
**Settings filters**: Filtros multi-selección en dashboards
|
|
468
|
+
**User assignment**: Asignar múltiples usuarios a tarea
|
|
469
|
+
**Language selector**: Selección de idioma con búsqueda
|
|
470
|
+
**Status selector con icono**: Usar `icon` + `alwaysShowPlaceholder` para selectores etiquetados
|
|
471
|
+
**Rich options**: Usar `renders.optionLabel` para mostrar avatares, badges, etc.
|
|
472
|
+
**Custom UI**: Usar `renders.trigger` para triggers completamente personalizados
|
|
473
|
+
|
|
474
|
+
## Estados Internos
|
|
475
|
+
|
|
476
|
+
### Display Text
|
|
477
|
+
|
|
478
|
+
- **Sin selección**: Muestra `labels.placeholder`
|
|
479
|
+
- **Single selection**: Muestra `label` de la opción seleccionada
|
|
480
|
+
- **Single selection + alwaysShowPlaceholder**: Muestra `placeholder: label` separados
|
|
481
|
+
- **Multiple (1 item)**: Muestra `label` del único item seleccionado
|
|
482
|
+
- **Multiple (1 item) + alwaysShowPlaceholder**: Muestra `placeholder: label` separados
|
|
483
|
+
- **Multiple (>1 items)**: Muestra resultado de `labels.multipleSelected(count)`
|
|
484
|
+
- **Multiple (>1 items) + alwaysShowPlaceholder**: Muestra `placeholder: X items selected` separados
|
|
485
|
+
|
|
486
|
+
### Popover State
|
|
487
|
+
|
|
488
|
+
- Controlado internamente con `useState`
|
|
489
|
+
- Se cierra automáticamente en single selection
|
|
490
|
+
- Permanece abierto en multiple selection
|
|
491
|
+
|
|
492
|
+
## Accesibilidad
|
|
493
|
+
|
|
494
|
+
- ✅ **ARIA**: Button tiene `role="combobox"` y `aria-expanded`
|
|
495
|
+
- ✅ **Navegación teclado**: Arrow keys para navegar opciones, Enter para seleccionar, Escape para cerrar
|
|
496
|
+
- ✅ **Screen readers**: Anuncia opciones y estado seleccionado
|
|
497
|
+
- ✅ **Focus management**: Focus automático en input cuando searchable
|
|
498
|
+
- ✅ **Disabled options**: No seleccionables, estilo visual diferente
|
|
499
|
+
|
|
500
|
+
## Notas de Implementación
|
|
501
|
+
|
|
502
|
+
- **Composición**: Usa Popover + Command + Button + Checkbox internamente
|
|
503
|
+
- **Búsqueda**: Command component maneja fuzzy search automáticamente
|
|
504
|
+
- **Estado interno**: Soporta modo controlado y no controlado
|
|
505
|
+
- **Tipo de value**: Cambia según `multiple` (string vs string[])
|
|
506
|
+
- **selectedFeedback**:
|
|
507
|
+
- `"checkbox"`: Muestra Checkbox a la izquierda de cada opción
|
|
508
|
+
- `"check"`: Muestra CheckIcon a la derecha (agrega `pr-8` automáticamente)
|
|
509
|
+
- **Popover alignment**: `align="start"` por defecto para alinear con trigger
|
|
510
|
+
- **ChevronDownIcon**: Se muestra en trigger con `opacity-50` (puede sobrescribirse con `renders.triggerIcon`)
|
|
511
|
+
- **No animation**: Selección instantánea, sin delays
|
|
512
|
+
- **alwaysShowPlaceholder**: Renderiza placeholder y valor como elementos separados con gap
|
|
513
|
+
- **valuePosition**: Controla donde aparece el valor cuando `alwaysShowPlaceholder` es true ("left" o "right", default "right")
|
|
514
|
+
- **icon**: Se muestra con `opacity-50` por defecto cuando se proporciona el prop `icon`
|
|
515
|
+
- **Render props**: Tienen prioridad sobre renderizado por defecto, permiten personalización completa
|
|
516
|
+
- **Composición de renders**: Puedes combinar múltiples render props para personalización granular
|
|
517
|
+
|
|
518
|
+
## Troubleshooting
|
|
519
|
+
|
|
520
|
+
**Value no cambia**: Verifica que uses `onValueChange` en modo controlado
|
|
521
|
+
**Búsqueda no funciona**: Asegúrate de pasar `searchable={true}`
|
|
522
|
+
**Multiple selection cierra popover**: Verifica que `multiple={true}` esté activo
|
|
523
|
+
**Labels incorrectas en multiple**: Usa función `multipleSelected` para pluralización
|
|
524
|
+
**Tipo de value incorrecto**: Single usa `string`, Multiple usa `string[]`
|
|
525
|
+
**Checkboxes no aparecen**: Solo aparecen cuando `selectedFeedback="checkbox"` (default)
|
|
526
|
+
**Check icon no visible**: Solo aparece cuando `selectedFeedback="check"`
|
|
527
|
+
**Icon no se muestra**: Asegúrate de pasar el prop `icon` con un componente válido
|
|
528
|
+
**Placeholder desaparece al seleccionar**: Si quieres mantenerlo visible, usa `alwaysShowPlaceholder={true}`
|
|
529
|
+
**Render prop no funciona**: Verifica que el render prop retorne un `ReactNode` válido
|
|
530
|
+
**Custom trigger pierde funcionalidad**: Cuando usas `renders.trigger`, debes manejar el click manualmente o envolver en `PopoverTrigger`
|
|
531
|
+
|
|
532
|
+
## Referencias
|
|
533
|
+
|
|
534
|
+
- **Radix UI Popover**: <https://www.radix-ui.com/primitives/docs/components/popover>
|
|
535
|
+
- **Command Component**: Ver documentación de Command
|
|
536
|
+
- **ARIA Combobox**: <https://www.w3.org/WAI/ARIA/apg/patterns/combobox/>
|