//! The one modal shell, a native ``. //! //! Opened with `showModal()`, so the browser supplies the backdrop //! (`::backdrop`), the focus trap, inertness for the rest of the page, the //! top layer, and Escape-to-close. None of that is hand-rolled here. use std::sync::atomic::{AtomicU32, Ordering}; use leptos::prelude::*; static NEXT_TITLE: AtomicU32 = AtomicU32::new(0); #[component] pub fn Modal( /// Classes for the card, appended to `modal` (e.g. `"card picker"`). #[prop(into)] class: String, /// Runs when the dialog asks to close: Escape, or a backdrop click. /// The owner clears its own state, which unmounts the dialog. on_close: Callback<()>, /// Id of the title element. Default: the first `.modal-title` present on /// mount. #[prop(optional, into)] labelled_by: Option, children: Children, ) -> impl IntoView { let dialog: NodeRef = NodeRef::new(); Effect::new(move |_| { if let Some(el) = dialog.get() { let title = labelled_by.clone().or_else(|| { let t = el.query_selector(".modal-title").ok().flatten()?; if t.id().is_empty() { t.set_id(&format!( "modal-title-{}", NEXT_TITLE.fetch_add(1, Ordering::Relaxed) )); } Some(t.id()) }); if let Some(id) = title { let _ = el.set_attribute("aria-labelledby", &id); } // `show_modal`, never `show`: only the modal form gets the top // layer, the backdrop and the focus trap. let _ = el.show_modal(); } }); view! {
{children()}
} }