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}