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 starterimport { defineFsSuggestion } from "@fristil/designsystem/suggestion"import "@fristil/designsystem/suggestion.css"
defineFsSuggestion()Kommunesøk
Section titled “Kommunesøk”- Bergen
- Bodø
- Oslo
- Stavanger
- Tromsø
- Trondheim
Ingen treff
Vi fyller ut resten av adressen for deg.
Markupen
Section titled “Markupen”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>Når noen andre filtrerer
Section titled “Når noen andre filtrerer”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 filter | Hva 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, 4601 | Det samme |
Uskarpt søk, oslo finner Osloveien | Treffene komponenten ikke kjenner igjen blir borte |
| Asynkront søk | Lista 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.
Hvem viser «Ingen treff»
Section titled “Hvem viser «Ingen treff»”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.
I en app
Section titled “I en app”<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>import { fs } from "@fristil/designsystem/react"import { defineFsSuggestion } from "@fristil/designsystem/suggestion"
defineFsSuggestion()
const KOMMUNER = ["Bergen", "Bodø", "Oslo", "Tromsø"]
export function Kommunesok() { const forslag = fs.suggestion({ id: "kommune", count: KOMMUNER.length })
return ( <fs-suggestion> <label {...forslag.label}>Kommune</label> <div {...forslag.field}> <input {...forslag.control} name="kommune" /> <ul {...forslag.list}> {forslag.options.map((valg, i) => ( <li key={valg.id} {...valg}>{KOMMUNER[i]}</li> ))} </ul> <p className="fs-suggestion__empty" hidden>Ingen treff</p> <span {...forslag.status} /> </div> </fs-suggestion> )}Her rendres hele lista, og komponenten filtrerer. Filtrerer du i React i stedet, og filteret ditt er et annet enn «teksten inneholder søkeordet», settes prefiltered. Se Når noen andre filtrerer.
---import { fs } from "@fristil/designsystem"
const kommuner = ["Bergen", "Bodø", "Oslo", "Tromsø"]const forslag = fs.suggestion({ id: "kommune", count: kommuner.length })---
<fs-suggestion> <label {...forslag.label}>Kommune</label> <div {...forslag.field}> <input {...forslag.control} name="kommune" /> <ul {...forslag.list}> {forslag.options.map((valg, i) => <li {...valg}>{kommuner[i]}</li>)} </ul> <p class="fs-suggestion__empty" hidden>Ingen treff</p> <span {...forslag.status}></span> </div></fs-suggestion>
<script> import { defineFsSuggestion } from "@fristil/designsystem/suggestion" defineFsSuggestion()</script><fs-suggestion prefiltered data-on:input__debounce.300ms="@get('/kommuner?q=' + evt.target.value)" data-on:suggestion-select="$kommune = evt.detail.value"> <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></ul> <span class="fs-sr-only" id="kommune-status" role="status" aria-live="polite" data-ignore-morph></span> </div></fs-suggestion>Her er det serveren som bestemmer hvilke alternativer som vises, derfor prefiltered. Komponenten kobler seg på alternativene serveren sender inn, så valg med mus virker også etter en oppdatering.
Tilgjengelighet
Section titled “Tilgjengelighet”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.
| Tast | Gjør |
|---|---|
| Pil ned | Åpner lista, eller flytter til neste alternativ |
| Pil opp | Åpner lista på det siste alternativet, eller flytter til forrige |
| Enter | Velger det markerte |
| Escape | Lukker lista |
Klasser du kan style
Section titled “Klasser du kan style”| Klasse | Hva det er |
|---|---|
.fs-suggestion__field | Feltet og lista, som ligger under det |
.fs-suggestion__list | Selve lista. hidden når den er lukket |
.fs-suggestion__option | Ett alternativ. Det markerte har aria-selected="true" |
.fs-suggestion__empty | Teksten som vises når ingenting passer |
| Variabel | Reserve |
|---|---|
--fs-suggestion-max-height | 18rem |
--fs-suggestion-option-padding | var(--size-2) var(--size-3) |
--fs-suggestion-radius | var(--size-1) |
.fs-suggestion__list { --fs-suggestion-max-height: 24rem;}