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>()
}