Created
March 10, 2025 07:35
-
-
Save AshKyd/7bf21daff5fb8795db74640eca79d546 to your computer and use it in GitHub Desktop.
Svelte native-ish typeahead
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts"> | |
import { untrack } from 'svelte'; | |
let { values = [], value = [], disabled, onChange = () => {} } = $props(); | |
const uniqueId = 'list' + (Math.random() * 10e15).toString(16); | |
let selectedValues = $state<string[]>(value); | |
let isFocused = $state(false); | |
let inputElement = $state<HTMLInputElement>(); | |
let inputValue = $state(''); | |
function getValueFromLabel(label) { | |
const foundValue = values.find(value => value.label === label); | |
return foundValue; | |
} | |
$effect(() => { | |
const foundValue = getValueFromLabel(inputValue); | |
if (!foundValue) { | |
return; | |
} | |
untrack(() => { | |
selectedValues = Array.from(new Set([...selectedValues, foundValue.value])); | |
inputValue = ''; | |
onChange(selectedValues); | |
}); | |
}); | |
$effect(() => { | |
selectedValues = value; | |
}); | |
let displayValues = $derived.by(() => { | |
return selectedValues.map(value => values.find(thisValue => thisValue.value === value)?.label); | |
}); | |
</script> | |
<!-- this interaction is supplementary to the input element, so it's not an accessibility issue --> | |
<!-- svelte-ignore a11y_click_events_have_key_events --> | |
<!-- svelte-ignore a11y_no_static_element_interactions --> | |
<div | |
class="typeahead" | |
class:typeahead--focused={isFocused} | |
class:typeahead--disabled={disabled} | |
onclick={e => inputElement?.focus()} | |
> | |
<input | |
{disabled} | |
bind:this={inputElement} | |
bind:value={inputValue} | |
list={uniqueId} | |
onfocus={() => { | |
isFocused = true; | |
}} | |
onblur={() => { | |
isFocused = false; | |
}} | |
/> | |
<ul class="typeahead__selected-items"> | |
{#each displayValues as label} | |
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> | |
<li class="typeahead__selected-item" onclick={e => e.stopPropagation()}> | |
<div class="typeahead__selected-item-text"> | |
{label} | |
</div> | |
<button | |
class="typeahead__selected-item-butt" | |
aria-label={`Remove ${label}`} | |
onclick={e => { | |
e.preventDefault(); | |
const foundValue = getValueFromLabel(label); | |
selectedValues = selectedValues.filter(thisValue => thisValue !== foundValue.value); | |
onChange(selectedValues); | |
}}>x</button | |
> | |
</li> | |
{/each} | |
</ul> | |
</div> | |
<datalist id={uniqueId}> | |
{#each values as value} | |
<option value={value.label}></option> | |
{/each} | |
</datalist> | |
<style lang="scss"> | |
.typeahead { | |
padding: 0.25rem 0.5rem; | |
background: var(--c-white); | |
border: 1px solid var(--c-grey); | |
border-radius: 0.2rem; | |
} | |
.typeahead--focused { | |
outline: 2px solid Highlight; | |
} | |
.typeahead--disabled { | |
background: transparent; | |
cursor: not-allowed; | |
} | |
.typeahead--disabled * { | |
pointer-events: none; | |
} | |
ul, | |
li { | |
list-style: none; | |
margin: 0; | |
padding: 0; | |
} | |
.typeahead input { | |
border: none; | |
outline: none; | |
width: 100%; | |
padding: 0.25rem 0; | |
background: transparent; | |
color: var(--c-black); | |
} | |
.typeahead__selected-items { | |
display: flex; | |
gap: 0.25rem; | |
flex-wrap: wrap; | |
max-height: 6rem; | |
overflow: auto; | |
} | |
.typeahead__selected-item { | |
background: var(--c-white); | |
border: 1px solid var(--c-grey); | |
border-radius: 0.2rem; | |
position: relative; | |
padding: 0.25rem; | |
padding-right: 1.75rem; | |
display: inline-block; | |
font-size: 0.75rem; | |
} | |
.typeahead__selected-item-text { | |
display: inline-flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.typeahead button.typeahead__selected-item-butt { | |
position: absolute; | |
right: 0; | |
top: 0; | |
padding: 0; | |
height: 100%; | |
width: 1.5rem; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
border: none; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment