Lomakkeet

Käsitellään HTML-lomakkeiden luomista, lomakkeiden ohjelmallista käsittelyä ja lomakekenttien syötteiden rajoitteita eli tarkistuksia. Jos lomakkeet eivät ole tuttuja, katso ensin WWW-lomakkeet-materiaali ja Basic native form controls.

Luentovideo: HTML-lomakkeet. Jos video ei näy, katso se suoraan YouTubesta.

Esimerkki 1

Huomio XHTML:stä

Lomakkeissa on paljon totuusarvoattribuutteja. HTML-koodissa riittää pelkkä attribuutin nimi, mutta XML:ssä ei ole totuusarvoattribuutteja, joten kurssin XHTML-sivuilla ne kirjoitetaan muodossa nimi="nimi":

<input type="text" name="nimi" required="required" />
<input type="checkbox" name="ehdot" checked="checked" />
<select name="maat" multiple="multiple"></select>
<form novalidate="novalidate"></form>

Tämä koskee attribuutteja required, checked, disabled, readonly, multiple, selected, autofocus, novalidate ja formnovalidate. Muista myös sulkea tyhjät elementit: <input ... />, ja ei-tyhjät elementit kokonaan: <option value="1"></option>.

form

Lomake-elementtejä voi käyttää myös ilman form-elementtiä, mutta yleensä ne on syytä sijoittaa sen sisään. Ilman lomaketta et saa käyttöösi submit-tapahtumaa, automaattista validointia, FormData-objektia etkä form.elements-kokoelmaa.

form-elementin html-attribuutit

Lomake-elementit

Yleisimmät lomakkeissa käytetyt elementit on käsitelty ITKP1011-kurssin WWW-lomakkeet-luennolla. Tässä kerrataan tämän kurssin kannalta olennaiset.

label on pakollinen

Jokaisella lomakekentällä on oltava label, jossa on selittävä teksti. Ilman sitä ruudunlukija ei kerro käyttäjälle mitä kenttään pitäisi kirjoittaa, eikä kentän klikkausalue laajene tekstiin.

<!-- kääritty muoto: label sisältää sekä tekstin että kentän -->
<label>Nimi <input type="text" name="nimi" /></label>

<!-- for- ja id-attribuuteilla yhdistetty muoto -->
<label for="nimi">Nimi</label>
<input type="text" name="nimi" id="nimi" />

placeholder ei korvaa labelia. Se katoaa heti kun käyttäjä alkaa kirjoittaa, sen kontrasti on selaimissa heikko, eikä se kelpaa kentän nimeksi. Käytä sitä korkeintaan esimerkkinä muodosta (placeholder="esim. 040 1234567").

autofocus-attribuuttia kannattaa välttää. Se siirtää kohdistuksen sivun latauduttua, mikä hämmentää ruudunlukijan käyttäjää ja voi vierittää sivua odottamattomasti.

input-elementin tyypit

input-elementin tärkein attribuutti on type, joka määrää millainen kenttä on kyseessä. Oikean tyypin valinta on tärkeää: selain antaa tyypin mukaisen näppäimistön mobiililaitteessa ja tekee tyyppikohtaisen validoinnin ilmaiseksi.

input-elementin tärkeimmät attribuutit

Huomaa ero readonly- ja disabled-attribuuttien välillä. Molemmat estävät muokkaamisen, mutta vain readonly-kentän arvo lähtee lomakkeen mukana. disabled-kenttiä ei myöskään validoida.

Dynaaminen lomake, joka lisää ja poistaa syöttökenttiä tarpeen mukaan:

datalist

Sisältää option-elementtejä. Datalistiin viitataan input-elementin list-attribuutilla, jolloin sen sisältöä ehdotetaan kentän arvoksi. Toisin kuin select-elementissä, käyttäjä voi kirjoittaa myös jotain muuta — jos vain listan arvot kelpaavat, käytä select-elementtiä.

<label>Luku
  <input name="luvut" type="text" value="30" list="lukuja"
         pattern="10|20|30|40" />
</label>
<datalist id="lukuja">
  <option value="10"></option>
  <option value="20"></option>
  <option value="30"></option>
  <option value="40"></option>
</datalist>

meter

Havainnollistaa mittalukemaa tunnetulla asteikolla: levytilan käyttöä, äänenvoimakkuutta, uunin lämpötilaa. Arvo voi liikkua kumpaankin suuntaan.

Lämmitä uuni <meter min="100" max="250" value="200">200 asteeseen</meter>

progress

Esittää tehtävän etenemistä, joka kulkee alusta loppuun. Ero meter-elementtiin on juuri tässä: latauspalkki on progress, levytilamittari on meter. Ilman value-attribuuttia palkki on määrittelemättömässä tilassa.

<progress value="70" max="100">70 %</progress>
<progress>Ladataan…</progress>

output

Esittää laskennan tai toiminnan tuloksen. for-attribuutti kertoo, mistä kentistä tulos on laskettu. Arvo asetetaan value-ominaisuudella.

<label>Luku 1 <input type="number" name="luku1" id="luku1" value="10" /></label>
<label>Luku 2 <input type="number" name="luku2" id="luku2" value="20" /></label>
<output name="summa" for="luku1 luku2">30</output>
const lomake = document.forms.laskuri;

lomake.addEventListener("input", function () {
  const a = lomake.elements.luku1.valueAsNumber;
  const b = lomake.elements.luku2.valueAsNumber;
  lomake.elements.summa.value = a + b;
});

output-elementillä on aria-live-käyttäytyminen sisään rakennettuna, joten ruudunlukija kertoo muuttuneen arvon automaattisesti.

Luentovideo: HTML5-lomake-elementit. Jos video ei näy, katso se suoraan YouTubesta.

Esimerkki 2

DOM-rajapinta

Luentovideo: lomakkeiden DOM-rajapinta. Jos video ei näy, katso se suoraan YouTubesta.

Lomakkeen ja kenttien hakeminen

Sivun kaikkiin lomakkeisiin pääsee käsiksi document.forms-kokoelman kautta. Se on elävä HTMLCollection, joten se heijastaa sivuun tehdyt muutokset.

document.forms[0]              // sivun ensimmäinen lomake
document.forms.lomake          // lomake, jonka name- tai id-attribuutti on "lomake"
document.forms["lomake"]       // sama asia

Lomakkeen kenttiin pääsee käsiksi lomakkeen kautta, ei suoraan document.forms-kokoelmasta:

<form id="lomake">
  <label>Eka <input name="eka" type="text" value="1" /></label>
  <label>Toka <input name="toka" type="text" value="2" /></label>
</form>
const lomake = document.forms.lomake;

lomake.elements.eka            // input-elementti, selkein muoto
lomake.elements[0]             // sama indeksillä
lomake.eka                     // toimii myös, mutta katso varoitus alla

// document.forms.eka           EI TOIMI: document.forms sisältää vain
//                              form-elementtejä, tulos on undefined

Käytä form.elements-muotoa. Lyhyempi form.eka toimii, mutta se törmää lomakkeen omiin ominaisuuksiin ja metodeihin. Jos kentän nimeksi antaa vaikkapa reportValidity, niin

lomake.reportValidity();       // TypeError: ei ole funktio

— koska nimi viittaakin nyt input-elementtiin eikä metodiin. Älä siis käytä kenttien niminä olemassa olevien ominaisuuksien tai metodien nimiä, ja hae kentät elements-kokoelman kautta.

FormData

Kun koko lomakkeen sisältö halutaan kerralla, FormData on siisteintä. Se kerää kaikki nimetyt kentät, jotka eivät ole disabled-tilassa.

const data = new FormData(lomake);

data.get("nimi");                         // yhden kentän arvo
data.getAll("harrastukset");              // kaikki samannimiset (checkboxit)

for (const [avain, arvo] of data) {
  console.log(`${avain} = ${arvo}`);
}

// koko lomake tavallisena objektina
const arvot = Object.fromEntries(data);   // { nimi: "Oona", ika: "21" }

Huomaa, että kaikki arvot ovat merkkijonoja, myös numerokenttien. Muunna ne Number()-funktiolla tai lue arvo suoraan kentän valueAsNumber-ominaisuudesta. Object.fromEntries hukkaa samannimisten kenttien arvoista kaikki paitsi viimeisen, joten checkbox-ryhmiin käytä getAll-metodia.

Esimerkki

Lomakkeiden rajapintaluokat

HTMLInputElement

Useimmat ominaisuudet viittaavat suoraan vastaaviin HTML-attribuutteihin. Huomaa kirjoitusasu: attribuutti maxlength, ominaisuus maxLength.

Tärkeimpiä metodeja:

radio ja checkbox

Valintanapit ja valintaruudut ovat erikoistapaus input-elementistä. Näistä ominaisuuksista on syytä muistaa:

Valitun valintanapin arvon saa suoraan ryhmän nimellä. Kun useammalla radiolla on sama name, form.elements palauttaa RadioNodeList-kokoelman, jonka value on valitun napin arvo. Silmukkaa ei tarvita:

<fieldset>
  <legend>Koko</legend>
  <label><input type="radio" name="koko" value="S" /> Pieni</label>
  <label><input type="radio" name="koko" value="M" /> Keskikoko</label>
  <label><input type="radio" name="koko" value="L" /> Suuri</label>
</fieldset>
lomake.elements.koko.value;        // "M" — valitun napin arvo
lomake.elements.koko.value = "L";  // valitsee L-vaihtoehdon

// jos mitään ei ole valittu, arvo on tyhjä merkkijono
if (lomake.elements.koko.value === "") {
  console.log("valinta puuttuu");
}

Valintaruuduilla value ei kelpaa sellaisenaan, koska niitä voi olla valittuna useita. Samannimiset kentät saa silti kerralla form.elements-kokoelmasta, ja valitut poimitaan siitä checked-ominaisuuden perusteella:

const ryhma = lomake.elements.harrastukset;   // RadioNodeList

const valitut = [...ryhma]
  .filter(kentta => kentta.checked)
  .map(kentta => kentta.value);

Varo yhden alkion tapausta. Jos kyseisellä nimellä on vain yksi kenttä, form.elements.harrastukset palauttaa kokoelman sijaan itse elementin, eikä sitä voi levittää taulukoksi. Jos määrä voi vaihdella, käytä FormData-objektin getAll-metodia, joka toimii kummassakin tapauksessa:

const valitut = new FormData(lomake).getAll("harrastukset");

Kts. checked.html

HTMLSelectElement

valikko.value;                                   // valitun arvo
valikko.selectedOptions[0].textContent;          // valitun näkyvä teksti

// monivalinnassa value antaa vain ensimmäisen valitun
const kaikki = [...valikko.selectedOptions].map(o => o.value);

Vaihtoehtoja voi lisätä ja poistaa options-kokoelman kautta tai tavallisilla DOM-metodeilla. Poistamiseen on myös remove()-metodi.

const vaihtoehto = document.createElement("option");
vaihtoehto.value = "fi";
vaihtoehto.textContent = "Suomi";
valikko.append(vaihtoehto);

HTMLTextAreaElement

Monirivisen tekstikentän tärkeimmät ominaisuudet:

Metodeja: select(), setSelectionRange(), setRangeText(), focus(), blur(), setCustomValidity(), checkValidity() ja reportValidity().

const kentta = lomake.elements.kuvaus;

kentta.value;              // nykyinen sisältö
kentta.defaultValue;       // alkuperäinen sisältö
kentta.textLength;         // merkkien määrä
kentta.value = "";         // tyhjennys

HTMLFormElement

Tärkeimmät ominaisuudet:

Metodeja:

Ero submit()- ja requestSubmit()-metodien välillä on merkittävä. Jos lomake lähetetään koodista submit()-metodilla, kentät jäävät tarkistamatta eikä oma submit-käsittelijä suoriudu lainkaan — mikä on lähes aina yllätys. Käytä requestSubmit()-metodia.

Tapahtumia: submit ja reset.

Validointi

Luentovideo: lomakkeiden validointi. Jos video ei näy, katso se suoraan YouTubesta.

Käytä aina ensisijaisesti HTML-koodissa määriteltyjä rajoitteita, jos ne riittävät tarkoitukseesi. Selain hoitaa tarkistuksen, virheilmoituksen ja sen kääntämisen käyttäjän kielelle ilmaiseksi.

HTML-rajoitteet

Rajoitteita ovat required, min, max, step, minlength, maxlength, pattern sekä kentän type (esimerkiksi type="email" tarkistaa osoitteen muodon).

pattern on aina ankkuroitu. Lauseke tarkistetaan koko arvoa vasten, ikään kuin sen ympärillä olisi ^ ja $. Tämä poikkeaa tavallisesta RegExp-käytöksestä. Anna kentälle myös title-attribuutti, jonka teksti näkyy selaimen virheilmoituksessa:

<label>Postinumero
  <input type="text" name="postinumero" pattern="[0-9]{5}"
         title="Viisi numeroa, esimerkiksi 40100" required="required" />
</label>

Huom. minlength- ja maxlength-rajoitteisiin liittyvä validiteetti päivittyy vain käyttäjän tekemien muutosten yhteydessä. Jos muutat kentän value-ominaisuutta koodista, se ei vaikuta näihin tarkistuksiin.

ValidityState

Jokaisella kentällä on validity-ominaisuus, josta näkee onko kenttä validi ja mitä rajoitetta se rikkoo. Sen lukeminen ei laukaise mitään tapahtumia, joten se sopii hiljaiseen tarkistukseen.

kentta.validity.valid;             // onko kenttä kelvollinen
kentta.validity.valueMissing;      // pakollinen kenttä on tyhjä
kentta.validity.typeMismatch;      // esim. type="email" ja väärä muoto
kentta.validity.patternMismatch;   // pattern ei täsmää
kentta.validity.rangeUnderflow;    // alle min-arvon
kentta.validity.rangeOverflow;     // yli max-arvon
kentta.validity.tooShort;          // alle minlength
kentta.validity.tooLong;           // yli maxlength
kentta.validity.stepMismatch;      // ei osu step-askeleeseen
kentta.validity.customError;       // setCustomValidity on asetettu

kentta.validationMessage;          // selaimen virheilmoitus tekstinä

Omat tarkistukset

Oman virheilmoituksen asettaa setCustomValidity()-metodi. Jos viesti on muuta kuin tyhjä merkkijono, kenttä on epävalidi. Virheilmoitus on siis muistettava tyhjentää, kun virhettä ei enää ole:

function tarkistaSalasana(e) {
  const kentta = e.currentTarget;

  if (kentta.value.length >= 8 && /[0-9]/.test(kentta.value)) {
    kentta.setCustomValidity("");        // ei virhettä
  }
  else {
    kentta.setCustomValidity("Salasanassa on oltava vähintään 8 merkkiä ja numero.");
  }

  kentta.reportValidity();               // näytä ilmoitus käyttäjälle
}

lomake.elements.salasana.addEventListener("input", tarkistaSalasana);

checkValidity, reportValidity ja invalid

TapaPalauttaaNäyttää ilmoituksenLaukaisee invalid-tapahtuman
validity.validtotuusarvoneiei
checkValidity()totuusarvoneikyllä
reportValidity()totuusarvonkylläkyllä

Molemmat metodit toimivat sekä yksittäiselle kentälle että koko lomakkeelle. Jos haluat vain tietää tilanteen aiheuttamatta mitään, lue validity.valid-ominaisuus.

invalid-tapahtuma laukeaa epävalidille kentälle, kun lomaketta yritetään lähettää tai kun kutsutaan checkValidity()- tai reportValidity()-metodia. invalid ei kupli, joten sitä ei voi delegoida lomakkeelle tavalliseen tapaan — käsittelijä on lisättävä jokaiseen kenttään tai käytettävä capture-vaihetta:

// tämä EI toimi, koska invalid ei kupli
lomake.addEventListener("invalid", kasittelija);

// capture-vaiheessa toimii
lomake.addEventListener("invalid", kasittelija, { capture: true });

Milloin validointi tapahtuu

Jos lomakkeella ei ole novalidate-attribuuttia, kentät validoidaan automaattisesti lomaketta lähetettäessä. Nämä painikkeet lähettävät lomakkeen:

<button>Painike 1</button>
<button type="submit">Painike 2</button>
<input type="submit" value="Painike 3" />

Nämä eivät lähetä lomaketta, vaan niistä laukeaa pelkkä click-tapahtuma:

<button type="button">Pelkkä click-painike</button>
<input type="button" value="Pelkkä click-painike" />

submit on lomakkeen tapahtuma, ei painikkeen. Kuuntelija on siis liitettävä form-elementtiin, ei lähetyspainikkeeseen. Painikkeessa submit-tapahtumaa ei koskaan tapahdu, vaikka sen kuuntelijan sinne lisäisikin:

lomake.addEventListener("submit", omasubmit);      // OIKEIN
nappi.addEventListener("submit", omasubmit);       // ei laukea koskaan

Tämä on myös syy siihen, miksi lomaketta ei kannata käsitellä painikkeen click-tapahtumassa: lomakkeen voi lähettää myös painamalla Enteriä tekstikentässä, jolloin painiketta ei klikata lainkaan, mutta submit-tapahtuma laukeaa normaalisti.

Lähetyspainikkeesta laukeaa ensin click-tapahtuma, ja lomakkeen submit-tapahtuma vasta sen jälkeen jos kaikki kentät ovat valideja. Poikkeuksia on kaksi: submit-tapahtumaa ei tapahdu, jos click-käsittelijässä kutsutaan preventDefault()-metodia, ja se tapahtuu aina, jos lomakkeella on novalidate-attribuutti.

formnovalidate on lähetyspainikkeen attribuutti, ei kentän. Sillä varustettu painike ohittaa koko lomakkeen validoinnin — tyypillinen käyttö on "tallenna luonnos" -painike:

<button type="submit" formnovalidate="formnovalidate">Tallenna luonnos</button>

Yksittäisen kentän validoinnin poistamiseen ei ole attribuuttia. Jos kenttää ei haluta validoida, siltä poistetaan rajoitteet tai se asetetaan disabled-tilaan.

Validointi heti kentän täyttämisen yhteydessä

  1. Tarkista kentän syöte input- tai change-tapahtumassa. input laukeaa jokaisen merkin jälkeen, change vasta kun muokkaus päättyy.
  2. Tee omat javascript-tarkistukset. Rajoitteiden tilan voi lukea validity-ominaisuudesta ilman sivuvaikutuksia.
  3. Aseta oma virheilmoitus setCustomValidity()-metodilla, tai tyhjennä se tyhjällä merkkijonolla jos virhettä ei ole.
  4. Kutsu kentän reportValidity()-metodia, joka näyttää käyttäjälle sekä oman että HTML-rajoitteiden virheilmoitukset.
  5. Muokkaa virheellisten kenttien ulkoasua CSS:llä, katso alla.

Lomake hyväksytään ja submit-tapahtuma sallitaan vasta, kun kaikissa kentissä on validi sisältö.

Koko lomakkeen validointi täyttämisen jälkeen

Kaikkia virheitä ei pysty tarkistamaan heti kentän täyttämisen yhteydessä — esimerkiksi kahden kentän keskinäistä suhdetta ei voi tarkistaa ennen kuin molemmat on täytetty. Tällöin tarkistus tehdään vasta lopuksi.

Tässä on yksi sudenkuoppa: submit-tapahtumassa ei voi poistaa omia virheilmoituksia, koska tapahtuma ei laukea silloin kun lomakkeella on virheitä. Vaihtoehtoja on kaksi:

// lomakkeella on novalidate="novalidate"
lomake.addEventListener("submit", function (e) {
  e.preventDefault();

  const alku = lomake.elements.alkupvm;
  const loppu = lomake.elements.loppupvm;

  if (alku.value !== "" && loppu.value !== "" && alku.value > loppu.value) {
    loppu.setCustomValidity("Loppupäivä ei voi olla ennen alkupäivää.");
  }
  else {
    loppu.setCustomValidity("");
  }

  if (!lomake.reportValidity()) {
    return;     // virheitä: ilmoitukset näytetty, ei jatketa
  }

  kasitteleLomake(new FormData(lomake));
});

Virheellisten kenttien ulkoasu

CSS:ssä on valmiit valitsimet kelvollisille ja kelvottomille kentille. Käytä :user-invalid- ja :user-valid-valitsimia eikä :invalid- ja :valid-valitsimia.

Ero on käytännössä ratkaiseva: :invalid osuu tyhjään pakolliseen kenttään heti sivun latauduttua, ennen kuin käyttäjä on ehtinyt tehdä mitään. Lomake näyttää siis punaiselta ennen kuin siihen on koskettu. :user-invalid osuu vasta kun käyttäjä on ollut kentässä ja poistunut siitä.

input:user-invalid {
  border-color: #b3261e;
  background-color: #fdeceb;
}

input:user-valid {
  border-color: #2e7d4f;
}

/* pakolliset kentät merkitään myös näkyvästi */
label:has(input:required)::after {
  content: " *";
  color: #b3261e;
}

Älä luota pelkkään väriin: värisokea käyttäjä ei erota punaista reunaa vihreästä. Virheilmoituksen teksti on aina näytettävä.

Selainerot

Selaimet esittävät virheilmoitukset eri tavoin, ja käyttäytyminen muuttuu versioittain. Erityisesti oman setCustomValidity()-viestin näkyminen ilman reportValidity()-kutsua vaihtelee selaimittain. Testaa siis aina sekä Firefoxilla että Chromella, äläkä rakenna logiikkaa sen varaan, että ilmoitus näkyisi ilman erillistä reportValidity()-kutsua.

Jos haluat täyden hallinnan ulkoasuun, ota selaimen ilmoitus pois käytöstä novalidate-attribuutilla ja näytä virheet itse sivun omissa elementeissä. Muista silloin liittää virheilmoitus kenttään aria-describedby-attribuutilla.

Esimerkki 1

Esimerkki 2 (lomake.js) esittelee click- ja submit-tapahtumien toimintaa eri tilanteissa. Sama esimerkki interaktiivisena — suorita run-painikkeella:

Saavutettavuus ja ARIA

Saavutettavuuden perusteet käydään läpi ITKP1011-kurssin luennolla Saavutettavuus ja käytettävyys: WCAG:n neljä periaatetta, lainsäädäntö, kontrastit, vaihtoehtoiset tekstit, otsikkotasot, linkkitekstit, hyppylinkki ja sivun testaaminen ilman kuvia, ilman tyylejä, näppäimistöllä ja ruudunlukijalla. Tämä luku ei toista niitä.

Tässä käsitellään sitä, mikä tulee mukaan kun sivu alkaa muuttua javascriptin vaikutuksesta. Staattisen sivun saa saavutettavaksi oikealla merkkauksella, mutta heti kun ohjelma muuttaa sivua, saavutettavuus siirtyy ohjelmoijan vastuulle: fokus ei siirry itsestään, eikä ruudunlukija huomaa muutosta jota se ei ole itse aiheuttanut.

ITKP1011Tämä luku
WCAG:n periaatteet ja lainsäädäntö—
oikeat elementit ja rakenneelementin tila, kun se muuttuu
vaihtoehtoiset tekstit ja kontrastitohjelmallisesti asetetut nimet
ARIAn ensimmäinen sääntömitä ARIA on ja miten se toimii
sarkaimella läpi, ei jumiinfokuksen siirtäminen ohjelmallisesti
staattisen sivun testaaminenmuuttuvan sivun ja ilmoitusten testaaminen

Mikä ARIA on

ARIA eli Accessible Rich Internet Applications on W3C:n määrittely, joka lisää merkkaukseen tietoa elementin roolista, tilasta ja ominaisuuksista. Käytännössä se on joukko role- ja aria--alkuisia attribuutteja.

Selain rakentaa sivusta DOM-puun rinnalle saavutettavuuspuun (accessibility tree). Se on karsittu esitys, joka kertoo apuvälineille mitä sivulla on: tässä on painike, tässä on otsikkotason 2 otsikko, tämä valintaruutu on valittuna. Ruudunlukija ei lue merkkausta vaan tätä puuta.

Saavutettavuuspuu johdetaan merkkauksesta automaattisesti, ja oikeat elementit tuottavat sen oikein ilman eri toimenpiteitä. ARIAa tarvitaan kahdessa tilanteessa.

1. HTML:stä puuttuu elementti. Välilehdille, puunäkymälle, yhdistelmäkentälle tai ilmoituslistalle ei ole omaa elementtiä. Ilman ARIAa niistä tulee saavutettavuuspuussa nimettömiä div-elementtejä.

2. Tila muuttuu ajon aikana. Kun valikko avautuu, rivi valitaan tai lomakkeen lähetys epäonnistuu, muutos näkyy silmällä mutta saavutettavuuspuussa ei mitään tapahtunut. ARIA on tapa kertoa siitä.

Mitä ARIA ei tee

Tämä on tärkeintä ymmärtää, ja se menee usein väärin. ARIA ei muuta elementin toimintaa eikä ulkoasua. Se muuttaa vain sitä, mitä apuvälineelle kerrotaan.

<!-- Tämä EI ole painike. Se näyttää ruudunlukijalle painikkeelta,
     mutta ei saa fokusta eikä reagoi välilyöntiin tai enteriin. -->
<div role="button">Tallenna</div>

Tästä seuraa ohjelmoijalle tärkeä päätelmä: jos otat role-attribuutin käyttöön, joudut toteuttamaan kaiken käyttäytymisen itse — fokusoituvuuden, näppäinkomennot ja tilan ylläpidon. Se on paljon työtä ja helppo tehdä puolittain.

Milloin ARIAa käytetään

ARIAn ensimmäinen sääntö — älä käytä ARIAa, jos tehtävään on olemassa HTML-elementti — on käyty läpi ITKP1011-kurssilla. Sama pätee tässä, mutta tällä kurssilla sivu muuttuu ajon aikana, joten ARIAa tarvitaan enemmän kuin yhteen asiaan.

Käytännön järjestys pysyy silti samana:

  1. oikea HTML-elementti
  2. jos elementtiä ei ole, ARIA täydentämään merkkausta
  3. omat roolit vasta kun molemmat edellä on käyty läpi

ARIA Authoring Practices Guide luettelee valmiit roolit, tilat ja näppäinkomennot tavallisimmille käyttöliittymän osille. Se on tämän aiheen tärkein yksittäinen lähde.

Saavutettavuuspuun katsominen

Saavutettavuuspuu näkyy kehittäjätyökaluissa: Firefoxissa välilehdellä Accessibility ja Chromessa Elements-välilehden Accessibility-paneelissa. Sieltä näkee jokaisen elementin roolin ja nimen sellaisena kuin apuväline sen saa.

Tämä on nopein tapa tarkistaa oma työ: jos elementin nimi on tyhjä tai rooli on generic, jotain on kesken.

Saavutettava nimi

Jokaisella vuorovaikutteisella elementillä on oltava saavutettava nimi (accessible name). Se on se teksti, jonka ruudunlukija lukee elementin kohdalla. Lomakekentän nimi tulee label-elementistä, mutta nimi voi tulla myös muualta.

Nimi lasketaan useasta lähteestä, ja järjestys on määritelty. Ensimmäinen löytyvä voittaa:

JärjestysLähde
1aria-labelledby — viittaus toisen elementin tunnisteeseen
2aria-label — teksti attribuutissa
3elementin oma tapa: label, alt, legend, caption
4elementin sisältö, esimerkiksi painikkeen teksti
5title-attribuutti — viimeinen oljenkorsi

Järjestyksestä seuraa yleinen virhe. aria-label ohittaa elementin oman sisällön, joten seuraavassa ruudunlukija lukee vain sanan "Sulje" — näkyvä teksti katoaa kokonaan:

<!-- Näkyvä teksti ja luettu nimi eroavat. Puheohjausta käyttävä
     sanoo "Peruuta" eikä mitään tapahdu. -->
<button aria-label="Sulje">Peruuta</button>

Sääntö: jos elementillä on näkyvä teksti, se kuuluu nimeen. aria-label on tarkoitettu elementeille, joilla ei ole tekstiä — tyypillisesti kuvakepainikkeille.

// Kuvakepainike: nimi on pakko antaa erikseen.
// Mukaan kuuluu myös se, MITÄ poistetaan - ruudunlukija lukee painikkeet
// myös irrallaan asiayhteydestään.
nappi.setAttribute("aria-label", `Poista ${auto.malli} ${auto.valmistaja}`);

aria-labelledby on usein parempi kuin aria-label, koska se viittaa sivulla jo olevaan tekstiin eikä kahdenna sitä. Huomaa, että viittaus etsitään samasta puusta — tunniste ei löydy shadow DOM:in toiselta puolelta.

Tila ja ominaisuudet

Kun javascript muuttaa elementin tilaa, tila on kerrottava myös ARIAlla. Nämä ovat tavallisimmat.

AttribuuttiMihin
aria-expandedonko painikkeen avaama alue auki — valikot, haitarit
aria-pressedonko painike päällä — tilaa vaihtava painike
aria-currentmikä on nykyinen sivu, vaihe tai rivi
aria-selectedvalinta listassa tai välilehdissä
aria-describedbylisäselitys, esimerkiksi virheilmoitus tai ohjeteksti
aria-invalidonko kentän arvo kelvoton
aria-busyalue on päivittymässä
// Valikkopainike: tila kerrotaan aina kun se vaihtuu
const avaa = function () {
  const auki = valikko.hidden;

  valikko.hidden = !auki;
  nappi.setAttribute("aria-expanded", String(auki));
};

Huomaa String(auki). ARIAn arvot ovat merkkijonoja, ja attribuutin puuttuminen ei tarkoita arvoa false vaan sitä, ettei tilaa ole kerrottu lainkaan. Totuusarvoiset ARIA-attribuutit kirjoitetaan siis näkyviin molemmissa tapauksissa — toisin kuin HTML:n disabled ja required.

aria-hidden ja disabled

aria-hidden="true" poistaa elementin saavutettavuuspuusta mutta jättää sen näkyviin ja fokusoitavaksi. Se on oikea valinta vain koristeille — esimerkiksi kuvakkeelle, jonka merkitys on jo painikkeen tekstissä.

<button type="button">
  <span aria-hidden="true">&times;</span> Sulje
</button>

Älä koskaan aseta sitä fokusoitavalle elementille. Silloin syntyy tila, jossa näppäimistökäyttäjä saa fokuksen elementtiin josta ruudunlukija ei kerro mitään.

Samoin aria-disabled="true" kertoo elementin olevan poissa käytöstä mutta ei estä sen käyttöä — toisin kuin HTML:n disabled. Ero on siinä, että disabled-elementti poistuu myös näppäinkierrosta, joten käyttäjä ei löydä sitä eikä saa selitystä siitä miksi se ei toimi. Siksi aria-disabled on joskus parempi, mutta silloin toiminto on estettävä itse käsittelijässä.

Fokus

Fokus on se elementti, joka vastaanottaa näppäinpainallukset. Se on näppäimistö- ja ruudunlukijakäyttäjälle sama asia kuin kursorin sijainti hiirikäyttäjälle: jos se katoaa tai hyppää väärään paikkaan, käyttäjä on eksyksissä.

Järjestys tulee DOM-puusta

Näppäinkierron järjestys on DOM-puun järjestys, ei se, miten elementit näkyvät ruudulla. Tästä seuraa, että CSS:llä uudelleenjärjestetty sisältö — order-ominaisuus flexissä tai grid-area-sijoittelu — voi tuottaa fokusjärjestyksen, joka hyppii näytöllä sinne tänne.

Korjaus on järjestää DOM-puu oikein, ei säätää tabindex-arvoja.

tabindexVaikutus
ei attribuuttiavuorovaikutteiset elementit ovat kierrossa, muut eivät
0elementti tulee kiertoon DOM-järjestyksessä
-1ei tule kiertoon, mutta voidaan fokusoida ohjelmallisesti
positiivinen lukurikkoo järjestyksen koko sivulla — älä käytä

tabindex="-1" on se, jota ohjelmoija oikeasti tarvitsee: se tekee elementistä fokusoitavan focus-metodilla ilman että se tulee näppäinkiertoon.

:focus-visible

Selain piirtää fokusoidun elementin ympärille reunuksen. Sitä ei saa poistaa — ilman sitä näppäimistökäyttäjä ei näe missä on.

Jos selaimen oma reunus ei sovi ulkoasuun, se korvataan omalla. Käytä :focus-visible-valitsinta: se osuu silloin kun selain arvioi, että reunus kannattaa näyttää — eli näppäimistöllä muttei hiirellä klikatessa.

/* EI näin: poistaa reunuksen kaikilta */
:focus { outline: none; }

/* Näin: oma reunus, joka näkyy kun sitä tarvitaan */
:focus-visible {
  outline: 3px solid;
  outline-offset: 2px;
}

Fokuksen siirtäminen ohjelmallisesti

Fokus on siirrettävä itse aina kun sivun rakenne muuttuu niin, että fokus katoaisi. Tavallisimmat tilanteet:

const poista = function (rivi) {
  // Otetaan talteen mihin fokus siirretään, ENNEN kuin rivi poistetaan
  const seuraava = rivi.nextElementSibling ?? rivi.previousElementSibling;

  rivi.remove();

  if (seuraava !== null) {
    seuraava.querySelector("button").focus();
  }
  else {
    // lista tyhjeni: siirretään fokus otsikkoon, joka ei ole
    // luontaisesti fokusoitava -> tabindex="-1"
    otsikko.focus();
  }
};

Otsikko tarvitsee tabindex="-1"-attribuutin, jotta focus-metodi toimii. Älä anna sille arvoa 0: se lisäisi otsikon näppäinkiertoon, mikä hämmentää.

focus-metodi vierittää elementin näkyviin. Jos sitä ei halua, parametri { preventScroll: true } estää vierityksen.

Poista rivejä sarkaimella ja enterillä. Fokus siirtyy seuraavaan riviin, ja viimeisen jälkeen otsikkoon. Ota valinta pois päältä ja tee sama uudelleen: fokus katoaa dokumentin alkuun. Sivu näyttää koko ajan, missä fokus on. Erillisenä sivuna: fokus.html.

Ilmoitukset ja aria-live-alueet

Kun javascript lisää sivulle tekstiä, ruudunlukija ei kerro siitä mitään. Näkevä käyttäjä huomaa uuden ilmoituksen heti, koska hän näkee koko sivun kerralla. Ruudunlukija lukee sivua kohta kohdalta, joten se ei palaa itsestään taaksepäin katsomaan mitä muualla tapahtui.

Ratkaisu on aria-live-alue: elementti, jonka sisällön muuttumisesta apuväline kertoo automaattisesti. Englanniksi termi on live region.

<p id="ilmoitus" aria-live="polite" class="vain-ruudunlukijalle"></p>
document.getElementById("ilmoitus").textContent = "Lomake tallennettiin";
ArvoMilloin
politelukee kun käyttäjä on tauolla — lähes aina oikea valinta
assertivekeskeyttää käynnissä olevan puheen — vain kiireelliselle

Valmiit roolit tekevät saman lyhyemmin: role="status" vastaa polite-arvoa ja role="alert" assertive-arvoa.

Alueen on oltava sivulla ennen kuin sen sisältö muuttuu. Tämä on aiheen tavallisin virhe: jos alue luodaan javascriptilla ja täytetään heti, apuväline ei ehdi huomata sitä eikä ilmoitusta lueta. Lisää tyhjä alue merkkaukseen ja kirjoita siihen vasta myöhemmin.

Alue piilotetaan näkyvistä niin, että apuväline silti lukee sen — samalla tavalla kuin ITKP1011:n hyppylinkki, eli ei display: none -määrityksellä:

.vain-ruudunlukijalle {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

Älä käytä aria-live-aluetta kaikkeen. Jos ilmoitus on tärkeä, se kannattaa näyttää myös näkyvästi — ja silloin erillistä piilotettua aluetta ei välttämättä tarvita lainkaan.

Valikkopainike päivittää aria-expanded-attribuutin ja suosikkipainike aria-pressed-attribuutin. Tarkista arvot kehittäjätyökalujen saavutettavuuspaneelista. Kahden ilmoituspainikkeen ero kuuluu vain ruudunlukijalla. Erillisenä sivuna: aria.html.

Omat komponentit ja näppäimistö

Kun teet oman käyttöliittymän osan — välilehdet, haitarin, yhdistelmäkentän — joudut toteuttamaan näppäinkomennot itse. Älä keksi niitä: käyttäjät odottavat samoja komentoja kuin muualla.

ARIA Authoring Practices Guide luettelee jokaiselle tavalliselle osalle roolit, tilat ja näppäinkomennot.

Kolme periaatetta, jotka kattavat suurimman osan tapauksista:

Ajastukset ja automaattinen liike

prefers-reduced-motion-kysely on käsitelty animointiluennolla ja raahaamisen vaihtoehdot drag & drop -luennolla. Tähän jää kaksi asiaa, jotka ovat ohjelman ajastuksista kiinni.

Muuttuvan sivun testaaminen

ITKP1011:n testauslista — validointi, sarkaimella läpi, tyylit pois, kuvat pois, tekstin suurentaminen, kontrastit ja ruudunlukijan kokeileminen — pätee sellaisenaan. Se tarkastaa sivun yhdessä tilassa.

Javascript-sovelluksessa tiloja on monta, eikä listan läpikäyminen kerran riitä. Kolme lisätarkistusta kohdistuu nimenomaan muutoksiin:

  1. Tee jokainen toiminto ja katso, mihin fokus menee. Avaa dialogi ja sulje se. Poista rivi. Lähetä lomake virheellisenä. Vaihda näkymää. Joka kerta sama kysymys: onko fokus jossain järkevässä, vai hyppäsikö se dokumentin alkuun?
  2. Tarkista, kertooko jokainen muutos itsestään. Kun jotain ilmestyy, katoaa tai valmistuu, tuleeko siitä ilmoitus — vai näkyykö muutos vain silmällä?
  3. Katso saavutettavuuspuu muutoksen jälkeen. Onko jokaisella painikkeella nimi? Kertooko aria-expanded totuuden avaamisen jälkeen? Jäikö aria-hidden päälle?

Kolmas kohta on se, jonka automaattiset työkalut jättävät kokonaan huomaamatta: ne tarkastavat sivun sellaisena kuin se on latautuessaan, eivät sellaisena kuin se on viiden klikkauksen jälkeen.

Lomakkeiden ulkoasu

Kertaa Asemointi CSS:n avulla.

Päivityslomakkeet

Seuraavassa on neljä eritasoista esimerkkiä, joissa päivitetään yksinkertaista tietorakennetta ja esitetään sivulla tietorakenteen sisältö.

Versio 0

Tässä esimerkissä ongelmana on muutosten päivittäminen suoraan tietorakenteeseen. Mitä jos käyttäjä kirjoittaa jotain väärin? Muutosta ei voi perua, koska se on heti peruuttamaton. Parempi ratkaisu olisi muokata kopiota ja päivittää tietorakenne vasta kun klikataan tallenna-painiketta. Tästä näkee kuitenkin, miten objektien viitteitä voi käyttää: viitteiden avulla tietorakenteen ja DOM-puun käsittely on pidetty minimissä.

muokkaus 0 · muokkaus 0 + live-editori

Käynnistä sovellus run-painikkeella:

Versio 1

Sama kuin edellä, mutta muokkaaminen tehdään kopion kautta. Tietorakenne päivitetään vasta kun on painettu tallenna-painiketta.

muokkaus 1 + live-editori

Käynnistä sovellus run-painikkeella:

Versio 2

Sama kuin edellä, mutta tietorakennetta on monimutkaistettu lisäämällä siihen yksinkertainen taulukko. Tämä vastaa viikkotehtävän tilannetta, jossa joukkueeseen lisätään jäseniä. Tässä käytetään uudelleen samoja input-kenttiä eikä siis luoda lomaketta joka kerta uudelleen.

muokkaus 2 · muokkaus 2 + live-editori

Käynnistä sovellus run-painikkeella:

Versio 3

Tämä on täysin dynaaminen versio: koodiin ei ole kovakoodattu mitään tietorakenteen avaimia. Rakenteeseen voi suoraan lisätä uusia taulukoita tai merkkijonotyyppisiä tietoja. Muita tietotyyppejä ei ole huomioitu. Tässä ei myöskään pärjätä pelkän viitteen avulla, vaan päivitys tehdään taulukon indeksin perusteella — se on yllättäen tässä tilanteessa yksinkertaisin vaihtoehto. Viitettä käyttäessä täytyisi erikseen päivittää kaikki objektin ominaisuudet, mutta indeksin avulla taulukossa oleva alkio voidaan suoraan vaihtaa viittaamaan muokkauksessa käytettyyn kopioon.

Tämä ei ole millään tavalla parempi versio kuin edellinen, ellei ihan oikeasti ole tarvetta dynaamiseen laajenemiseen. Edellinen versio on yksinkertaisempi ja aivan riittävä, jos rakenne on tiedossa.

muokkaus 3

Harjoitustehtävänä voi miettiä, miten näitä muuttaisi, jos haluttaisiin mahdollistaa uusien tietojen lisääminen. Nämä osaavat nyt vasta muokata.

Lomake-esimerkkejä

Lisätietoa

Käyttäjien kommentit

Kommentoi Lisää kommentti