patternfly_yew/components/
backdrop.rs

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