From cf8bb00a40cdfe19e207c6ef80b1e536ea8cccb8 Mon Sep 17 00:00:00 2001 From: Natxo <1172351+natxocc@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:12:13 +0200 Subject: [PATCH] Implement Autocomplete component with options and navigation --- src/components/Autocomplete.js | 95 ++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/components/Autocomplete.js diff --git a/src/components/Autocomplete.js b/src/components/Autocomplete.js new file mode 100644 index 0000000..cbe33ba --- /dev/null +++ b/src/components/Autocomplete.js @@ -0,0 +1,95 @@ +import { $, $html, $for } from "sigpro"; +import { val } from "../core/utils.js"; +import { tt } from "../core/i18n.js"; +import { Input } from "./Input.js"; // Importamos el componente hermano + +/** AUTOCOMPLETE */ +export const Autocomplete = (props) => { + const { options = [], value, onSelect, label, placeholder, ...rest } = props; + + const query = $(val(value) || ""); + const isOpen = $(false); + const cursor = $(-1); + + const list = $(() => { + const q = query().toLowerCase(); + const data = val(options) || []; + return q + ? data.filter((o) => (typeof o === "string" ? o : o.label).toLowerCase().includes(q)) + : data; + }); + + const pick = (opt) => { + const valStr = typeof opt === "string" ? opt : opt.value; + const labelStr = typeof opt === "string" ? opt : opt.label; + + query(labelStr); + if (typeof value === "function") value(valStr); + onSelect?.(opt); + + isOpen(false); + cursor(-1); + }; + + const nav = (e) => { + const items = list(); + if (e.key === "ArrowDown") { + e.preventDefault(); + isOpen(true); + cursor(Math.min(cursor() + 1, items.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + cursor(Math.max(cursor() - 1, 0)); + } else if (e.key === "Enter" && cursor() >= 0) { + e.preventDefault(); + pick(items[cursor()]); + } else if (e.key === "Escape") { + isOpen(false); + } + }; + + return $html("div", { class: "relative w-full" }, [ + Input({ + label, + placeholder: placeholder || tt("search")(), + value: query, + onfocus: () => isOpen(true), + onblur: () => setTimeout(() => isOpen(false), 150), + onkeydown: nav, + oninput: (e) => { + const v = e.target.value; + query(v); + if (typeof value === "function") value(v); + isOpen(true); + cursor(-1); + }, + ...rest, + }), + $html( + "ul", + { + class: "absolute left-0 w-full menu bg-base-100 rounded-box mt-1 p-2 shadow-xl max-h-60 overflow-y-auto border border-base-300 z-50", + style: () => (isOpen() && list().length ? "display:block" : "display:none"), + }, + [ + $for( + list, + (opt, i) => + $html("li", {}, [ + $html( + "a", + { + class: () => `block w-full ${cursor() === i ? "active bg-primary text-primary-content" : ""}`, + onclick: () => pick(opt), + onmouseenter: () => cursor(i), + }, + typeof opt === "string" ? opt : opt.label, + ), + ]), + (opt, i) => (typeof opt === "string" ? opt : opt.value) + i, + ), + () => (list().length ? null : $html("li", { class: "p-2 text-center opacity-50" }, "No results")), + ], + ), + ]); +};