yew_styles/components/
modal.rs

1use crate::styles::{get_palette, get_size, get_style, Palette, Size, Style};
2use crate::utils::get_html_element_by_class;
3use stylist::{css, StyleSource};
4use wasm_bindgen::JsCast;
5use wasm_bindgen_test::*;
6use web_sys::Element;
7use yew::prelude::*;
8use yew::{utils, App};
9
10/// # Modal component
11///
12/// ## Features required
13///
14/// modal
15///
16/// ## Example
17///
18/// ```rust
19/// use wasm_bindgen::JsCast;
20/// use web_sys::HtmlElement;
21/// use yew::prelude::*;
22/// use yew::utils::document;
23/// use yew_prism::Prism;
24/// use yew_styles::button::Button;
25/// use yew_styles::modal::Modal;
26/// use yew_styles::styles::{get_size, Palette, Size, Style};
27///
28/// pub struct ModalExample {
29///     link: ComponentLink<Self>,
30///     show_modal: bool,
31/// }
32///
33/// pub enum Msg {
34///     CloseModal,
35///     OpenModal,
36///     CloseModalByKb(KeyboardEvent),
37/// }
38///
39/// impl Component for ModalExample {
40///     type Message = Msg;
41///     type Properties = ();
42///
43///     fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
44///         Self {
45///             link,
46///             show_modal: false,
47///         }
48///     }
49///
50///     fn update(&mut self, msg: Self::Message) -> ShouldRender {
51///         let body_style = document()
52///             .body()
53///             .unwrap()
54///             .dyn_into::<HtmlElement>()
55///             .unwrap()
56///             .style();
57///
58///         match msg {
59///             Msg::CloseModal(index) => {
60///                 body_style.set_property("overflow", "auto").unwrap();
61///                 self.show_modal = false;
62///             }
63///             Msg::CloseModalByKb(keyboard_event) => {
64///                 if keyboard_event.key_code() == 27 {
65///                     body_style.set_property("overflow", "auto").unwrap();
66///                     self.show_modal = false;
67///                 }
68///             }
69///             Msg::OpenModal => {
70///                 body_style.set_property("overflow", "hidden").unwrap();
71///
72///                 self.show_modal = true;
73///             }
74///         };
75///         true
76///     }
77///
78///     fn change(&mut self, _: Self::Properties) -> ShouldRender {
79///         false
80///     }
81///
82///     fn view(&self) -> Html {
83///         html! {
84///             <>
85///                 <Modal
86///                     header=html!{
87///                         <b>{"Standard modal"}</b>
88///                     }
89///                     header_palette=Palette::Link
90///                     body=html!{
91///                         <div class="body-content">
92///                             <p>{"This is a example modal"}</p>
93///                             <Button
94///                                 button_palette= Palette::Info
95///                                 onclick_signal= self.link.callback(|_| Msg::CloseModal)
96///                             >{"Accept"}</Button>
97///                         </div>
98///                     }
99///                     body_style=Style::Outline
100///                     body_palette=Palette::Link
101///                     is_open=self.show_modal
102///                     onclick_signal= self.link.callback(|_| Msg::CloseModal)
103///                     onkeydown_signal= self.link.callback(Msg::CloseModalByKb)
104///                 />
105///                 <Button
106///                     button_palette= Palette::Primary
107///                     onclick_signal= self.link.callback(Msg::OpenModal)
108///                 >{"Standard modal"}</Button>
109///             </>
110///         }
111///     }
112/// }
113/// ```    
114pub struct Modal {
115    link: ComponentLink<Self>,
116    props: Props,
117}
118
119#[derive(Clone, PartialEq, Properties)]
120pub struct Props {
121    /// Header of the modal. Required
122    pub header: Html,
123    /// body of the modal. Required
124    pub body: Html,
125    /// if it is true, shows the modal otherwise is hidden. Required
126    pub is_open: bool,
127    /// click event for modal (usually to close the modal)
128    #[prop_or(Callback::noop())]
129    pub onclick_signal: Callback<MouseEvent>,
130    /// keyboard event for modal (usually to close the modal)
131    #[prop_or(Callback::noop())]
132    pub onkeydown_signal: Callback<KeyboardEvent>,
133    /// Type modal background style. Default `Palette::Standard`
134    #[prop_or(Palette::Standard)]
135    pub modal_palette: Palette,
136    /// Three diffent modal standard sizes. Default `Size::Medium`
137    #[prop_or(Size::Medium)]
138    pub modal_size: Size,
139    /// Type modal header style. Default `Palette::Standard`
140    #[prop_or(Palette::Standard)]
141    pub header_palette: Palette,
142    /// Modal header styles. Default `Style::Regular`
143    #[prop_or(Style::Regular)]
144    pub header_style: Style,
145    /// If hove, focus, active effects are enable in the header. Default `false`
146    #[prop_or(false)]
147    pub header_interaction: bool,
148    /// Type modal body style. Default `Palette::Standard`
149    #[prop_or(Palette::Standard)]
150    pub body_palette: Palette,
151    /// Modal body styles. Default `Style::Regular`
152    #[prop_or(Style::Regular)]
153    pub body_style: Style,
154    /// If hove, focus, active effects are enable in the body. Default `false`
155    #[prop_or(false)]
156    pub body_interaction: bool,
157    /// If the modal content get the focus. Set to false if the modal includes input events. Default `true`
158    #[prop_or(true)]
159    pub auto_focus: bool,
160    /// General property to get the ref of the component
161    #[prop_or_default]
162    pub code_ref: NodeRef,
163    /// General property to add keys
164    #[prop_or_default]
165    pub key: String,
166    /// General property to add custom class styles
167    #[prop_or_default]
168    pub class_name: String,
169    /// General property to add custom id
170    #[prop_or_default]
171    pub id: String,
172    /// Set css styles directly in the component
173    #[prop_or(css!(""))]
174    pub styles: StyleSource<'static>,
175}
176
177pub enum Msg {
178    Clicked(MouseEvent),
179    Pressed(KeyboardEvent),
180}
181
182impl Component for Modal {
183    type Message = Msg;
184    type Properties = Props;
185
186    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
187        Self { link, props }
188    }
189
190    fn update(&mut self, msg: Self::Message) -> ShouldRender {
191        match msg {
192            Msg::Clicked(mouse_event) => {
193                let target_event = mouse_event
194                    .target()
195                    .unwrap()
196                    .dyn_into::<Element>()
197                    .unwrap()
198                    .class_list();
199
200                if target_event.value().starts_with("modal container") {
201                    self.props.onclick_signal.emit(mouse_event);
202                }
203            }
204            Msg::Pressed(keyboard_event) => {
205                self.props.onkeydown_signal.emit(keyboard_event);
206            }
207        };
208        true
209    }
210
211    fn change(&mut self, props: Self::Properties) -> ShouldRender {
212        if self.props != props {
213            self.props = props;
214            true
215        } else {
216            false
217        }
218    }
219
220    fn rendered(&mut self, _first_render: bool) {
221        if self.props.is_open && self.props.auto_focus {
222            let modal_form = get_html_element_by_class("modal", 0);
223
224            modal_form.focus().unwrap();
225        }
226    }
227
228    fn view(&self) -> Html {
229        get_modal(self.props.clone(), self.link.clone())
230    }
231}
232
233fn get_modal(props: Props, link: ComponentLink<Modal>) -> Html {
234    if props.is_open {
235        html! {
236            <div
237                class=classes!("modal", "container", get_palette(props.modal_palette), props.class_name, props.styles)
238                key=props.key
239                ref=props.code_ref
240                tabindex="0"
241                id=props.id
242                onclick=link.callback(Msg::Clicked)
243                onkeydown=link.callback(Msg::Pressed)
244            >
245                <div class=format!("modal-content {}", get_size(props.modal_size))>
246                    <div class=format!(
247                        "modal-header {} {} {}",
248                        get_style(props.header_style),
249                        get_palette(props.header_palette),
250                        if props.header_interaction { "interaction" } else { "" }
251                    )>
252                        {props.header}
253                    </div>
254                    <div class=format!(
255                        "modal-body {} {} {}",
256                        get_style(props.body_style),
257                        get_palette(props.body_palette),
258                        if props.body_interaction { "interaction" } else { "" }
259                    )>
260                        {props.body}
261                    </div>
262                </div>
263            </div>
264        }
265    } else {
266        html! {}
267    }
268}
269
270wasm_bindgen_test_configure!(run_in_browser);
271
272#[wasm_bindgen_test]
273fn should_create_modal_component() {
274    let props = Props {
275        class_name: "test-modal".to_string(),
276        id: "modal-id-test".to_string(),
277        key: "".to_string(),
278        code_ref: NodeRef::default(),
279        onclick_signal: Callback::noop(),
280        onkeydown_signal: Callback::noop(),
281        modal_palette: Palette::Standard,
282        modal_size: Size::Medium,
283        header: html! {<div id="header">{"Modal Test"}</div>},
284        header_style: Style::Regular,
285        header_palette: Palette::Standard,
286        header_interaction: false,
287        body: html! {<div id="body">{"Content Test"}</div>},
288        body_style: Style::Regular,
289        body_palette: Palette::Standard,
290        body_interaction: false,
291        is_open: true,
292        auto_focus: false,
293        styles: css!(
294            "modal-content {
295                color: #000;
296            }"
297        ),
298    };
299
300    let modal: App<Modal> = App::new();
301
302    modal.mount_with_props(
303        utils::document().get_element_by_id("output").unwrap(),
304        props,
305    );
306
307    let modal_header_element = utils::document().get_element_by_id("header").unwrap();
308
309    let modal_body_element = utils::document().get_element_by_id("body").unwrap();
310
311    assert_eq!(modal_header_element.text_content().unwrap(), "Modal Test");
312    assert_eq!(modal_body_element.text_content().unwrap(), "Content Test");
313}
314
315#[wasm_bindgen_test]
316fn should_hide_modal_component_from_doom() {
317    let props = Props {
318        class_name: "test-modal".to_string(),
319        id: "modal-id-test".to_string(),
320        key: "".to_string(),
321        code_ref: NodeRef::default(),
322        onclick_signal: Callback::noop(),
323        onkeydown_signal: Callback::noop(),
324        modal_palette: Palette::Standard,
325        modal_size: Size::Medium,
326        header: html! {<div id="header">{"Modal Test"}</div>},
327        header_style: Style::Regular,
328        header_palette: Palette::Standard,
329        header_interaction: false,
330        body: html! {<div id="body">{"Content Test"}</div>},
331        body_style: Style::Regular,
332        body_palette: Palette::Standard,
333        body_interaction: false,
334        is_open: false,
335        auto_focus: false,
336        styles: css!(
337            "modal-content {
338                color: #000;
339            }"
340        ),
341    };
342
343    let modal: App<Modal> = App::new();
344
345    modal.mount_with_props(
346        utils::document().get_element_by_id("output").unwrap(),
347        props,
348    );
349
350    let modal_element = utils::document().get_element_by_id("modal-id-test");
351
352    assert_eq!(modal_element, None);
353}