//! 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 leptos::prelude::*; use wasm_bindgen::JsCast; use web_sys::MouseEvent; #[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<()>, children: Children, ) -> impl IntoView { let dialog: NodeRef = NodeRef::new(); Effect::new(move |_| { if let Some(el) = dialog.get() { // `show_modal`, never `show`: only the modal form gets the top // layer, the backdrop and the focus trap. let _ = el.show_modal(); } }); view! { itself; a click // anywhere inside targets that element instead. The card below // carries the padding, so the dialog box is never a click // target except on the backdrop. on:click=move |ev: MouseEvent| { let Some(el) = dialog.get() else { return }; let on_backdrop = ev .target() .and_then(|t| t.dyn_into::().ok()) .is_some_and(|t| el.is_same_node(Some(&t))); if on_backdrop { on_close.run(()); } } >
{children()}
} }