This commit is contained in:
Philipp Dormann 2025-05-20 00:55:05 +02:00
parent 2c91f46375
commit 50b5e4e455
Signed by: philipp
GPG Key ID: 3BB9ADD52DCA4314
2 changed files with 166 additions and 244 deletions

View File

@ -12,235 +12,99 @@
//
let options = [
"Bulbasaur",
"Ivysaur",
"Venusaur",
"Charmander",
"Charmeleon",
"Charizard",
"Squirtle",
"Wartortle",
"Blastoise",
"Caterpie",
"Metapod",
"Butterfree",
"Weedle",
"Kakuna",
"Beedrill",
"Pidgey",
"Pidgeotto",
"Pidgeot",
"Rattata",
"Raticate",
"Spearow",
"Fearow",
"Ekans",
"Arbok",
"Pikachu",
"Raichu",
"Sandshrew",
"Sandslash",
"Nidoran♀",
"Nidorina",
"Nidoqueen",
"Nidoran♂",
"Nidorino",
"Nidoking",
"Clefairy",
"Clefable",
"Vulpix",
"Ninetales",
"Jigglypuff",
"Wigglytuff",
"Zubat",
"Golbat",
"Oddish",
"Gloom",
"Vileplume",
"Paras",
"Parasect",
"Venonat",
"Venomoth",
"Diglett",
"Dugtrio",
"Meowth",
"Persian",
"Psyduck",
"Golduck",
"Mankey",
"Primeape",
"Growlithe",
"Arcanine",
"Poliwag",
"Poliwhirl",
"Poliwrath",
"Abra",
"Kadabra",
"Alakazam",
"Machop",
"Machoke",
"Machamp",
"Bellsprout",
"Weepinbell",
"Victreebel",
"Tentacool",
"Tentacruel",
"Geodude",
"Graveler",
"Golem",
"Ponyta",
"Rapidash",
"Slowpoke",
"Slowbro",
"Magnemite",
"Magneton",
"Farfetch'd",
"Doduo",
"Dodrio",
"Seel",
"Dewgong",
"Grimer",
"Muk",
"Shellder",
"Cloyster",
"Gastly",
"Haunter",
"Gengar",
"Onix",
"Drowzee",
"Hypno",
"Krabby",
"Kingler",
"Voltorb",
"Electrode",
"Exeggcute",
"Exeggutor",
"Cubone",
"Marowak",
"Hitmonlee",
"Hitmonchan",
"Lickitung",
"Koffing",
"Weezing",
"Rhyhorn",
"Rhydon",
"Chansey",
"Tangela",
"Kangaskhan",
"Horsea",
"Seadra",
"Goldeen",
"Seaking",
"Staryu",
"Starmie",
"Mr. Mime",
"Scyther",
"Jynx",
"Electabuzz",
"Magmar",
"Pinsir",
"Tauros",
"Magikarp",
"Gyarados",
"Lapras",
"Ditto",
"Eevee",
"Vaporeon",
"Jolteon",
"Flareon",
"Porygon",
"Omanyte",
"Omastar",
"Kabuto",
"Kabutops",
"Aerodactyl",
"Snorlax",
"Articuno",
"Zapdos",
"Moltres",
"Dratini",
"Dragonair",
"Dragonite",
"Mewtwo",
"Mew",
"Chikorita",
"Bayleef",
"Meganium",
"Cyndaquil",
"Quilava",
"Typhlosion",
"Totodile",
"Croconaw",
"Feraligatr",
"Sentret",
"Furret",
"Hoothoot",
"Noctowl",
"Ledyba",
"Ledian",
"Spinarak",
"Ariados",
"Crobat",
"Chinchou",
"Lanturn",
"Pichu",
"Cleffa",
"Igglybuff",
"Togepi",
"Togetic",
"Natu",
"Xatu",
"Mareep",
"Flaaffy",
"Ampharos",
"Bellossom",
"Marill",
"Azumarill",
"Sudowoodo",
"Politoed",
"Hoppip",
"Skiploom",
"Jumpluff",
"Aipom",
"Sunkern",
"Sunflora",
"Yanma",
"Wooper",
"Quagsire",
"Espeon",
"Umbreon",
"Murkrow",
"Slowking",
"Misdreavus",
"Unown",
"Wobbuffet",
"Girafarig",
"Pineco",
"Forretress",
"Dunsparce",
"Gligar",
"Steelix",
"Snubbull",
"Granbull",
"Qwilfish",
"Scizor",
"Shuckle",
"Heracross",
"Sneasel",
"Teddiursa",
"Ursaring",
"Slugma",
"Magcargo",
"Swinub",
"Piloswine",
"Corsola",
"Remoraid",
"Octillery",
"Delibird",
"Mantine",
"Skarmory",
"Houndour",
"Houndoom",
{ label: "Bulbasaur", value: { id: 456 } },
{ label: "Ivysaur", value: { id: 456 } },
{ label: "Venusaur", value: { id: 456 } },
{ label: "Charmander", value: { id: 456 } },
{ label: "Charmeleon", value: { id: 456 } },
{ label: "Charizard", value: { id: 456 } },
{ label: "Squirtle", value: { id: 456 } },
{ label: "Wartortle", value: { id: 456 } },
{ label: "Blastoise", value: { id: 456 } },
{ label: "Caterpie", value: { id: 456 } },
{ label: "Metapod", value: { id: 456 } },
{ label: "Butterfree", value: { id: 456 } },
{ label: "Weedle", value: { id: 456 } },
{ label: "Kakuna", value: { id: 456 } },
{ label: "Beedrill", value: { id: 456 } },
{ label: "Pidgey", value: { id: 456 } },
{ label: "Pidgeotto", value: { id: 456 } },
{ label: "Pidgeot", value: { id: 456 } },
{ label: "Rattata", value: { id: 456 } },
{ label: "Raticate", value: { id: 456 } },
{ label: "Spearow", value: { id: 456 } },
{ label: "Fearow", value: { id: 456 } },
{ label: "Ekans", value: { id: 456 } },
{ label: "Arbok", value: { id: 456 } },
{ label: "Pikachu", value: { id: 456 } },
{ label: "Raichu", value: { id: 456 } },
{ label: "Sandshrew", value: { id: 456 } },
{ label: "Sandslash", value: { id: 456 } },
{ label: "Nidoran♀", value: { id: 456 } },
{ label: "Nidorina", value: { id: 456 } },
{ label: "Nidoqueen", value: { id: 456 } },
{ label: "Nidoran♂", value: { id: 456 } },
{ label: "Nidorino", value: { id: 456 } },
{ label: "Nidoking", value: { id: 456 } },
{ label: "Clefairy", value: { id: 456 } },
{ label: "Clefable", value: { id: 456 } },
{ label: "Vulpix", value: { id: 456 } },
{ label: "Ninetales", value: { id: 456 } },
{ label: "Jigglypuff", value: { id: 456 } },
{ label: "Wigglytuff", value: { id: 456 } },
{ label: "Zubat", value: { id: 456 } },
{ label: "Golbat", value: { id: 456 } },
{ label: "Oddish", value: { id: 456 } },
{ label: "Gloom", value: { id: 456 } },
{ label: "Vileplume", value: { id: 456 } },
{ label: "Paras", value: { id: 456 } },
{ label: "Parasect", value: { id: 456 } },
{ label: "Venonat", value: { id: 456 } },
{ label: "Venomoth", value: { id: 456 } },
{ label: "Diglett", value: { id: 456 } },
{ label: "Dugtrio", value: { id: 456 } },
{ label: "Meowth", value: { id: 456 } },
{ label: "Persian", value: { id: 456 } },
{ label: "Psyduck", value: { id: 456 } },
{ label: "Golduck", value: { id: 456 } },
{ label: "Mankey", value: { id: 456 } },
{ label: "Primeape", value: { id: 456 } },
{ label: "Growlithe", value: { id: 456 } },
{ label: "Arcanine", value: { id: 456 } },
{ label: "Poliwag", value: { id: 456 } },
{ label: "Poliwhirl", value: { id: 456 } },
{ label: "Poliwrath", value: { id: 456 } },
{ label: "Abra", value: { id: 456 } },
{ label: "Kadabra", value: { id: 456 } },
{ label: "Alakazam", value: { id: 456 } },
{ label: "Machop", value: { id: 456 } },
{ label: "Machoke", value: { id: 456 } },
{ label: "Machamp", value: { id: 456 } },
{ label: "Bellsprout", value: { id: 456 } },
{ label: "Weepinbell", value: { id: 456 } },
{ label: "Victreebel", value: { id: 456 } },
{ label: "Tentacool", value: { id: 456 } },
{ label: "Tentacruel", value: { id: 456 } },
{ label: "Geodude", value: { id: 456 } },
{ label: "Graveler", value: { id: 456 } },
{ label: "Golem", value: { id: 456 } },
{ label: "Ponyta", value: { id: 456 } },
{ label: "Rapidash", value: { id: 456 } },
{ label: "Slowpoke", value: { id: 456 } },
{ label: "Slowbro", value: { id: 456 } },
{ label: "Magnemite", value: { id: 456 } },
{ label: "Magneton", value: { id: 456 } },
{ label: "Farfetch'd", value: { id: 456 } },
{ label: "Doduo", value: { id: 456 } },
{ label: "Dodrio", value: { id: 456 } },
{ label: "Seel", value: { id: 456 } },
{ label: "Dewgong", value: { id: 456 } },
{ label: "Grimer", value: { id: 456 } },
{ label: "Muk", value: { id: 456 } },
{ label: "Shellder", value: { id: 456 } },
{ label: "Cloyster", value: { id: 456 } },
{ label: "Gastly", value: { id: 456 } },
{ label: "Haunter", value: { id: 456 } },
];
let selectedOption;
@ -351,14 +215,14 @@
{options}
filterFn={(option, searchTerm) => {
// Example: Match if option starts with search term (case-insensitive)
return option.toLowerCase().startsWith(searchTerm.toLowerCase());
return option.label.toLowerCase().startsWith(searchTerm.toLowerCase());
}}
bind:selected={selectedOption}
placeholder="Search fruits..."
on:select={handleSelect}
/>
{#if selectedOption}
<p class="mt-4 text-lg">Selected: {selectedOption}</p>
<p class="mt-4 text-lg">Selected: {JSON.stringify(selectedOption)}</p>
{/if}
<!-- -->

View File

@ -4,7 +4,11 @@
// Props
export let options = [];
export let selected = null;
export let placeholder = "Search options...";
export let inputPlaceholder = "Search options...";
export let noOptionsText = "No options found";
export let inputAriaLabel = "Search and select an option";
export let toggleAriaLabel = "Toggle dropdown";
export let clearAriaLabel = "Clear selection";
export let filterFn = null; // Custom filter function
// Internal state
@ -26,7 +30,7 @@
? filterFn
? options.filter((option) => filterFn(option, searchTerm))
: options.filter((option) =>
option.toLowerCase().includes(searchTerm.toLowerCase())
option.label.toLowerCase().includes(searchTerm.toLowerCase())
)
: options;
// Reset scroll and focus when filtered options change
@ -63,11 +67,19 @@
// Handle option selection
function selectOption(option) {
selected = option;
selected = option.value;
isOpen = false;
searchTerm = "";
focusedIndex = -1;
dispatch("onSelected", option);
dispatch("onSelected", option.value);
}
// Handle clear selection
function clearSelection() {
selected = null;
searchTerm = "";
focusedIndex = -1;
dispatch("onSelected", null);
}
// Toggle dropdown
@ -147,6 +159,13 @@
return () => resizeObserver.disconnect();
}
});
// Get display text for the input
function getDisplayText() {
if (!selected) return inputPlaceholder;
const selectedOption = options.find((option) => option.value === selected);
return selectedOption ? selectedOption.label : inputPlaceholder;
}
</script>
<svelte:window on:click={handleClickOutside} />
@ -154,14 +173,14 @@
<div class="select-container relative w-full">
<!-- Select element with inline search -->
<div
class="border rounded-md px-3 py-2 bg-white shadow-sm flex items-center"
class="border rounded-md px-3 py-2 bg-white shadow-sm flex items-center gap-2"
role="combobox"
aria-expanded={isOpen}
>
<input
type="text"
bind:value={searchTerm}
placeholder={selected || placeholder}
placeholder={getDisplayText()}
class="w-full bg-transparent focus:outline-none {selected
? 'text-black'
: 'text-gray-700'}"
@ -173,8 +192,42 @@
handleKeydown(e, focusedIndex);
}
}}
aria-label="Search and select an option"
aria-label={inputAriaLabel}
/>
{#if selected}
<button
type="button"
class="w-5 h-5 flex items-center justify-center text-gray-500 hover:text-gray-700"
on:click={clearSelection}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
clearSelection();
} else if (e.key === "Escape") {
e.preventDefault();
isOpen = false;
focusedIndex = -1;
}
}}
role="button"
tabindex="0"
aria-label={clearAriaLabel}
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/if}
<svg
class="w-4 h-4 text-gray-500 transform {isOpen ? 'rotate-180' : ''}"
fill="none"
@ -186,11 +239,12 @@
on:keydown={(e) => {
if (e.key === "Enter") toggleDropdown();
else if (e.key === "Escape") {
e.preventDefault();
isOpen = false;
focusedIndex = -1;
}
}}
aria-label="Toggle dropdown"
aria-label={toggleAriaLabel}
>
<path
stroke-linecap="round"
@ -213,10 +267,10 @@
<!-- Virtualized list container -->
<div style="height: {filteredOptions.length * itemHeight}px;">
<div style="transform: translateY({startIndex * itemHeight}px);">
{#each visibleItems as item, i (item + "-" + (startIndex + i))}
{#each visibleItems as item, i (item.label + "-" + (startIndex + i))}
<div
class="px-3 py-2 hover:bg-blue-100 cursor-pointer {selected ===
item
item.value
? 'bg-blue-50'
: ''} {focusedIndex === startIndex + i
? 'bg-blue-200 outline outline-2 outline-blue-500'
@ -225,15 +279,15 @@
on:keydown={(e) => handleKeydown(e, startIndex + i)}
role="option"
tabindex="0"
aria-selected={selected === item}
aria-selected={selected === item.value}
>
{item}
{item.label}
</div>
{/each}
</div>
</div>
{:else}
<div class="px-3 py-2 text-gray-500">No options found</div>
<div class="px-3 py-2 text-gray-500">{noOptionsText}</div>
{/if}
</div>
{/if}
@ -248,4 +302,8 @@
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
:global([role="button"]:focus) {
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
</style>