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}