Element <dialog>: nevymýšlej znovu modál!
Vídáme je všude. Vyskakují na nás s potvrzením cookies, odesíláme přes ně tweety, otevírají se v nich menu či jenom prostá upozornění. Každý je implementuje po svém, a přitom již dospělo nativní řešení v podobě elementu <dialog>. Pojďme ho poznat společně do hloubky.
Každá UI knihovna pojmenovává komponenty trochu jinak. Co se týká anatomie, eventuálně implementace, tak jsou si často některé komponenty podobné (byť každá může být vhodná pro jiné účely). S variantou jedné takové se ale setkáme snad všude. A to je Modal. Někde se jmenuje Dialog či Popup. V různých mutacích se pak ještě objevuje jako Drawer či Offcanvas (dialog, který vyjíždí ze strany). Všechny ale mají jedno společné. Objeví se nad současným rozhraním a prioritizují pro uživatele konkrétní akci či informaci. A ve všech případech je jejich implementace dosti složitá. Pojďme se tedy podívat, jak si takovou implementaci zjednodušit a zpříjemnit pomocí nativního elementu <dialog>
.
Seznamte se, <dialog> element
Část z nás se jistě při vytváření a stylování modálu již určitě zapotila. Vždy je to stále dokola to samé:
- postavit dialog a jeho obsah,
- udělat overlay se ztmaveným backdropem,
- vytvořit API pro otevírání a zavírání modálu,
- a nakonec vyřešit zavírání při stisknutí klávesy
Escape
, kliknutí mimo<dialog>
(na backdrop) a jiné okrajové případy (třeba zabránit scrollování na stránce).
Tolik věcí se opakuje, a přitom tu máme element <dialog>
, který nám tohle (téměř) všechno servíruje na stříbrném podnosu. A ještě k tomu řeší i přístupnost.
První náhled na element <dialog>
už nám udělal Tomáš Pustelník ve své přednášce na WebExpu – HTML can do that. Pojďme si to zrekapitulovat.
Samotný dialog je relativně jednoduchý element. Nemá žádné vlastnosti až na atribut open
, který určuje, jestli bude dialog při načtení stránky otevřen. Zde však pozor, takto otevřený dialog je tzv. nemodální (mj. nelze zavřít klávesou Escape
) a v samotném HTML neexistuje žádný způsob, jak jednou zavřený dialog znovu otevřít. Ovládání výlučně přes JavaScript je proto preferovanou variantou. Ale o tom později.
<dialog open>
<p>Greetings, one and all!</p>
<form method="dialog">
<button>OK</button>
</form>
</dialog>
Zajímavé to začíná být až v kombinaci s elementem <form>
, pokud má atribut method="dialog"
, anebo pokud má jeho odesílací tlačítko nastaveno formmethod="dialog"
. (Tlačítko musí být typu submit
, což je ostatně výchozí hodnota atributu type
pro <button>
.) V takovém případě je po stisknutí tlačítka stav formuláře uložen, dialog zavřen a návratová hodnota dialog.returnValue
je nastavena na hodnotu tlačítka.
<main>
<button id="open">Open dialog</button>
<dialog>
<form method="dialog">
<h1>Hello, would you like to do something cool?<h1/>
<button id="close" value="cancel">Go to hell!</button>
<button id="confirm" value="confirm">Let's go!</button>
</dialog>
</main>
Tím to však nekončí. Element <dialog>
přidává nově také pseudoelement ::backdrop
. Díky tomu lze snadno nastylovat vrstvu, která se zobrazuje za dialogem – typicky jako ztmavení nedosažitelného obsahu.
dialog::backdrop {
background-color: rgba(0 0 0 / 50%);
visibility: visible;
opacity: 1;
}
Zavři mě a otevři mě, samozřejmě JavaScriptem
A nyní k ovládání, protože ne všechno funguje samo a trocha JavaScriptu je vždy potřeba. Nicméně i zde nám <dialog>
šetří psaní a nabízí nám hned několik možností, jak s ním interagovat.
Metody
.show()
Rovnou začnu metodou, která může zprvu vyvolat dojem nekonzistentního API. Metoda show()
totiž nezobrazí dialog, jak by vývojář předpokládal, tedy v modálním stavu. Tato metoda otevírá Dialog v takzvaném nemodálním stavu, kdy interakce s obsahem mimo dialog je nadále povolena. Je to podobné jako s atributem open
, který jsem popisoval na začátku.
.showModal()
Tohle je ta pravá metoda, kterou chcete použít k otevírání dialogu. Dojde k otevření dialogu tak, jak jsme všichni zvyklí, přes všechny další dialogy do nejsvrchnější vrstvy. Aktivuje se ::backdrop
pseudoelement a interakce s obsahem mimo dialog je blokována. (Bohužel, posouvání obsahu pod dialogem blokováno není – zatím.)
.close()
Zavírání dialogu už je snadné, k tomu slouží jediná metoda, která navíc jako argument přijímá návratovou hodnotu, kterou chceme dostat do dialog.returnValue
.
Události
Element <dialog>
vyvolává dvě události.
close
Událost close
je vyvolána, když je dialog zavřen tlačítkem.
cancel
Událost cancel
je vyvolána, když je dialog zrušen, např. klávesou Escape
. Už to samotné automaticky dialog zavírá a nám tak odpadá další část implementace.
<script>
(() => {
const updateButton = document.getElementById('updateDetails');
const closeButton = document.getElementById('close');
const dialog = document.getElementById('favDialog');
dialog.returnValue = 'favAnimal';
function openCheck(dialog) {
if (dialog.open) {
console.log('Dialog open');
} else {
console.log('Dialog closed');
}
}
// Otevření modálního dialogu
updateButton.addEventListener('click', () => {
dialog.showModal();
openCheck(dialog);
});
// Zavření dialogu
closeButton.addEventListener('click', () => {
dialog.close('animalNotChosen');
openCheck(dialog);
});
})();
</script>
Pak už stačí jen vyřešit zavírání při kliknutí na backdrop a je hotovo. Ale i zde je řešení jednoduché, neboť kliknutí na backdrop dostává jako event.target
daný dialog. Tím pádem stačí pouze porovnat, zda uživatel kliknul do obsahu dialogu, anebo na samotný element dialogu, a ten případně zavřít.
A co React?
Také implementace v Reactu se podstatně zjednodušila. Začněme samotnou komponentou Modal využívající element <dialog>
a referenci na něj.
function Modal({ children }) {
const dialogRef = React.useRef(null);
return <dialog ref={dialogRef}>{children}</dialog>;
}
Dále potřebujeme ovládat otevřený a zavřený stav pomocí metod showModal()
a close()
, jež jsme popsali výše.
function Modal({ children, open }) {
const dialogRef = React.useRef(null);
React.useEffect(() => {
const dialogNode = dialogRef.current;
if (open) {
dialogNode.showModal();
} else {
dialogNode.close();
}
}, [open]);
return <dialog ref={dialogRef}>{children}</dialog>;
}
Poté už je potřeba pouze dořešit, co se má stát, když uživatel stiskne Escape
a dojde k vyvolání události cancel
.
function Modal({ children, open, onRequestClose }) {
const dialogRef = React.useRef(null);
React.useEffect(() => {
const dialogNode = dialogRef.current;
if (open) {
dialogNode.showModal();
} else {
dialogNode.close();
}
}, [open]);
React.useEffect(() => {
const dialogNode = dialogRef.current;
const handleCancel = (event) => {
event.preventDefault();
onRequestClose();
};
dialogNode.addEventListener('cancel', handleCancel);
return () => {
dialogNode.removeEventListener('cancel', handleCancel);
}
}, [onRequestClose]);
return <dialog ref={dialogRef}>{children}</dialog>;
}
A jako třešničku na dortu můžeme přidat vrácení focusu na element, který modal otevřel, jak doporučuje WAI-ARIA.
function Modal({ children, open, onRequestClose }) {
// …
const lastActiveElement = React.useRef(null);
React.useEffect(() => {
const node = ref.current;
if (open) {
lastActiveElement.current = document.activeElement;
node.showModal();
} else {
node.close();
lastActiveElement.current.focus();
}
}, [open]);
// …
}
Anebo si můžete celou implementaci rozložit na hooky a základní komponentu Dialog. Modal si pak už jen sestavíte jako LEGO, jako jsme to udělali i my ve Spirit Design System.
Co si z toho odnést
Výhody
- Je potřeba napsat minimum JavaScriptu,
- přístupnost je takřka vyřešena až na pár problémů s autofocusy,
- existuje pseudoelement
::backdrop
, který lze jednoduše stylovat (ale pozor, ne animovat – to se zatím obchází pseudoelementem::before
nebo::after
), - formulářový atribut
method="dialog"
zpřístupňuje hodnoty z formuláře a tlačítek při akci v dialogu, - stisknutí klávesy
Escape
dialog automaticky zavře.
Nevýhody
- Rozšířenější funkcionalita potřebuje více JavaScriptu, ale zase ne o tolik ;-),
- kliknutí na backdrop nezavře dialog automaticky – k tomu je potřeba trochu JavaScriptu,
- některé drobnější problémy s přístupností, viz autofocusy.
Podpora v prohlížečích
Jak je vidět v následující tabulce, element <dialog>
je možné již dnes využít v drtivé většině prohlížečů, přičemž na řešení dalších problémů se pracuje.
Pokud tedy váš web nepotřebuje nějaké specialitky jako třeba podporu hodně starých verzí prohlížečů, případně nemáte extrémně omezené zdroje či čas, určitě vám doporučuji na element <dialog>
přejít. Vždy asi budou nějaké případy, kdy vám nezbude než upřednostnit role="dialog"
před značkou <dialog>
, ale takových už bude jen a jen méně.
Nechcete-li však přidávat do projektu další, byť sebemenší závislosti jen proto, abyste mohli používat modál, sáhněte po elementu <dialog>
. Nechcete přece znovu vymýšlet kolo, tedy pardon – dialog :-).
Související odkazy
- <dialog>: The Dialog Element (MDN)
- Use the dialog element (reasonably) (Scott O'Hara, 2023)
- Build a Dialog Component in React (Travis Arnold, souporserious.com, 2020)
- Building a dialog component (Adam Argyle, web.dev, 2022)
Anglickou verzi článku najdete na webu autora.
Sdílejte
Líbí se vám článek? Podpořte jej sdílením!
Komentujte
Chcete k článku něco doplnit? Našli jste chybu? Napište e-mail.