Gå til innholdet

Dialog

Klassen settes på et <dialog>, og resten er nettleserens eget element. Har appen din JavaScript i nettleseren, trenger du ingenting fra Fristil: åpne dialogen med showModal(), lukk den med close().

import "@fristil/designsystem/tokens.css"
import "@fristil/designsystem/dialog.css"

Å åpne den må være et kall. showModal() flytter fokus inn, holder fokus inne i dialogen, lukker på Escape og gjør resten av siden utilgjengelig. Et <dialog open> i markupen gjør ingen av delene, og er bare en boks på siden. Det finnes ingen deklarativ måte å åpne en modal dialog på, verken i Fristil eller i HTML.

Det er et problem bare for én slags app: den som ikke har noe sted å kalle fra, fordi all markup blir til på serveren. Til den finnes <fs-dialog>, lenger ned på siden. Den er valgfri, på samme måte som <fs-field> er valgfri ved siden av fs.field().

Fanen «Prøv den» er dialogen i drift. De fire andre er den samme dialogen skrevet for hvert sitt miljø, og hver av dem er hele oppskriften. De tre første gjør kallet selv; Datastar-utgaven lar <fs-dialog> gjøre det, fordi en server som bare sender HTML ikke har noe sted å kalle fra. Stilarkene er de samme i alle fire, og står i hver fane.

Knappen i «Prøv den» gjør det en server ellers ville gjort: setter open på <fs-dialog>. Det er Datastar-oppskriften, siden den er den eneste som kan vises uten at siden selv kjører koden din.

Å åpne dialogen må være et kall. showModal() flytter fokus inn, holder det der, lukker på Escape og gjør resten av siden utilgjengelig. <dialog open> alene gjør ingen av delene, og er bare en boks på siden. De tre første oppskriftene gjør kallet selv; Datastar-oppskriften lar <fs-dialog> gjøre det, fordi en server som bare sender HTML ikke har noe sted å kalle fra. Velg én av delene: inne i <fs-dialog> er det open på verten som åpner. Et eget showModal()-kall der ser komponenten ikke: den melder ikke fra om det, og lukker dialogen neste gang open eller server-controlled på verten endres, siden verten ikke sier open. En patch som bare rører innholdet lar den stå.

<form method="dialog"> lukker dialogen når en knapp trykkes, og legger knappens value i dialog.returnValue. Da trenger du ingen lyttere på knappene, og Enter virker som forventet.

Escape lukker dialogen av seg selv. Skal du hindre det, for eksempel midt i en lagring, avbryt cancel-hendelsen. Gjør det sjelden: en dialog som ikke kan lukkes er en felle.

Uten showModal() er markupen en boks på siden. Det er tilstanden serveren sender, og den du ser hvis JavaScript aldri kjører.

Dette avsnittet gjelder bare apper uten egen kode i nettleseren. Har du JavaScript der, hopp over det: showModal() er ett kall, og en komponent i mellom gir deg ingenting.

En app i Datastar, htmx, en Go-mal eller en Razor-visning har ikke det stedet. Den sender open på <fs-dialog>, og komponenten gjør kallet.

import { defineFsDialog } from "@fristil/designsystem/dialog"
defineFsDialog()
<fs-dialog open>
<dialog class="fs-dialog" open>
<h2>Vedtaket er registrert</h2>
<div class="fs-dialog__body">
<p>Saken er ferdigbehandlet og sendt til arkiv.</p>
</div>
<form method="dialog" class="fs-dialog__footer">
<button class="fs-button" value="lukk">Lukk</button>
</form>
</dialog>
</fs-dialog>

Inne i <fs-dialog> gir komponenten dialogen navnet sitt fra den første overskriften: aria-labelledby mot overskriftens id, som den lager om den mangler, og klassen fs-dialog__title på overskriften. Har serveren skrevet aria-labelledby eller aria-label, står det. Lages markupen med JavaScript, skriver fs.dialog({ titleId }) det samme.

Gi knappen som lukker dialogen en value. Uten JavaScript er dialogen en boks på siden, og <form method="dialog"> lukker den før noe skript har kjørt. Komponenten kjenner igjen det som alt har skjedd på returnValue, som nettleseren setter til verdien på knappen. Er den tom, ser komponenten en vert som sier «åpen» og en lukket dialog, og åpner den igjen.

Lukker brukeren dialogen, med Escape eller med en knapp i <form method="dialog">, fjerner komponenten open fra <fs-dialog> igjen, slik at markupen sier det samme som skjermen. Et klikk på flaten bak lukker den ikke; det gjør heller ikke en vanlig <dialog>. Komponenten melder fra med hendelsen dialog-toggle, som har detail.open og detail.returnValue, altså verdien på knappen som lukket den. Det er den du skiller «Avbryt» fra «Slett» med.

De to open-ene i eksempelet er ikke det samme:

HvorHvem setter denHva som skjer i en oppdatering
På <fs-dialog>Serveren, når dialogen skal visesServeren bestemmer. Sender den open på nytt, åpnes dialogen igjen
På <dialog>Serveren når dialogen skal vises, og nettleseren i showModal()Komponenten setter det tilbake så lenge dialogen står i topplaget

server-controlled på <fs-dialog> slår av den siste. Da bestemmer hver oppdatering om dialogen vises, også når den står i topplaget.

At verten følger serveren er et valg og ikke en forglemmelse. Sender serveren området på nytt med open fortsatt satt, åpnes dialogen altså igjen; skal en avvisning vare, må appen si fra til serveren, som for all annen tilstand serveren eier.

Attributtet på selve <dialog> er noe annet: nettleseren setter det i showModal(), og serveren sendte det ikke, så en oppdatering river det bort og skjuler dialogen i det øyeblikket den åpnet den. Komponenten setter det tilbake når dialogen fortsatt står i topplaget, så malen din trenger ingenting ekstra.

Serveren skriver det likevel når dialogen skal vises, og fs.dialog({ titleId, open: true }) gir det på begge elementene. Grunnen er at en <dialog> uten open er skjult: uten JavaScript fantes ikke innholdet serveren ville vise, i det hele tatt.

Inne i <fs-dialog> er en slik dialog stylet som en modal allerede før den er det: midt i vinduet, med flaten bak malt av en skygge, siden ::backdrop bare finnes i topplaget. Den har også det samme maksmålet på bredden som nettleseren gir en modal. Da er geometrien den samme i begge tilstandene, og dialogen står stille i det komponenten kaller showModal(), også på en side som bruker tid på å laste. Laget kan settes med --fs-dialog-layer.

Ett forbehold: dialogen er fastposisjonert før den blir modal, mens en ekte modal ligger i topplaget. Står den inne i et element med transform, filter eller contain: layout, er det elementet som blir utgangspunktet for den første, og vinduet for den andre. Da hopper den likevel, og hvor langt kommer an på hvor det elementet står. La dialogen stå utenfor slike elementer.

Utenfor verten gjelder dette ikke. En .fs-dialog med open, uten <fs-dialog> rundt, står der den står i flyten, slik nettleserens egen stil gjør det.

// src/fristil.d.ts, gir <fs-dialog> typer i JSX
import "@fristil/designsystem/react-jsx"
import { fs } from "@fristil/designsystem/react"
const boks = fs.dialog({ titleId: "kvittering-tittel", open: true })
<fs-dialog {...boks.host}>
<dialog {...boks.dialog}>
<h2 {...boks.title}>Vedtaket er registrert</h2>
<div {...boks.body}>Saken er ferdigbehandlet og sendt til arkiv.</div>
<form method="dialog" {...boks.footer}>
<button {...fs.button()} value="lukk">Lukk</button>
</form>
</dialog>
</fs-dialog>

Importen går til /react her, siden eksempelet er JSX. Derfra heter nøklene className og htmlFor. Skriver du markupen i noe annet, importer @fristil/designsystem og få class og for.

fs.dialog.title, fs.dialog.body og fs.dialog.footer gir klassenavnene alene. Se Typesikker bruk.

Lager du markupen med JavaScript og styrer alt i nettleseren, trenger du ingen komponent: kall showModal() selv, som i «Ren HTML»-oppskriften over. Da skal du ikke sende open inn i fs.dialog(). Attributtet er serverens beskjed til <fs-dialog>, og showModal() kaster InvalidStateError på en dialog som alt står åpen.

I React er attributtet dessuten det eneste som stemmer når serveren rendrer dialogen åpen. Komponenten setter open på <dialog> før React hydrerer, så sto det ikke i serverens HTML, meldte React avvik ved hvert eneste oppslag.

Skal dialogen si hvordan det gikk før leseren har lest et ord, får den en topp med farge. fs.dialog({ titleId, color }) gir data-color på dialogen og en header-del som pakker overskriften. Fargene er de samme som varsleren bruker, med brand i tillegg for merkefargen.

colorBetyr
neutralIngen farge. Standard
brandMerkefargen, for en topp som sier hvem som snakker
infoTil orientering
successDet gikk bra
warningSe på dette
dangerNoe er galt, eller kan ikke angres
---
const boks = fs.dialog({ titleId: "resultat-tittel", color: "success" })
---
<dialog {...boks.dialog}>
<div {...boks.header}>
<h2 {...boks.title}>Full pott</h2>
<p {...boks.subtitle}>25 av 25 poeng</p>
</div>
<div {...boks.body}>…</div>
<form method="dialog" {...boks.footer}>…</form>
</dialog>

Toppen er valgfri, og uten den står dialogen som før. Fargen gjelder toppen, så uten header i markupen har den ingenting å farge. En topp uten farge er heller ingen stripe: da slipper den bunnluften, og avstanden ned til teksten er den samme som ellers.

KlasseHva det er
.fs-dialogSelve dialogen. data-color farger toppen
.fs-dialog__headerToppen, med overskriften og en eventuell undertekst i. Valgfri: med den flyttes luften fra dialogen til delene, så et farget bånd går helt ut i kantene
.fs-dialog__titleOverskriften
.fs-dialog__subtitleEn linje under overskriften, inne i toppen
.fs-dialog__bodyInnholdet, som ruller når dialogen blir for høy. Må være et direkte barn av .fs-dialog, ellers ruller dialogen i stedet, og overskriften og knapperaden forsvinner ut av syne sammen med teksten
.fs-dialog__footerRaden med knapper nederst

display er den ene egenskapen du ikke kan sette fritt på .fs-dialog. En <dialog> uten open skjules av nettleserens eget stilark, og en forfatterregel med display slår den uansett lag og spesifisitet, så en lukket dialog blir stående som et kort på siden. Trenger du et annet oppsett, ta med dialog.fs-dialog:not([open]):not(:is(:popover-open)) { display: none } selv, og gjør den minst like spesifikk som din egen regel. Systemet setter display: flex bare når .fs-dialog__body er et direkte barn, og gjør nettopp dette ved siden av.

VariabelStandard
--fs-dialog-widthmin(32rem, 100%, calc(100vw - var(--fs-spacing-8)))
--fs-dialog-paddingvar(--fs-spacing-5)
--fs-dialog-radiusvar(--fs-spacing-2)
--fs-dialog-layer40

--fs-dialog-layer gjelder bare i det korte øyeblikket dialogen er åpen uten å være modal, altså før komponenten har kalt showModal(). Da ligger den ikke i topplaget ennå, og må stables som alt annet. Er dialogen først modal, ligger den over alt uansett.

100% i bredden gjelder når dialogen ikke ligger i topplaget. En <dialog> åpnet med showModal() forholder seg til vindusruten uansett, men lager du en boks på siden av den, som i forhåndsvisningen over, ville 100vw gjort den bredere enn boksen.

Flaten bak dialogen er ::backdrop, og finnes bare når dialogen er åpnet med showModal(). Fargen kommer fra --fs-color-overlay og er kraftigere i mørkt tema, der en sort flate ellers ville vært usynlig.

Se Tilpasning for hvordan variablene og laget virker.

  • Dialogen trenger et navn. fs.dialog({ titleId }) skriver aria-labelledby mot overskriften, og inne i <fs-dialog> setter komponenten det fra den første overskriften når det mangler. Har du ingen overskrift, bruk aria-label.
  • Overskriften skal stille spørsmålet: «Slette søknaden?», ikke «Bekreft».
  • Knappeteksten skal si hva som skjer. «Slett søknaden» er tydeligere enn «OK», særlig for den som hører knappen uten å se overskriften.
  • Etter at dialogen lukkes, gir nettleseren fokus tilbake til knappen som åpnet den. Flytter du fokus selv i mellomtiden, må du sette det tilbake.
  • Ikke bruk en dialog til noe som kunne stått på siden. Den tar over hele skjermen for den som bruker forstørrelse.
  • <fs-dialog> åpner alltid modalt. Et show() ville gitt en boks som ser lik ut, men som ikke fanger fokus og ikke lukker på Escape. Forskjellen er :modal, og den er testet i alle tre nettlesermotorene.