Esikatselu
const BasicModal = () => {
const [isModalOpen, setModalOpen] = useState(false);
return (
<>
<Button onClick={() => setModalOpen(!isModalOpen)}>Näytä Modal</Button>
<Modal
appRootId="root"
variant="primary"
toggle={() => setModalOpen(!isModalOpen)}
isOpen={isModalOpen}
showClose
closeLabel="Sulje"
>
<ModalHeader>Lorem ipsum</ModalHeader>
<ModalContent>
<Text>Dolor sit amet.</Text>
</ModalContent>
<ModalFooter>
<ButtonGroup horizontal="sm">
<Button onClick={() => setModalOpen(!isModalOpen)}>Consectetur </Button>
<Button appearance="outline" onClick={() => setModalOpen(!isModalOpen)}>
Adipiscing
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
);
};
render(<BasicModal />);
Käyttötarkoitus
Modaalisen dialogin tarkoitus on pysäyttää asiakkaan prosessi ja ohjata huomio tiettyyn toimintoon. Asiakas voi jatkaa eteenpäin vain ottamalla kantaa dialogissa esitettyyn asiaan. Kun dialogi on näkyvissä, asiakas ei pysty selailemaan muuta käyttöliittymää eikä suorittamaan muita toimintoja.
Dialogin avulla voit varmistaa, haluaako asiakas todella suorittaa jonkin toiminnon, jolla voi olla vakavia seurauksia. Esimerkiksi sellaisten tietojen poistaminen, joiden lisääminen uudelleen on vaikeaa tai mahdotonta, kannattaa varmistaa tällä tavoin. Mieti kuitenkin aina, onko virheen mahdollisuus ja seuraukset sellaiset, että jokaisen asiakkaan kohdalla kannattaa tehdä ylimääräinen varmistus.
Dialogit sopivat myös tilanteisiin, joissa virheen tai epämieluisan tilanteen estäminen vaatii asiakkaalta välitöntä reagointia. Dialogi voi esimerkiksi ilmoittaa, että asiakas kirjataan pian ulos palvelusta, ellei hän itse valitse jatkavansa palvelun käyttöä.
Lisäksi dialogia voi käyttää harkiten tilanteissa, joissa asiakkaalta vaaditaan jotain prosessin jatkamisen kannalta tärkeää informaatiota.
- Käytä dialogia, jos prosessissa eteneminen edellyttää asiakkaalta erillistä varmistusta.
- Käytä dialogia, jos tilanne vaatii asiakkaalta välittömiä toimenpiteitä (esimerkiksi asiakas kirjataan ulos palvelusta tietyn ajan kuluessa).
- Kirjoita dialogin tekstit mahdollisimman lyhyesti ja kuvaavasti.
- Lisää dialogiin aina otsikko ja vähintään yksi toimintopainike.
- Nimeä dialogin painikkeet selkeästi, jotta asiakas ymmärtää varmasti, mitä hän on tekemässä. (Mieluummin "Poista valmistelutiedot" kuin "OK")
- Älä käytä dialogia rutiininomaisesti esimerkiksi kaikissa poistotoiminnoissa. Tällöin asiakas helposti oppii sivuuttamaan ne, eivätkä ne enää palvele tarkoitustaan, vaan tuovat asiakkaalle vain yhden turhan klikkauksen lisää.
- Älä sisällytä Modal-komponenttiin ylimääräistä lisätietoa tai hyvä tietää -linkkejä.
Dialogin sisältö
Dialogin tulee sisältää aina vähintään seuraavat elementit:
ModalHeader
- Otsikko, joka kuvaa dialogin toiminnon.
ModalContent
- Sisältö, joka selventää otsikkoa tai tarjoaa käyttäjälle ohjeita.
ModalFooter
& Button
- Vähintään yksi painike, josta käyttäjä voi sulkea dialogin.
Pyri käyttämään yhtenäisiä värejä dialogin sisällä. Vältä esimerkiksi useiden eriväristen painikkeiden käyttöä samassa dialogissa.
Saavutettavuus
Komponentti on toteutettu saavutettavaksi, mutta käytössä tulee huomioida seuraavat asiat:
- Oletuksena ruudunlukija lukee otsikon ja sisällön automaattisesti, kun dialogi avataan.
- Dialogissa on aina oltava vähintään yksi klikattava/fokusoitava komponentti näppäimistöfokuksen käsittelyä varten.
- Oletuksena fokus siirretään automaattisesti dialogin ensimmäiseen fokusoitavaan komponenttiin.
- Mikäli dialogin sisältö on pitkä, tulee sisällön automaattinen lukeminen poistaa käytöstä ja kohdistus siirtää dialogin otsikkoon.
- Kohdistuksen oletustapaa voidaan muuttaa hyödyntämällä komponentin
focusRef
-proppia (kts. esimerkit alla).
- Kohde-elementin tulee olla kohdistettava elementti tai sillä tulee olla
tabIndex="-1"
.
Käyttö näppäimistöllä
Näppäimistön kohdistus pysyy dialogin sisällä kunnes käyttäjä sulkee dialogin.
|
Esc | Sulje dialogi. |
Tab | Seuraava dialogissa oleva toiminto. |
Shift + Tab | Edellinen dialogissa oleva toiminto. |
Erilaiset dialogit
Neutraali Dialogi
Esitä neutraalit sisällöt sinisessä dialogissa, jonka painikkeet ovat myös sinisiä.
const BasicModal = () => {
const [isModalOpen, setModalOpen] = useState(false);
return (
<>
<Button onClick={() => setModalOpen(!isModalOpen)}>Rekisteröi palvelu</Button>
<Modal
appRootId="root"
variant="primary"
toggle={() => setModalOpen(!isModalOpen)}
isOpen={isModalOpen}
showClose
closeLabel="Sulje"
>
<ModalHeader>Rekisteröi uusi kuntoutuspalvelu</ModalHeader>
<ModalContent>
<form noValidate>
<SelectionGroup
id="example-radio-group"
labelText="Kuntoutuspalvelu"
helpText="Valitse kuntoutuspalvelu, johon haluat rekisteröityä"
required
>
<Radio name="example-option" id="radio-example-option-1" labelText="Oma väylä" defaultChecked />
<Radio name="example-option" id="radio-example-option-2" labelText="Kuntoutuskurssi" />
</SelectionGroup>
</form>
</ModalContent>
<ModalFooter>
<ButtonGroup horizontal="sm">
<Button onClick={() => setModalOpen(!isModalOpen)}>Jatka</Button>
<Button appearance="outline" onClick={() => setModalOpen(!isModalOpen)}>
Peruuta
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
);
};
render(<BasicModal />);
Dialogi tummalla taustalla
Modalin taustavärin voi vaihtaa tummaksi tilanteissa, joissa Modal ei muuten erottuisi riittävästi taustasta.
const DarkModal = () => {
const [isModalOpen, setModalOpen] = useState(false);
return (
<>
<Button onClick={() => setModalOpen(!isModalOpen)}>Rekisteröi palvelu</Button>
<Modal
appRootId="root"
backdropColor="dark"
variant="primary"
toggle={() => setModalOpen(!isModalOpen)}
isOpen={isModalOpen}
showClose
closeLabel="Sulje"
>
<ModalHeader>Rekisteröi uusi kuntoutuspalvelu</ModalHeader>
<ModalContent>
<form noValidate>
<SelectionGroup
id="example-radio-group"
labelText="Kuntoutuspalvelu"
helpText="Valitse kuntoutuspalvelu, johon haluat rekisteröityä"
required
>
<Radio name="example-option" id="radio-example-option-1" labelText="Oma väylä" defaultChecked />
<Radio name="example-option" id="radio-example-option-2" labelText="Kuntoutuskurssi" />
</SelectionGroup>
</form>
</ModalContent>
<ModalFooter>
<ButtonGroup horizontal="sm">
<Button onClick={() => setModalOpen(!isModalOpen)}>Jatka</Button>
<Button appearance="outline" onClick={() => setModalOpen(!isModalOpen)}>
Peruuta
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
);
};
render(<DarkModal />);
Kriittinen dialogi
Käytä dialogissa ja sen painikkeissa punaista väriä, kun toiminto on asiakkaan kannalta kriittinen ja asiakkaan on vaikea peruuttaa toimintoa. Esimerkiksi tietojen poistaminen ja kirjautuminen ulos palvelusta voivat olla kriittisiä toimintoja.
const DestructiveModal = () => {
const [isModalOpen, setModalOpen] = useState(false);
return (
<>
<Button variant="danger" onClick={() => setModalOpen(!isModalOpen)}>
Poista sopimusluonnos
</Button>
<Modal
appRootId="root"
variant="danger"
toggle={() => setModalOpen(!isModalOpen)}
isOpen={isModalOpen}
showClose
closeLabel="Sulje"
>
<ModalHeader>Poista sopimusluonnos</ModalHeader>
<ModalContent>
<p className="kds-mb-0">
Olet poistamassa sopimusluonnoksen, jolloin menetät kaikki siihen liittyvät tiedot. Haluatko poistaa
sopimusluonnoksen?
</p>
</ModalContent>
<ModalFooter>
<ButtonGroup horizontal="sm">
<Button variant="danger" onClick={() => setModalOpen(!isModalOpen)}>
Poista sopimusluonnos
</Button>
<Button variant="danger" appearance="outline" onClick={() => setModalOpen(!isModalOpen)}>
Peruuta
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
);
};
render(<DestructiveModal />);
Dialogi, jota ei voi ohittaa
Dialogi on oletuksena suljettavissa painikkeiden lisäksi klikkaamalla sen ulkopuolelta tai painamalla Esc-näppäintä. Tämän toiminnallisuuden voi estää closeOnClickOutside={false}
-propilla. Tässä tapauksessa myös toggle
-propin voi jättää pois.
Käytä tätä vain erikoistapauksissa, kun haluat varmistaa, että käyttäjä ei voi ohittaa dialogia painamatta siinä olevia painikkeita. Esimerkkinä palvelun istunnon vanheneminen, jolloin palvelu avaa dialogin automaattisesti eikä siinä ole “Peruuta”-painiketta.
const MandatoryModal = () => {
const sessionTimeout = 300;
const sessionTimeoutAnnounceInterval = 20;
const [isModalOpen, setModalOpen] = useState(false);
const [timeLeft, setTimeLeft] = useState(sessionTimeout);
const [lastAnnounceTime, setLastAnnounceTime] = useState(sessionTimeout);
const [minutes, setMinutes] = useState(Math.floor(sessionTimeout / 60));
const [seconds, setSeconds] = useState(sessionTimeout % 60);
const [announceText, setAnnounceText] = useState("");
const resetLogoutTimer = () => {
setTimeLeft(sessionTimeout);
setLastAnnounceTime(sessionTimeout);
setMinutes(Math.floor(sessionTimeout / 60));
setSeconds(sessionTimeout % 60);
setAnnounceText("");
};
useEffect(() => {
if (timeLeft === 0 || !isModalOpen) {
return resetLogoutTimer();
}
const intervalId = setInterval(() => {
setTimeLeft((prevTime) => {
const newTimeLeft = prevTime - 1;
const newMinutes = Math.floor(newTimeLeft / 60);
const newSeconds = newTimeLeft % 60;
setMinutes(newMinutes);
setSeconds(newSeconds);
const nextAnnounceTime = lastAnnounceTime - sessionTimeoutAnnounceInterval;
if (newTimeLeft < nextAnnounceTime || newTimeLeft === sessionTimeout) {
setLastAnnounceTime(newTimeLeft);
if (minutes === 0 && seconds <= 10) {
setAnnounceText(
`Istuntosi suljetaan automaattisesti ${seconds} sekunnin kuluttua. Jos et jatka istuntoa tänä aikana, kirjaudut automaattisesti ulos asiointipalvelusta ja menetät tallentamattomat muutokset.`
);
} else {
setAnnounceText(
`Istuntosi suljetaan automaattisesti ${minutes} minuutin ja ${seconds} sekunnin kuluttua. Jos et jatka istuntoa tänä aikana, kirjaudut automaattisesti ulos asiointipalvelusta ja menetät tallentamattomat muutokset.`
);
}
}
setTimeout(() => {
setAnnounceText("");
}, 5000);
return newTimeLeft;
});
}, 1000);
return () => clearInterval(intervalId);
}, [timeLeft, lastAnnounceTime, isModalOpen, sessionTimeoutAnnounceInterval, announceText]);
return (
<>
<Button
variant="danger"
onClick={() => {
setModalOpen(!isModalOpen);
setTimeLeft(sessionTimeout);
}}
>
Istunnon vanheneminen
</Button>
<Modal appRootId="root" variant="danger" closeOnClickOutside={false} isOpen={isModalOpen}>
<ModalHeader>Istuntosi on vanhenemassa</ModalHeader>
<ModalContent>
<div className="kds-sr-only" aria-atomic="true" aria-live="polite">
<p>{announceText}</p>
</div>
<div>
<p>
Istuntosi suljetaan automaattisesti{" "}
<strong>
{minutes}:{seconds < 10 ? `0${seconds}` : seconds}
</strong>{" "}
minuutin kuluttua.
</p>
<p className="kds-mb-0">
Jos et jatka istuntoa tänä aikana, kirjaudut automaattisesti ulos asiointipalvelusta ja menetät
tallentamattomat muutokset.
</p>
</div>
</ModalContent>
<ModalFooter>
<ButtonGroup horizontal="sm">
<Button variant="danger" onClick={() => setModalOpen(!isModalOpen)}>
Jatka istuntoa
</Button>
<Button variant="danger" appearance="outline" onClick={() => setModalOpen(!isModalOpen)}>
Kirjaudu ulos
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
);
};
render(<MandatoryModal />);
Dialogi ikonilla
Dialogiin voi lisätä käyttöliittymäkuvakkeen käyttöliittymäkuvakkeen. Kuvakkeen väri määräytyy dialogin tyypin mukaan.
const IconModal = () => {
const [isModalOpen, setModalOpen] = useState(false);
return (
<>
<Button variant="danger" onClick={() => setModalOpen(!isModalOpen)}>
Istunnon vanheneminen
</Button>
<Modal
icon={<IconError />}
iconAriaLabel="Huutomerkki"
appRootId="root"
variant="danger"
closeOnClickOutside={false}
isOpen={isModalOpen}
>
<ModalHeader>Istuntosi on vanhenemassa</ModalHeader>
<ModalContent>
<p>
Istuntosi suljetaan automaattisesti <strong>MM:SS</strong> minuutin kuluttua.
</p>
<p className="kds-mb-0">
Jos et jatka istuntoa tänä aikana, kirjaudut automaattisesti ulos asiointipalvelusta ja menetät
tallentamattomat muutokset.
</p>
</ModalContent>
<ModalFooter>
<ButtonGroup horizontal="sm">
<Button variant="danger" onClick={() => setModalOpen(!isModalOpen)}>
Jatka istuntoa
</Button>
<Button variant="danger" appearance="outline" onClick={() => setModalOpen(!isModalOpen)}>
Kirjaudu ulos
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
);
};
render(<IconModal />);
Dialogin sisällön räätälöinti ja kohdistuksen käsittely
Dialogiin voi yhdistää muita komponentteja tarpeen mukaan. Vältä ylimääräisen paddingin käyttöä dialogin sisällä, muuten dialogi toimii huonosti pienillä näytöillä.
Tässä esimerkissä kohdistuksen käsittelyä on muokattu. Näppäimistökohdistus on siirretty dialogin otsikkoon focusRef
-propilla.
const CustomModal = () => {
const [isModalOpen, setModalOpen] = useState(false);
const [isCollapseOpen, setCollapseOpen] = useState(false);
const focusElemRef = useRef();
return (
<>
<Button onClick={() => setModalOpen(!isModalOpen)}>Lisää toimipiste</Button>
<Modal
appRootId="root"
variant="primary"
toggle={() => setModalOpen(!isModalOpen)}
isOpen={isModalOpen}
readContent={false}
focusRef={focusElemRef}
showClose
closeLabel="Sulje"
>
<ModalHeader ref={focusElemRef} tabIndex="-1">
Lisää toimipiste
</ModalHeader>
<Alert variant="primary" className="kds-mb-0" flush>
<p className="kds-mb-0">
Huomaa miten <strong>Alert</strong> ja <strong>Accordion</strong> on tasattu.
</p>
</Alert>
<ModalContent>
<form noValidate className="kds-py-4">
<SelectionGroup
id="radio-group"
labelText="Valitse lisättävä toimipiste"
helpText="Valitse jokin Soteriin ilmoitetuista toimipisteistä tai muu ainoastaan musiikkiterapiaa tarjoava toimipiste."
required
>
<Radio name="radio-location" id="radio-leppavaara" labelText="Leppävaaran toimipiste" defaultChecked />
<Radio name="radio-location" id="radio-martinlaakso" labelText="Martinlaakson toimipiste" />
</SelectionGroup>
</form>
</ModalContent>
<Accordion id="instuction-accordion" isOpen={isCollapseOpen}>
<AccordionToggle onClick={() => setCollapseOpen(!isCollapseOpen)}>
<AccordionTitle>Mitä toimipisteitä voi lisätä?</AccordionTitle>
</AccordionToggle>
<AccordionBody>
<p>
Voit lisätä toimipisteitä, jotka on ilmoitettu sosiaali- ja terveydenhuollon tuottajien rekisteriin eli
Soteriin. Poikkeuksena toimipisteet, jotka tarjoavat vain musiikkiterapiaa.
</p>
<p>Jos palveluntuottajalla ei ole omia toimitiloja, lisää hallinnollinen toimipiste.</p>
<p>Lisää jokainen palveluita tarjoava toimipiste erikseen.</p>
</AccordionBody>
</Accordion>
<ModalFooter>
<ButtonGroup horizontal="sm">
<Button onClick={() => setModalOpen(!isModalOpen)}>Jatka</Button>
<Button appearance="outline" onClick={() => setModalOpen(!isModalOpen)}>
Peruuta
</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
);
};
render(<CustomModal />);