Skip to content

feat(autocomplete): implement searchable single-select component#286

Open
Gabo-Dev wants to merge 3 commits into
mainfrom
feat/12-autocomplete
Open

feat(autocomplete): implement searchable single-select component#286
Gabo-Dev wants to merge 3 commits into
mainfrom
feat/12-autocomplete

Conversation

@Gabo-Dev

Copy link
Copy Markdown
Collaborator

Closes #12


Summary

Implementa el componente Autocomplete como átomo del design system.
Permite buscar y seleccionar una opción de una lista con popover,
search input y navegación por teclado completa.


Tabla de cambios

Archivo Tipo Descripción
src/components/atoms/autocomplete/Autocomplete.tsx New Componente presentacional con portal
src/components/atoms/autocomplete/useAutocomplete.ts New Hook con lógica de estado, búsqueda, navegación y ARIA
src/components/atoms/autocomplete/types.ts New Tipos TypeScript y CVAs propios del componente
src/components/atoms/autocomplete/Autocomplete.test.tsx New Suite completa de 24 tests
src/components/atoms/autocomplete/Autocomplete.stories.tsx New 10 stories documentando todos los estados
src/components/atoms/autocomplete/index.ts New Public API exports
src/index.ts Modified Export del nuevo componente
src/styles/theme.css Modified Tokens --spacing-search-input-*

Evidencia de tests/build

Test Files  36 passed (36)
Tests       769 passed (769)

Pre-commit hooks pasan:

  • ✅ biome (lint + format)
  • ✅ harness-skills (skill boundaries verified)
  • ✅ prop-docs (prop default docs verified)
  • ✅ typecheck (TypeScript compilation)
  • ✅ commitlint (conventional commit format)
  • ✅ test (full suite en pre-push)

Resultado de pre-PR component review

Component Audit → PASS

  • 6-file pattern correcto ✅
  • TypeScript estricto, sin any
  • JSDoc en props públicas con @default
  • Tokens del design system, sin valores raw ✅
  • Storybook convenciones seguidas ✅

Visual Review → PASS

  • Estados: base, hover, focus, active, disabled ✅
  • Focus-ring nativo con focus-visible:focus-ring
  • Transiciones con motion-safe: prefix ✅
  • Contraste WCAG AA ✅

Judgment Day (accesibilidad) → APPROVED

Issues CRITICAL encontrados y resueltos:

  1. Empty state sin role="status" aria-live="polite" → fixed
  2. Home/End keys rotas sin opción activa → fixed

Issues WARNING encontrados y resueltos:
3. Tab handler con DOM traversal frágil → robusto con querySelectorAll
4. needsScopedDarkPortal lee ref durante render → movido a useEffect
5. Clases data-[focused=true] duplicadas → eliminadas
6. popoverProps retornado pero nunca usado → eliminado
7. Trigger sin nombre accesible sin label → dev warning agregado

Mejoras menores adicionales:

  • ARIA structure: empty state fuera del listbox
  • Warning message: menciona aria-label además de label/ariaLabel
  • Tests: 4 tests nuevos para Home/End, empty state ARIA, dev warning, dark portal

Notas de accesibilidad

ARIA pattern

  • Trigger: button con aria-haspopup="listbox" + aria-expanded
  • Search input: role="combobox" con aria-controls, aria-activedescendant, aria-autocomplete="list"
  • Listbox: role="listbox" con aria-label
  • Options: role="option" con aria-selected, aria-disabled, data-focused
  • Empty state: role="status" + aria-live="polite"
  • Loading: role="status" + aria-live="polite"

Keyboard navigation

Tecla Comportamiento
ArrowDown Siguiente opción habilitada
ArrowUp Opción anterior / cierra si estamos al inicio
Home Primera opción (funciona sin flecha previa)
End Última opción (funciona sin flecha previa)
Enter Selecciona opción activa o única filtrada
Escape Cierra popover, foco al trigger
Tab Cierra popover, foco al siguiente elemento

Separación de Select

Autocomplete tiene CVAs propios (copiados de Select) para ser
independiente según roadmap v1. Ambos componentes podrán evolucionar
por separado. Select queda pendiente de sus propios fixes de
motion-safe: y dev warning en issue separada.

- Add 6-file structure (component, hook, types, test, stories, index)
- Implement popover-based autocomplete with search input
- Support controlled/uncontrolled modes
- Add clear button, loading state, custom filtering
- Add keyboard navigation (ArrowUp/Down, Home/End, Enter, Escape, Tab)
- Add 24 tests covering hook logic, DOM behavior, and ARIA
- Include 10 Storybook stories documenting all states
- Apply accessibility fixes (live regions, robust Tab handler)
- Add dev warning for missing accessible name
- Copy shared CVAs from Select for component independence
- Add search-input spacing tokens to theme.css

Closes #12
@Gabo-Dev Gabo-Dev requested a review from egdev6 as a code owner June 19, 2026 12:41
@egdev6

egdev6 commented Jun 19, 2026

Copy link
Copy Markdown
Member

Buen trabajo con la estructura del componente y la evidencia de CI. La duplicidad con Select queda fuera de este comentario porque está asumida como decisión consciente. Sí dejaría estos puntos antes de aprobar:

  • useAutocomplete.ts:540-557: cuando el popover está abierto, Enter solo hace preventDefault() si hay opción activa o exactamente un resultado habilitado. En un formulario, presionar Enter con cero o varios resultados puede disparar submit con una selección no resuelta. Conviene bloquear el submit mientras el autocomplete está abierto y seleccionar solo cuando aplique.
  • useAutocomplete.ts:564-580 + Autocomplete.tsx:92-103: el manejo de Tab calcula el siguiente foco desde el trigger. Si existe el clear button, el foco puede caer en “Clear selection” en lugar de salir al siguiente campo. Deberíamos definir que Tab salga del componente completo y saltar trigger/clear/popover.
  • Autocomplete.tsx:122-134: el onMouseDown={(e) => e.preventDefault()} del popover aplica también sobre el input de búsqueda. Eso puede romper comportamiento normal de mouse, como posicionar el caret o seleccionar texto. Mejor limitarlo a targets no editables o usar otra estrategia para conservar foco.
  • types.ts:300-307 + useAutocomplete.ts:648-666: AutocompleteProps hereda todo VariantProps<typeof selectTrigger>, incluyendo status, pero status no se consume como prop pública y puede filtrarse al DOM vía ...rest. Mejor exponer explícitamente solo variant y size, con JSDoc/defaults, y dejar status como estado interno derivado de hint/isInvalid.
  • useAutocomplete.ts:98-99: el hint info usa tone: 'muted'; debería usar el tono semántico info para mantener consistencia visual/accesible.
  • Autocomplete.stories.tsx:273-280: SingleMatchEnter duplica Default y no demuestra realmente el flujo de Enter con un único resultado. Lo quitaría o lo convertiría en un escenario que deje visible ese caso.

Además, el PR tiene +2025 líneas, por encima del presupuesto de review de 400 líneas. Si no se va a partir, dejemos explícita una excepción de maintainer para este caso.

@egdev6

egdev6 commented Jun 19, 2026

Copy link
Copy Markdown
Member

Actualizo la revisión sobre el commit nuevo 3f340bd. Los puntos del comentario anterior sobre Enter, Tab con clear button, mousedown del popover, status público, tono info y la story duplicada ya quedaron resueltos.

Quedan estos puntos antes de aprobar:

  • Autocomplete.tsx:129-139 + useAutocomplete.ts:274-282,512-566,662-680: en isLoading no se renderiza el search input, por lo tanto los handlers de Escape/Tab que viven en searchInputProps.onKeyDown no existen. El foco queda en el trigger, pero triggerProps no tiene fallback de teclado para cerrar. Resultado: Escape no cierra y Tab puede dejar el popover de loading abierto mientras el foco sale. Agregaría onKeyDown en trigger/container cuando isLoading esté abierto, con test para Escape y Tab.
  • useAutocomplete.ts:683-695 + Autocomplete.tsx:142-153: el search input siempre declara aria-controls="${id}-listbox", pero en empty state no se renderiza ningún elemento con ese id. Hay que renderizar un listbox vacío/contenedor con ese id, o no declarar aria-controls cuando no exista el listbox. Agregaría test que valide que el IDREF existe.
  • Falta una regresión de formulario para Enter: ahora el código hace preventDefault(), pero los tests no cubren que Enter con el popover abierto no dispare submit del <form> cuando no hay selección resuelta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ATOMS] Autocomplete

2 participants