Ir al contenido
Portal de documentación del EGD
Menú de la sección

Selectores con búsqueda en el propio campo

Componente base: src/components/ui/searchable-combobox.tsx (SearchableCombobox). Reemplaza el patrón anterior (botón que abría un desplegable con otro buscador dentro) por el patrón ARIA «combobox con lista»: el campo es un <input role="combobox">; se hace clic, se abre la lista y se escribe ahí mismo.

Asociación de etiqueta (15/10/2026): si el campo usa etiqueta externa, transmitir su id hasta el input y asociar FieldLabel htmlFor al mismo valor. Usar useId para varias instancias. CitizenCombobox y LocationCombobox aceptan ahora id; al recibirlo usan la etiqueta externa como nombre en lugar de sobrescribirla con una etiqueta ARIA genérica. Sin id conservan el nombre de respaldo. DateField nombra su diálogo de calendario «Elegir fecha» y admite calendarLabel para contextualizarlo; no añadir otro calendario ni otro Popover.

Lado de la lista (21/10/2026): la lista se abre hacia abajo; si debajo del campo hay menos de 280 px y arriba hay más sitio, se abre hacia arriba, para mostrar la lista completa (hasta 300 px) en lugar de encogerla a una o dos opciones cuando el campo está cerca del borde inferior de la ventana. Se decide al abrir (clic, flecha o escritura) en SearchableCombobox, que usan también EntityCombobox y OptionCombobox. Select y DropdownMenu ya cambian de lado por la colisión de Radix, porque su alto no se ata al espacio disponible.

Uniformización de todos los selectores (2026-10-08)

Lista de opciones común. Todos los desplegables usan el mismo contenedor y las mismas filas (clases planas en globals.css, sin depender de @media (hover)):

  • .sgd-listbox: fondo --popover, borde de 1 px, esquinas de control, sombra --sgd-shadow-popover y 4 px de relleno.
  • .sgd-option: 40 px de alto, texto de menú (14 px), columna izquierda para la marca de selección (.sgd-option-check), resaltado --accent para la opción activa (data-highlighted de Radix, data-active de los combobox propios, data-selected de cmdk) o con foco, texto en 500 para la elegida, atenuada si está deshabilitada. .sgd-option-flat (sin columna de marca) para acciones.
  • ListboxStatus (searchable-combobox.tsx) + .sgd-listbox-status: «Buscando…», aviso previo, «Sin resultados.» y error (loadError, rojo). EntityCombobox ahora muestra «No se pudieron cargar las opciones. Intenta de nuevo.» si la consulta falla (antes decía «Sin resultados.»).
  • Chevron único expand_more (18 px, 60 % de opacidad) en Select, SearchableCombobox y MultiSelectCombobox (el combobox usaba unfold_more; el múltiple cambiaba a expand_less).

Dónde se aplicó (un cambio en la primitiva alcanza a todos sus usos):

PrimitivaAlcance
ui/select.tsx (SelectContent/SelectItem)los ~94 archivos con Select (filtros, diálogos, paginación…) y FloatingUnderlineSelect (el «Tipo» de la imagen)
ui/dropdown-menu.tsx (contenedor, ítems, casillas y radios)todos los menús de acciones: mismo aspecto, comportamiento propio (siguen siendo menús, no selectores)
ui/command.tsx (CommandItem)paleta/búsqueda global
ui/searchable-combobox.tsxEntityCombobox y *Combobox de entity-pickers, CitizenCombobox, LocationCombobox, FloatingSelectField, OptionCombobox
ui/data-table/multi-select-combobox.tsxfiltros de tablas y, ahora, MultiSelectChecklist

Implementaciones duplicadas sustituidas:

  • MultiSelectChecklist (botón + Command con buscador dentro del desplegable; 2 usos: grupos de destino y asignación masiva de motivos) ahora es un envoltorio de MultiSelectCombobox (se escribe en el campo, chips, fila «Seleccionar todos»), con la misma API.
  • Un <select> nativo (inspections/inspection-document-list.tsx) pasó a Select.
  • Las clases de los campos-combobox (con borde / subrayado) y el marco de etiqueta flotante + error estaban copiados en cada selector: ahora viven en ui/option-combobox.tsx (COMBOBOX_BOXED_INPUT, COMBOBOX_UNDERLINE_INPUT, ComboboxFieldFrame, comboboxChevronClass).
  • Nuevo OptionCombobox (ui/option-combobox.tsx): selector con búsqueda para listas estáticas, subrayado con etiqueta flotante (con label) o con borde. Es la vía para convertir un Select de catálogo largo en uno con búsqueda.

Corrección (2026-10-08): FloatingUnderlineSelect (el «Tipo» de Destinatario, «Motivo de la firma» y «Agregar criterio» de los filtros) seguía siendo un botón Radix que no permitía escribir y se veía distinto de los demás selectores del formulario. Ahora es un envoltorio de OptionCombobox con la misma API (label, value, options, onValueChange, clearable, placeholder, error, disabled): se escribe en el campo, filtra sin distinguir tildes y usa la lista común. Con clearable añade la fila «Todos» (texto de placeholder) en lugar de la «X». Verificado en Chrome: clic abre la lista sin buscador interno, «ciud» filtra a «Ciudadano», ↓ + Enter elige y deja el valor.

Se mantienen las diferencias funcionales: selección simple corta = Select (Radix); simple con búsqueda = SearchableCombobox/OptionCombobox/EntityCombobox; múltiple = MultiSelectCombobox con chips; dependientes (p. ej. Personal según Dependencia) conservan sus parámetros; menús de acciones = DropdownMenu.

Muestra: /design-system/componentes → «Selectores desplegables» (simple subrayado y con borde, con búsqueda en ambos estilos, múltiple, menú de acciones y los tres estados de la lista).

Verificado en Chrome (2026-10-08): contenedor y filas idénticos en las seis variantes de la muestra; modo claro y oscuro (LATAM e Institucional oscuro); axe sin violaciones con la lista abierta; dentro de un modal a 700 × 520 px las listas no se cortan ni salen de la pantalla, Escape cierra solo la lista y un segundo Escape cierra el modal; elegir con teclado (↓, Enter) en simple, con búsqueda y múltiple actualiza el valor (ids correctos). tsc 0 errores, ESLint 0 errores (6 avisos previos ajenos), icons:check OK.

Pendientes reales:

  • Catálogos largos con Select simple. Siguen siendo Select sin búsqueda (solo se unificó su lista y chevron): cambiarlos exige tocar cada pantalla y no se pueden probar sin sesión/backend. Son los de oficinas, usuarios, tipos de documento, cargos, grupos, calendarios, etc. en diálogos y filtros de features/ y en Mesa de Partes (bandeja, referencias, destinatarios). Cada uno: sustituir por OptionCombobox (o EntityCombobox si la lista viene de un endpoint paginado).
  • Los selectores con borde de los diálogos de administración mantienen etiqueta externa (el subrayado flotante se pidió para formularios de documentos y filtros).
  • El campo con borde del combobox muestra, con foco, el contorno global de input además del anillo (igual que Input); el Select con borde solo con teclado. Diferencia menor.
  • No probado: pantallas reales de administración y Mesa de Partes (sin sesión ni backend), CitizenCombobox/LocationCombobox contra el backend, lector de pantalla, Firefox/Safari.

Qué componentes lo usan

ComponenteDóndeNotas
EntityCombobox y todos los *Combobox de entity-pickers.tsx (Oficina, Usuario, Tipo de documento, Prioridad, Trámite, Organización, Ciudadano de catálogo, Grupo de destino, …)src/components/admin/Consulta endpoint?q= con 300 ms de retraso; con label usa subrayado con etiqueta flotante, sin label un campo con borde (tablas compactas). Conserva params (p. ej. officeId de Personal según Dependencia), getLabel, getDescription, clearable, error, required
CitizenComboboxfeatures/citizens/Busca desde 2 letras (DNI, nombres, apellidos)
LocationComboboxfeatures/locations/Busca desde 2 letras; fila «Sin especificar» (campo opcional)
FloatingSelectFieldfeatures/mesa-partes/components/shared/Filtra localmente; conserva su geometría de FloatingInput

No se tocó MultiSelectCombobox/multi-select-checklist (selección múltiple con casillas): es otro patrón. searchPlaceholder de EntityCombobox quedó obsoleto e ignorado (ya no hay buscador dentro del desplegable).

Comportamiento

  • Abrir: clic en el campo, ↓, o empezar a escribir. Al abrir se carga la primera página de opciones (onOpen).
  • Escribir filtra mientras se escribe: en consultas remotas, con ?q= (el llamador hace el retraso y las peticiones); en listas locales (filterLocally), sin distinguir mayúsculas ni tildes («direccion» encuentra «Dirección»; normalizeSearch).
  • El texto escrito es una búsqueda temporal, no un valor: value/displayLabel (la selección confirmada) solo cambian al elegir una opción (clic o Enter). Escribir, abrir y cerrar sin elegir no cambian lo que se guarda (se comprobó con el payload de «Grabar»: tras escribir «carta» y cerrar, documentTypeId sigue siendo el del tipo elegido).
  • Cerrar sin elegir (Escape, clic fuera, Tab): el campo recupera el nombre de la selección anterior, o queda vacío si no había.
  • Al elegir se muestra el nombre en el campo y se conserva el id (onChange(id)/onValueChange). Con clearable/clearLabel, una fila inicial («Sin selección») la quita (se oculta mientras se escribe).
  • Teclado: ↓/↑ mueven la opción activa (aria-activedescendant), Enter elige la activa (solo si hay lista abierta; si no, el Enter sigue su curso), Escape cierra la lista sin cerrar el diálogo que la contiene, Tab cierra sin elegir. ←, →, Inicio, Fin, Supr, copiar/pegar y demás conservan su función normal de edición (verificado: ← mueve el cursor sin cambiar la opción activa).
  • Reemplazo al escribir: al enfocar o abrir con el clic el nombre actual queda seleccionado; lo primero que se teclea lo reemplaza, y si se teclea al final de un nombre ya elegido, solo cuenta lo nuevo como búsqueda.
  • Estados de la lista: «Buscando…», aviso previo («Escribe al menos 2 letras…» en Ciudadano/Ubicación) y «Sin resultados.» Sin buscador adicional dentro del desplegable.
  • Posicionamiento: Popover de Radix anclado al campo (PopoverAnchor), ancho del campo (mín. 14 rem), collisionPadding de 8 px, alto limitado al espacio disponible; el foco no sale del campo al abrir ni al cerrar (elegir con el mouse no le quita el foco: onMouseDown evita el blur).
  • Estilo: subrayado con etiqueta flotante del DS (.sgd-underline-input; la etiqueta queda siempre arriba y el texto de ayuda/valor debajo, así no se superpone al escribir ni al seleccionar) o campo con borde; chevron a la derecha reservado con padding-right.
  • Accesibilidad: role="combobox", aria-autocomplete="list", aria-expanded, aria-controls (lista role="listbox"), opciones role="option" con aria-selected, nombre por aria-labelledby (etiqueta) o aria-label, error con aria-invalid/aria-describedby.

API de SearchableCombobox

value, displayLabel (selección confirmada) · options ({ value, label, description? }), loading, hint, emptyMessage · filterLocally · onOpen, onQueryChange, onSelect(option | null) · clearLabel · placeholder, disabled · inputClassName, className, inputProps, trailing, chevron. Quien lo usa pone el estilo (subrayado, cuadro, etc.) y, si hay, la etiqueta y el error.

Verificación (2026-10-08, Chrome real)

«Nuevo documento» con la API simulada y FloatingSelectField en una página temporal (ya retirada): abrir con clic; sin buscador extra dentro de la lista (0 input); filtrado «resolucion» → «Resolución Directoral»; Enter; «Sin resultados.»; Escape y Tab restauran la selección anterior; ↓↓+Enter; ← sin cambiar la opción; selección con mouse; payload de «Grabar» con el tipo confirmado tras escribir sin elegir; Dependencia → Personal (users?officeId=o2) y filtro local con fila «sin selección»; axe sin violaciones con la lista abierta.

No verificado: CitizenCombobox y LocationCombobox (no hay backend/sesión: solo se comprobó que compilan, pasan lint y reutilizan la misma base); lector de pantalla; el desplegable dentro de diálogos reales (Escape cierra solo la lista por la pila de capas de Radix, no se probó en un diálogo); Firefox/Safari; el tooltip de texto recortado que tenía el selector con etiqueta se retiró (el campo muestra el texto completo al enfocarlo).