Skip to main content

zu/loading_button/
mod.rs

1// Copyright (c) 2024 Xu Shaohua <shaohua@biofan.org>. All rights reserved.
2// Use of this source is governed by Lesser General Public License
3// that can be found in the LICENSE file.
4
5mod color;
6mod position;
7mod size;
8mod variant;
9
10use yew::{
11    classes, function_component, html, AttrValue, Callback, Children, Classes, DragEvent,
12    FocusEvent, Html, KeyboardEvent, MouseEvent, Properties, TouchEvent,
13};
14
15use crate::button::Button;
16use crate::circular_progress::{CircularProgress, Size as CircularProgressSize};
17use crate::styles::button_variant::ButtonVariant;
18use crate::styles::color::Color;
19use crate::styles::size::Size;
20pub use position::Position;
21
22#[derive(Debug, Clone, PartialEq, Properties)]
23pub struct Props {
24    #[prop_or_default]
25    pub aria_label: AttrValue,
26
27    #[prop_or_default]
28    pub children: Children,
29
30    #[prop_or_default]
31    pub classes: Classes,
32
33    /// Default value is `Color::Default`.
34    #[prop_or(Color::Default)]
35    pub color: Color,
36
37    #[prop_or(false)]
38    pub disabled: bool,
39
40    #[prop_or(false)]
41    pub disable_elevation: bool,
42
43    #[prop_or(false)]
44    pub full_width: bool,
45
46    /// If `true`, the loading indicator is shown.
47    ///
48    /// Default is false.
49    #[prop_or(false)]
50    pub loading: bool,
51
52    /// Element placed before the children if the button is in loading state.
53    #[prop_or_default]
54    pub loading_indicator: Option<Html>,
55
56    #[prop_or(Position::Center)]
57    pub loading_position: Position,
58
59    #[prop_or_default]
60    pub start_icon: Option<Html>,
61
62    #[prop_or_default]
63    pub end_icon: Option<Html>,
64
65    /// Default is `Size::Medium`.
66    #[prop_or(Size::Medium)]
67    pub size: Size,
68
69    #[prop_or_default]
70    pub style: AttrValue,
71
72    #[prop_or_default]
73    pub tab_index: Option<i32>,
74
75    #[prop_or(ButtonVariant::Text)]
76    pub variant: ButtonVariant,
77
78    #[prop_or_default]
79    pub on_blur: Option<Callback<FocusEvent>>,
80
81    #[prop_or_default]
82    pub on_click: Option<Callback<MouseEvent>>,
83
84    #[prop_or_default]
85    pub on_context_menu: Option<Callback<MouseEvent>>,
86
87    #[prop_or_default]
88    pub on_drag_leave: Option<Callback<DragEvent>>,
89
90    #[prop_or_default]
91    pub on_focus: Option<Callback<FocusEvent>>,
92
93    /// Callback fired when the component is focused with a keyboard.
94    #[prop_or_default]
95    pub on_focus_visible: Option<Callback<FocusEvent>>,
96
97    #[prop_or_default]
98    pub on_key_down: Option<Callback<KeyboardEvent>>,
99
100    #[prop_or_default]
101    pub on_key_up: Option<Callback<KeyboardEvent>>,
102
103    #[prop_or_default]
104    pub on_mouse_down: Option<Callback<MouseEvent>>,
105
106    #[prop_or_default]
107    pub on_mouse_enter: Option<Callback<MouseEvent>>,
108
109    #[prop_or_default]
110    pub on_mouse_leave: Option<Callback<MouseEvent>>,
111
112    #[prop_or_default]
113    pub on_mouse_up: Option<Callback<MouseEvent>>,
114
115    #[prop_or_default]
116    pub on_touch_end: Option<Callback<TouchEvent>>,
117
118    #[prop_or_default]
119    pub on_touch_move: Option<Callback<TouchEvent>>,
120
121    #[prop_or_default]
122    pub on_touch_start: Option<Callback<TouchEvent>>,
123}
124
125fn create_indicator(
126    indicator: &Option<Html>,
127    position: Position,
128    button_size: Size,
129    button_variant: ButtonVariant,
130    full_width: bool,
131) -> Html {
132    let root_cls = classes!(
133        "ZuLoadingButton-loadingIndicator",
134        position.indicator_class(),
135        size::indicator_class(button_size),
136        variant::indicator_class(button_variant),
137        if full_width {
138            "ZuLoadingButton-loadingIndicatorFullWidth"
139        } else {
140            ""
141        }
142    );
143
144    if let Some(indicator) = indicator {
145        html! {
146            <span class={root_cls}>
147                {indicator.clone()}
148            </span>
149        }
150    } else {
151        html! {
152            <div class={root_cls}>
153                <CircularProgress color={Color::Inherit} size={CircularProgressSize::Num(16)} />
154            </div>
155        }
156    }
157}
158
159#[function_component(LoadingButton)]
160pub fn loading_button(props: &Props) -> Html {
161    if props.start_icon.is_some() && props.loading_position != Position::Start {
162        log::error!("The loading_position='start' should be used in combination with start_icon.");
163    }
164    if props.end_icon.is_some() && props.loading_position != Position::End {
165        log::error!("The loading_position='end' should be used in combination with end_icon.");
166    }
167
168    let root_cls = classes!(
169        "ZuLoadingButton-root",
170        if props.loading && props.loading_position == Position::Center {
171            "ZuLoadingButton-loading"
172        } else {
173            ""
174        },
175        if props.disable_elevation {
176            "ZuLoadingButton-disableElevation"
177        } else {
178            ""
179        },
180        if props.full_width {
181            "ZuLoadingButton-fullWidth"
182        } else {
183            ""
184        },
185        props.classes.clone(),
186    );
187
188    let indicator = if props.loading {
189        create_indicator(
190            &props.loading_indicator,
191            props.loading_position,
192            props.size,
193            props.variant,
194            props.full_width,
195        )
196    } else {
197        html! {}
198    };
199
200    html! {
201        <Button
202            classes={root_cls}
203            aria_label={&props.aria_label}
204            disabled={props.disabled || props.loading}
205            size={props.size}
206            start_icon={props.start_icon.clone()}
207            start_icon_classes={"ZuLoadingButton-startIconLoadingStart"}
208            end_icon={props.end_icon.clone()}
209            end_icon_classes={"ZuLoadingButton-endIconLoadingEnd"}
210            tab_index={props.tab_index}
211            on_blur={&props.on_blur}
212            on_click={&props.on_click}
213            on_context_menu={&props.on_context_menu}
214            on_drag_leave={&props.on_drag_leave}
215            on_focus={&props.on_focus}
216            on_focus_visible={&props.on_focus_visible}
217            on_key_down={&props.on_key_down}
218            on_key_up={&props.on_key_up}
219            on_mouse_down={&props.on_mouse_down}
220            on_mouse_enter={&props.on_mouse_enter}
221            on_mouse_leave={&props.on_mouse_leave}
222            on_mouse_up={&props.on_mouse_up}
223            on_touch_end={&props.on_touch_end}
224            on_touch_move={&props.on_touch_move}
225            on_touch_start={&props.on_touch_start}
226            >
227
228            if props.loading_position == Position::End {
229                {for props.children.iter()}
230                {indicator}
231            } else {
232                {indicator}
233                {for props.children.iter()}
234            }
235
236        </Button>
237    }
238}