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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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`] must be wrapping the [`BackdropViewer`].
///
/// ## Example
///
/// ```
/// # use yew::prelude::*;
/// # use patternfly_yew::*;
/// #[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);
    }
}

// component

#[derive(Clone, PartialEq, Properties)]
pub struct Props {
    pub children: Children,
}

pub struct BackdropViewer {
    content: Rc<Backdrop>,
    open: bool,
    ctx: Backdropper,
}

pub enum Msg {
    Open(Rc<Backdrop>),
    Close,
}

impl Component for BackdropViewer {
    type Message = Msg;
    type Properties = Props;

    fn create(ctx: &Context<Self>) -> Self {
        let ctx = Backdropper {
            callback: ctx.link().callback(|msg| msg),
        };

        Self {
            content: Default::default(),
            open: false,
            ctx,
        }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::Open(content) => {
                self.content = content;
                self.open();
            }
            Msg::Close => {
                if self.open {
                    self.content = Default::default();
                    self.close();
                }
            }
        }
        true
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        html!(
            <>
                <ContextProvider<Backdropper> context={self.ctx.clone()}>
                    if self.open {
                        <div class="pf-c-backdrop">
                            { self.content.content.clone() }
                        </div>
                    }
                    { for ctx.props().children.iter() }
                </ContextProvider<Backdropper>>
            </>
        )
    }
}

impl BackdropViewer {
    fn open(&mut self) {
        if let Some(body) = document().body() {
            let classes = js_sys::Array::of1(&JsValue::from_str("pf-c-backdrop__open"));
            body.class_list().add(&classes).ok();
        }
        self.open = true;
    }

    fn close(&mut self) {
        if let Some(body) = document().body() {
            let classes = js_sys::Array::of1(&JsValue::from_str("pf-c-backdrop__open"));
            body.class_list().remove(&classes).ok();
        }
        self.open = false;
    }
}

/// Interact with the [`BackdropViewer`] through the [`Backdropper`].
#[hook]
pub fn use_backdrop() -> Option<Backdropper> {
    use_context::<Backdropper>()
}