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/tokens.css"import "@fristil/designsystem/suggestion.css"
defineFsSuggestion()Slik tar du den i bruk
Section titled “Slik tar du den i bruk”- Bergen
- Bodø
- Oslo
- Stavanger
- Tromsø
- Trondheim
Ingen treff
Vi fyller ut resten av adressen for deg.
<!-- Stilarkene siden trenger. Uten byggesteg er det `<link>`, ikke `import`: en nettleser slår ikke opp et pakkenavn. --><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/tokens/tokens.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/components/ramme/suggestion/suggestion.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/components/css/label/label.css"><!-- Eller alt i én fil: https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/dist/fristil.css --><!-- Rollene, id-ene og koblingen setter komponenten. Statuselementet skriver du: det er der antall treff leses opp. --><fs-suggestion id="kommunesok"> <label class="fs-label">Kommune</label> <div class="fs-suggestion__field"> <input class="fs-input" name="kommune" type="text"> <ul class="fs-suggestion__list"> <li>Bergen</li> <li>Bodø</li> </ul> <p class="fs-suggestion__empty" hidden>Ingen treff</p> <span class="fs-sr-only" role="status" aria-live="polite" data-ignore-morph></span> </div></fs-suggestion>
<!-- Én gang for hele appen, i sidemalen. Ikke én gang per komponent. --><script type="module"> import { defineFsSuggestion } from "https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/dist/components/ramme/suggestion/fs-suggestion.js"
defineFsSuggestion()</script>
<script type="module"> document.getElementById("kommunesok").addEventListener("suggestion-select", (evt) => { console.log("valgte", evt.detail.value) })</script>// src/fristil.d.ts, gir <fs-suggestion> typer i JSXimport "@fristil/designsystem/react-jsx"import { fs } from "@fristil/designsystem/react"import { defineFsSuggestion } from "@fristil/designsystem/suggestion"import "@fristil/designsystem/tokens.css"import "@fristil/designsystem/suggestion.css"
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 {...forslag.empty}>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"import "@fristil/designsystem/tokens.css"import "@fristil/designsystem/suggestion.css"
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 {...forslag.empty}>Ingen treff</p> <span {...forslag.status}></span> </div></fs-suggestion>
<script> import { defineFsSuggestion } from "@fristil/designsystem/suggestion" defineFsSuggestion()</script><!-- Datastar selv. Uten den gjør `data-`-attributtene under ingenting. --><script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar.js"></script>
<!-- Stilarkene siden trenger. Uten byggesteg er det `<link>`, ikke `import`: en nettleser slår ikke opp et pakkenavn. --><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/tokens/tokens.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/components/ramme/suggestion/suggestion.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/components/css/label/label.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/components/css/input/input.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/components/css/sr-only/sr-only.css"><!-- Eller alt i én fil: https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/dist/fristil.css -->
<!-- Én gang for hele appen, i sidemalen. Ikke én gang per komponent. --><script type="module"> import { defineFsSuggestion } from "https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/dist/components/ramme/suggestion/fs-suggestion.js"
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">Kommune</label> <div class="fs-suggestion__field"> <input class="fs-input" name="kommune" type="text"> <ul class="fs-suggestion__list"></ul> <span class="fs-sr-only" 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.
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.
Kommer markupen fra en mal uten JavaScript, er strukturen nok, slik «Ren HTML»-oppskriften viser. Komponenten setter rollene, lager id-ene, skriver for på ledeteksten og aria-controls på feltet, og holder lista lukket til brukeren rører feltet. Hver <li> i lista blir et alternativ; en overskrift i lista trenger role="presentation" for å bli latt i fred. Det serveren har skrevet står, og det komponenten fylte inn settes tilbake om en oppdatering tar det. Statuselementet må stå i markupen: det er innhold serveren sender, og uten det leses ikke antall treff opp.
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 |
Rendrer du bare treffene, og filteret ditt er smalere enn komponentens, som startsWith, er det 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 og open avgjør sammen om «Ingen treff» starter synlig, og den vises bare når lista er åpen og count er 0.
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, open }) skriver hidden på den så lenge lista er lukket eller 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.
Før komponenten er registrert
Section titled “Før komponenten er registrert”fs.suggestion() skriver hidden på lista når den er lukket, så markupen er riktig fra første tegning. Er lista skrevet bar, holder fs-suggestion:not(:defined) .fs-suggestion__list i suggestion.css den skjult til defineFsSuggestion() har kjørt.
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(--fs-spacing-2) var(--fs-spacing-3) |
--fs-suggestion-radius | var(--fs-spacing-1) |
--fs-suggestion-layer | 30 |
.fs-suggestion__list { --fs-suggestion-max-height: 24rem;}Tilgjengelighet
Section titled “Tilgjengelighet”Feltet er en role="combobox" med aria-autocomplete="list", og lista en role="listbox" med role="option" i seg. Skriver serveren dem med fs.suggestion(), står de i HTML-en før skriptet kjører. Kommer markupen uten, setter komponenten dem i det den registreres.
- 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 |