1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
//! Backdrop visual
use gloo_utils::document;
use std::rc::Rc;
use wasm_bindgen::JsValue;
use yew::prelude::*;
/// Backdrop overlay the main content and show some new content, until it gets closed.
///
/// New content can be sent to the backdrop viewer using the [`Backdropper::open`] call. It can be
/// closed using the [`Backdropper::close`] call.
///
/// ## Contexts
///
/// The [`BackdropViewer`] must be wrapped by all contexts which the backdrop content might use,
/// as the content is injected as a child into the backdrop element. So if you can to send toasts
/// from a modal dialog, the [`ToastViewer`](crate::prelude::ToastViewer) must be wrapping the
/// [`BackdropViewer`].
///
/// ## Example
///
/// ```
/// # use yew::prelude::*;
/// # use patternfly_yew::prelude::*;
/// #[function_component(App)]
/// fn app() -> Html {
/// html! {
/// <>
/// <BackdropViewer>
/// <View/>
/// </BackdropViewer>
/// </>
/// }
/// }
/// #[function_component(View)]
/// fn view() -> Html {
/// let backdropper = use_backdrop().expect("Must be nested under a BackdropViewer component");
/// html!{
/// <div>
/// <button onclick={move |_| backdropper.open(Backdrop::new(
/// html! {
/// <Bullseye>
/// <Modal
/// title={"Example modal"}
/// variant={ ModalVariant::Medium }
/// description={"A description is used when you want to provide more info about the modal than the title is able to describe."}
/// >
/// <p>{"The modal body can contain text, a form, any nested html will work."}</p>
/// </Modal>
/// </Bullseye>
/// }))
/// }>
/// { "Click me" }
/// </button>
/// </div>
/// }
/// }
/// ```
#[derive(Clone, Debug)]
pub struct Backdrop {
pub content: Html,
}
impl Backdrop {
pub fn new(content: Html) -> Self {
Self { content }
}
}
impl Default for Backdrop {
fn default() -> Self {
Self { content: html!() }
}
}
impl From<Html> for Backdrop {
fn from(content: Html) -> Self {
Self { content }
}
}
/// A context for displaying backdrops.
#[derive(Clone, PartialEq)]
pub struct Backdropper {
callback: Callback<Msg>,
}
impl Backdropper {
/// Request a backdrop from the backdrop agent.
pub fn open<B>(&self, backdrop: B)
where
B: Into<Backdrop>,
{
self.callback.emit(Msg::Open(Rc::new(backdrop.into())));
}
/// Close the current backdrop.
pub fn close(&self) {
self.callback.emit(Msg::Close);
}
}
/// Properties for [``BackdropViewer]
#[derive(Clone, PartialEq, Properties)]
pub struct BackdropProperties {
pub children: Html,
}
#[doc(hidden)]
enum Msg {
Open(Rc<Backdrop>),
Close,
}
#[function_component(BackdropViewer)]
pub fn backdrop_viewer(props: &BackdropProperties) -> Html {
// hold the state of the current backdrop
let open = use_state::<Option<Rc<Backdrop>>, _>(|| None);
// create the context, only once
let ctx = {
let open = open.clone();
use_memo((), |()| Backdropper {
callback: Callback::from(move |msg| match msg {
Msg::Open(backdrop) => open.set(Some(backdrop)),
Msg::Close => open.set(None),
}),
})
};
// when the open state changes, change the overlay
use_effect_with(open.is_some(), |open| {
match open {
true => body_open(),
false => body_close(),
}
body_close
});
// render
html!(
<ContextProvider<Backdropper> context={(*ctx).clone()}>
if let Some(open) = &*open {
<div class="pf-v5-c-backdrop">
{ open.content.clone() }
</div>
}
{ props.children.clone() }
</ContextProvider<Backdropper>>
)
}
fn body_open() {
if let Some(body) = document().body() {
let classes = js_sys::Array::of1(&JsValue::from_str("pf-v5-c-backdrop__open"));
body.class_list().add(&classes).ok();
}
}
fn body_close() {
if let Some(body) = document().body() {
let classes = js_sys::Array::of1(&JsValue::from_str("pf-v5-c-backdrop__open"));
body.class_list().remove(&classes).ok();
}
}
/// Interact with the [`BackdropViewer`] through the [`Backdropper`].
#[hook]
pub fn use_backdrop() -> Option<Backdropper> {
use_context::<Backdropper>()
}