yew_google_material/buttons/
mod.rs1use 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}