Gå til innholdet

Suggestion

Serveren skriver feltet og lista. Komponenten filtrerer mens brukeren skriver, tar piltastene, og melder antall treff til skjermlesere.

Nettleserens egen <datalist> gjør noe av det samme, men kan ikke styles, oppfører seg ulikt i hver nettleser, og sier ingenting om hvor mange treff det ble.

// main.ts, kjøres én gang når appen starter
import { defineFsSuggestion } from "@fristil/designsystem/suggestion"
import "@fristil/designsystem/suggestion.css"
defineFsSuggestion()
const forslag = fs.suggestion({ id: "kommune", count: kommuner.length, help: true })
<fs-suggestion>
<label class="fs-label" for="kommune">Kommune</label>
<div class="fs-suggestion__field">
<input class="fs-input" id="kommune" name="kommune" type="text" role="combobox"
autocomplete="off" aria-expanded="false" aria-controls="kommune-list"
aria-autocomplete="list" aria-describedby="kommune-help kommune-status">
<ul class="fs-suggestion__list" id="kommune-list" role="listbox" hidden>
<li class="fs-suggestion__option" id="kommune-option-0" role="option"
aria-selected="false">Bergen</li>
<li class="fs-suggestion__option" id="kommune-option-1" role="option"
aria-selected="false">Bodø</li>
</ul>
<p class="fs-suggestion__empty" hidden>Ingen treff</p>
<span class="fs-sr-only" id="kommune-status" role="status" aria-live="polite"
data-ignore-morph></span>
</div>
<p class="fs-help-text" id="kommune-help">Vi fyller ut resten av adressen for deg.</p>
</fs-suggestion>

Feltet er et ekte <input> med name, så det blir med i innsendingen før skriptet har kjørt. Komponenten lagde tidligere hele feltet selv, og da fantes det verken ledetekst eller inndatafelt for en bruker som ventet på JavaScript. Se Markup og oppførsel.

Alternativet kan bære en annen verdi enn teksten sin med data-value:

<li class="fs-suggestion__option" id="kommune-option-0" role="option" data-value="4601">Bergen</li>

Komponenten skjuler et alternativ når teksten ikke inneholder det som står i feltet. Filtrerer du selv, og filteret ditt er et annet, blir de to uenige, og da er det ditt som skal gjelde. Det sier du med prefiltered.

<fs-suggestion prefiltered></fs-suggestion>

Filtrerer du på nøyaktig det samme, altså at teksten inneholder søkeordet, trenger du ingenting: de to er enige, og komponenten skjuler ikke noe du ville vist. Attributtet er for de gangene dere ikke er det:

Ditt filterHva som skjer uten prefiltered
Søk uten diakritikk, bodo finner BodøKomponenten skjuler Bodø, og lista ser tom ut
Søk på en kode som ikke vises, 4601Det samme
Uskarpt søk, oslo finner OsloveienTreffene komponenten ikke kjenner igjen blir borte
Asynkront søkLista blinker mens svaret er underveis

Et smalere filter enn komponentens, som startsWith, er derimot trygt: alt du beholder inneholder også søkeordet, så komponenten skjuler ingenting av det. Da er prefiltered likevel riktig for tommeldingen, som forklart under.

Det gjelder like mye i React som i Datastar. I React er det lett å overse, for der er ingen server involvert:

// Søk uten diakritikk. Komponentens eget filter ville skjult Bodø igjen.
const treff = kommuner.filter((k) => utenTegn(k).includes(utenTegn(sok)))
<fs-suggestion prefiltered>
<ul {...forslag.list}>
{treff.map((navn, i) => <li {...forslag.options[i]} key={navn}>{navn}</li>)}
</ul>
</fs-suggestion>

Bygg da fs.suggestion({ id: "kommune", count: treff.length }), ikke med hele lista: count avgjør om «Ingen treff» starter skjult.

Du gjør det. Tommeldingen er en del av det lista viser, og med prefiltered er det du som bestemmer hva den viser. fs.suggestion({ id, count }) skriver hidden på den når du har treff, så rendrer du feltet på nytt for hvert søk, er det gjort. Komponenten rører den ikke, og det er med vilje: den kan ikke se forskjell på «søket ga ingenting» og «svaret er ikke kommet ennå», og et asynkront søk sa derfor «Ingen treff» i hvert opphold mellom tastetrykket og svaret.

Antall treff leses opp uansett hvem som filtrerte. Det er ikke markup, men en beskjed i [role="status"] til den som ikke ser skjermen, og den kommer når lista faktisk har endret seg, ikke ved tastetrykket. Med prefiltered betyr det: når du har rendret på nytt. Det første antallet leses ikke opp, siden det er utgangspunktet og ikke en endring.

Komponenten tar fortsatt piltastene, markeringen og aria-activedescendant, altså det verken React eller serveren gjør.

prefiltered handler om hva som vises. Skal serveren i tillegg eie om lista er utvidet og hvilket alternativ som er markert, setter du server-controlled ved siden av. Det trenger du sjelden i React: komponenten må få sette aria-selected tilbake, siden en ny rendring nullstiller den.

<fs-suggestion id="kommunesok">
<label class="fs-label" for="kommune">Kommune</label>
<div class="fs-suggestion__field">
<input class="fs-input" id="kommune" name="kommune" type="text" role="combobox"
autocomplete="off" aria-expanded="false" aria-controls="kommune-list"
aria-autocomplete="list">
<ul class="fs-suggestion__list" id="kommune-list" role="listbox" hidden>
<li class="fs-suggestion__option" id="kommune-option-0" role="option">Bergen</li>
<li class="fs-suggestion__option" id="kommune-option-1" role="option">Bodø</li>
</ul>
<p class="fs-suggestion__empty" hidden>Ingen treff</p>
<span class="fs-sr-only" id="kommune-status" role="status" aria-live="polite"
data-ignore-morph></span>
</div>
</fs-suggestion>
<script type="module">
import { defineFsSuggestion } from "@fristil/designsystem/suggestion"
defineFsSuggestion()
document.getElementById("kommunesok").addEventListener("suggestion-select", (evt) => {
console.log("valgte", evt.detail.value)
})
</script>

Feltet er en role="combobox" med aria-autocomplete="list", og lista en role="listbox" med role="option" i seg. Alt står i markupen fra serveren, så semantikken er riktig før skriptet kjører.

  • Fokus blir i feltet. Markeringen flyttes med aria-activedescendant, ikke ved å flytte fokus, slik at brukeren kan fortsette å skrive.
  • Antall treff leses opp. Uten det får den som ikke ser skjermen ingen beskjed om at lista snevret seg inn. Teksten skrives av komponenten, og området har derfor data-ignore-morph.
  • Escape lukker lista uten å tømme feltet, og et klikk utenfor gjør det samme.
TastGjør
Pil nedÅpner lista, eller flytter til neste alternativ
Pil oppÅpner lista på det siste alternativet, eller flytter til forrige
EnterVelger det markerte
EscapeLukker lista
KlasseHva det er
.fs-suggestion__fieldFeltet og lista, som ligger under det
.fs-suggestion__listSelve lista. hidden når den er lukket
.fs-suggestion__optionEtt alternativ. Det markerte har aria-selected="true"
.fs-suggestion__emptyTeksten som vises når ingenting passer
VariabelReserve
--fs-suggestion-max-height18rem
--fs-suggestion-option-paddingvar(--size-2) var(--size-3)
--fs-suggestion-radiusvar(--size-1)
.fs-suggestion__list {
--fs-suggestion-max-height: 24rem;
}