yew_google_material/buttons/
mod.rs

1//! # GButton
2//! is similar to google material common buttons (not identical)
3//! 
4//! The key size attribute of button is `font_size`. It bonds a lot of other sizes and has the default value 14px. 
5//! According to this 1px here = 0.0714em
6//! 
7//! GButton has a lot of attributes (and you can make something similar to FAB button via them), but only `id` are required. If you use icon in button, `icon_style` attribute is also required. 
8//! 
9//! Attention! You must set `label` and/or use icon to make your button readable! 
10//! 
11//! All other attributes with default parameters:
12//!- label: `AttrValue`,
13//![default ""] Attention! If you do not use icon in button, you must use this!
14//!- button_type: `AttrValue`,
15//![default "button"]
16//!- style: `GButtonStyle`,
17//![default GButtonStyle::Filled]
18//!- outlined_border_color: `Option<AttrValue>`,
19//![default "#79747E"]
20//!- font_size: `AttrValue`, 
21//![default "14px"]
22//!- onclick: `Option<Callback<PointerEvent>>`,
23//![default None] Use PointerEvent instead of MouseEvent
24//!- class: `AttrValue`,
25//![default ""]
26//!- height: `AttrValue`,
27//![default "2.85em"]
28//!- width: `Option<AttrValue>`,
29//![default None]
30//!- parent: `DependsOn`,
31//![default None] This attribute required only with GTextInput
32//!- background_color: `AttrValue`,
33//![default "#6750A4"]
34//!- label_color: `AttrValue`, 
35//![default "#ffffff"]
36//!- border_radius: `AttrValue`,
37//![default "20px"] It is similar to container_shape in google material buttons
38//!- has_icon: `bool`,
39//![default false]
40//!- trailing_icon: `bool`,
41//![default false]
42//!- dark_theame: `bool`,
43//![default false] Experimental! Now it changes shadows from black to white if true.
44//!- disabled: `bool`,
45//![default false]
46//! 
47//! ## Examples
48//! ```
49//! use yew::prelude::*;
50//! use yew_google_material::prelude::*;
51//! 
52//! <GButton 
53//! id="use_g_button" 
54//! label="Button" />
55//! ```
56//! 
57//! Also you can add icon with `has_icon` attribute. If so, you also need to set `icon_style` attribute together with stylesheet inside `<head></head>`(see GIcon docs). If you need trailing icon use `trailing_icon` with `true` together with `has_icon` attributes in GButton.
58//! To adjust icon parameters use `fill`, `wght`, `grade`, `opsz` attributes as well as with GIcon.
59//! 
60//! Attention! The way to add icon in this version is different from v.0.0.7. 
61//! ```
62//! use yew::prelude::*;
63//! use yew_google_material::prelude::*;
64//! 
65//! <GButton 
66//! id="login_button"  // requiered
67//! label="Sign In"
68//! style={GButtonStyle::Outlined}
69//! label_color="#fff"
70//! has_icon="login"                     // requiered to add icon
71//! trailing_icon=true
72//! icon_style={GIconStyle::Outlined}    // requiered to add icon
73//! wght="400"                           // add it only for icon if you need it
74//! />
75//! ```
76//! Attention! If you change icon size within button you can break the design. Probably then you need to adjust `width` and `height`. Do it with caution.
77
78use button_css::input_style;
79use gloo_timers::future::TimeoutFuture;
80use web_sys::HtmlElement;
81use yew::platform::spawn_local;
82use yew::prelude::*;
83use wasm_bindgen::JsCast;
84use crate::{GButtonStyle, GIconStyle, icons::GIcon};
85
86mod button_css;
87
88#[derive(Default, PartialEq)]
89pub enum DependsOn {
90    GTextInput,
91    #[default]
92    None,
93}
94
95pub enum Msg {
96    OnPointerDown(PointerEvent),
97    OnKeyPress(KeyboardEvent),
98    OnPointerUp(PointerEvent),
99}
100
101#[derive(Properties, PartialEq)]
102pub struct GButtonProps {
103    pub id: AttrValue,
104    #[prop_or_default]
105    pub label: AttrValue,
106    #[prop_or_else(|| AttrValue::from("submit"))]
107    pub button_type: AttrValue,
108    #[prop_or_default]
109    pub style: GButtonStyle,
110    #[prop_or_default]
111    pub outlined_border_color: Option<AttrValue>,
112    #[prop_or_else(|| AttrValue::from("14px"))]
113    pub font_size: AttrValue, 
114    #[prop_or_default]
115    pub onclick: Option<Callback<PointerEvent>>,
116    #[prop_or_default]
117    pub class: AttrValue,
118    #[prop_or_else(|| AttrValue::from("2.85em"))]
119    pub height: AttrValue,
120    #[prop_or_default]
121    pub width: Option<AttrValue>,
122    #[prop_or_default]
123    pub children: Html,
124    #[prop_or_default]
125    pub parent: DependsOn,
126    #[prop_or_else(|| AttrValue::from("#6750A4"))]
127    pub background_color: AttrValue,
128    #[prop_or_else(|| AttrValue::from("#FFFFFF"))]
129    pub label_color: AttrValue, 
130    #[prop_or_else(|| AttrValue::from("20px"))]
131    pub border_radius: AttrValue,
132    #[prop_or_default]
133    pub has_icon: Option<AttrValue>,
134    #[prop_or_default]
135    pub trailing_icon: bool,
136    #[prop_or_default]
137    pub icon_style: Option<GIconStyle>,
138    #[prop_or_default]
139    pub autofocus: bool,
140    #[prop_or_else(|| false )]
141    pub fill: bool,
142    #[prop_or_else(|| AttrValue::from("300"))]
143    pub wght: AttrValue,
144    #[prop_or_else(|| AttrValue::from("100"))]
145    pub grade: AttrValue,
146    #[prop_or_else(|| AttrValue::from("24"))]
147    pub opsz: AttrValue,
148    #[prop_or_default]
149    pub dark_theame: bool,
150    #[prop_or_default]
151    pub disabled: bool,
152}
153
154pub struct GButton {
155    button: NodeRef,
156    only_icon: bool,
157    leading_icon: bool,
158    pointer_id: Option<i32>,
159    button_node: NodeRef,
160}
161
162impl Component for GButton {
163    type Message = Msg;
164
165    type Properties = GButtonProps;
166
167    fn create(ctx: &Context<Self>) -> Self {
168        let only_icon: bool = if ctx.props().label == AttrValue::default() {true} else {false};
169        let leading_icon: bool = if ctx.props().has_icon.is_none() { 
170            false 
171        } else if ctx.props().trailing_icon {
172            false
173        } else {
174            true
175        };
176        Self {
177            button: NodeRef::default(),
178            only_icon,
179            leading_icon,
180            pointer_id: None,
181            button_node: NodeRef::default(),
182        }
183    }
184
185    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
186        match msg {
187            Msg::OnPointerDown(event) => {
188                let onmouse = true;
189                let button = self.button.cast::<HtmlElement>().unwrap();
190                self.pointer_id = Some(event.pointer_id());
191                button.set_pointer_capture(self.pointer_id.unwrap()).unwrap();
192                let button_rect = button.get_bounding_client_rect();
193                let x = {
194                    let x = event.client_x() - button_rect.left().round() as i32;
195                    format!("{x}px")
196                };
197                let y = {
198                    let y = event.client_y() - button_rect.top().round() as i32;
199                    format!("{y}px")
200                };
201                if !ctx.props().disabled {
202                    ripple_effect(onmouse, &x, &y, button, &ctx.props().id);
203                }
204            },
205            Msg::OnKeyPress(event) => {
206                if event.key() == "Enter" {
207                    let onmouse = false;
208                    let button = self.button.cast::<HtmlElement>().unwrap();
209                    let x = {
210                        let x = button.offset_width() / 2;
211                        format!("{x}px")
212                    };
213                    let y = {
214                        let y = button.offset_height() / 2;
215                        format!("{y}px")
216                    };
217                    if !ctx.props().disabled {
218                        ripple_effect(onmouse, &x, &y, button, &ctx.props().id);
219                    }
220                    if let Some(onclick) = ctx.props().onclick.as_ref() {
221                        onclick.emit(PointerEvent::new("pointerup").expect("Key to Pointer fail"));
222                    }
223                }
224            },
225            Msg::OnPointerUp(event) => {
226                if self.pointer_id.is_some() {
227                    let g_span_ripple_selector = AttrValue::from(format!("span#g_init_span{}", ctx.props().id));
228                    if let Some(span) = self.button.cast::<HtmlElement>().unwrap().query_selector(&g_span_ripple_selector).unwrap() {
229                        span.remove()
230                    }
231                    self.button.cast::<HtmlElement>().unwrap().release_pointer_capture(self.pointer_id.expect("No button pointer id")).unwrap();
232                    if let Some(value) = ctx.props().onclick.as_ref() {
233                        value.emit(event)
234                    }
235                    self.pointer_id = None;
236                }
237            },
238        }
239        false
240    }
241
242    fn view(&self, ctx: &Context<Self>) -> Html {
243        let g_init = AttrValue::from(format!("g_init_{}", ctx.props().id));
244        let has_icon = if ctx.props().has_icon.is_some() { true } else { false };
245        let stylesheet = input_style(
246            &ctx.props().style,
247            &ctx.props().id,
248            self.only_icon,
249            &g_init,
250            ctx.props().font_size.clone(),
251            ctx.props().height.clone(),
252            &ctx.props().width,
253            &ctx.props().background_color,
254            ctx.props().label_color.clone(),
255            &ctx.props().outlined_border_color,
256            ctx.props().border_radius.clone(),
257            ctx.props().disabled,
258            has_icon,
259            ctx.props().trailing_icon,
260            ctx.props().dark_theame,
261            &ctx.props().parent,
262        );
263
264        let onpointerdown = ctx.link().callback(|event: PointerEvent| Msg::OnPointerDown(event));
265        let onkeydown = ctx.link().callback(|event: KeyboardEvent| Msg::OnKeyPress(event));
266        let onpointerup = ctx.link().callback(|event: PointerEvent| Msg::OnPointerUp(event));
267        html! {
268            <gbutton ref={&self.button_node} style="line-height: 0">
269                <stl class={stylesheet}>
270                    <div id={g_init}>
271                        <button 
272                            id={ctx.props().id.clone()} 
273                            type={ctx.props().button_type.clone()}
274                            ref={&self.button}
275                            class={&ctx.props().class}
276                            {onpointerdown}
277                            {onkeydown}
278                            {onpointerup}
279                            aria-label={ctx.props().id.clone()} 
280                            disabled={ctx.props().disabled}
281                            autofocus={ctx.props().autofocus}
282                        >
283                            {&ctx.props().label}
284                        </button>
285                        if ctx.props().has_icon.is_some() {
286                            <GIcon 
287                                icon={ctx.props().has_icon.clone().unwrap()}
288                                icon_style={ctx.props().icon_style.clone().unwrap()}
289                                fill={ctx.props().fill}
290                                wght={&ctx.props().wght}
291                                grade={&ctx.props().grade}
292                                opsz={&ctx.props().opsz}
293                                leading_icon={self.leading_icon}
294                                trailing_icon={ctx.props().trailing_icon}
295                            />
296                        }
297                        {ctx.props().children.clone()}
298                    </div>
299                </stl>
300            </gbutton>
301        }
302    }
303
304    fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
305        if first_render {
306            match ctx.props().parent {
307                DependsOn::GTextInput => {
308                    let button = self.button_node.cast::<HtmlElement>().unwrap();
309                    let input_height = button.parent_element().unwrap().first_element_child().unwrap().client_height() as f64;
310                    let button_height = button.query_selector("button").unwrap().unwrap().client_height() as f64;
311                    let icon_margin_top_and_side = (input_height - button_height) / 2.0 + 1.0;
312                    let button_align = if self.leading_icon {
313                        "left"
314                    } else {
315                        "right"
316                    };
317                    let css = format!(r#"
318                        display: block;
319                        position: absolute;  
320                        top: {icon_margin_top_and_side}px;
321                        {button_align}: 0.25em;
322                    "#);
323                    button.style().set_css_text(&css);
324                },
325                DependsOn::None => (),
326            }
327        }
328    }
329}
330
331fn ripple_effect(onmouse: bool, x: &str, y: &str, button: HtmlElement, id: &AttrValue) {
332    let span = button
333        .owner_document()
334        .unwrap()
335        .create_element("span")
336        .unwrap()
337        .dyn_into::<HtmlElement>()
338        .unwrap();
339    let g_span_ripple = AttrValue::from(format!("g_init_span{}", id));
340    span.set_id(&g_span_ripple);
341    span.style().set_property("left", &x).unwrap();
342    span.style().set_property("top", &y).unwrap();
343    
344    button.append_child(&span).unwrap();
345    if !onmouse {
346        spawn_local(async move {
347            TimeoutFuture::new(300).await;
348            span.remove()
349        })
350    }
351}