patternfly_yew/components/
progress.rs

1//! Progress bar
2
3use crate::core::{AsClasses, ExtendClasses};
4use crate::icon::Icon;
5use crate::prelude::*;
6use std::ops::Range;
7use yew::prelude::*;
8use yew::virtual_dom::VChild;
9
10#[derive(Copy, Clone, Eq, PartialEq, Default)]
11pub enum ProgressValueFormat {
12    #[default]
13    Percentage,
14    Decimal(usize),
15    Integer,
16    Raw,
17}
18
19impl ProgressValueFormat {
20    pub fn format(&self, value: f64) -> String {
21        match self {
22            Self::Percentage => format!("{:.0}%", value * 100f64),
23            Self::Decimal(precision) => format!("{value:.precision$}"),
24            Self::Raw => format!("{value}"),
25            Self::Integer => format!("{:.0}", value),
26        }
27    }
28}
29
30#[derive(Copy, Clone, Eq, PartialEq, Default)]
31pub enum ProgressSize {
32    #[default]
33    Default,
34    Small,
35    Large,
36}
37
38impl AsClasses for ProgressSize {
39    fn extend_classes(&self, classes: &mut Classes) {
40        match self {
41            Self::Default => {}
42            Self::Small => classes.push(classes!("pf-m-sm")),
43            Self::Large => classes.push(classes!("pf-m-lg")),
44        }
45    }
46}
47
48#[derive(Copy, Clone, Eq, PartialEq, Default)]
49pub enum ProgressMeasureLocation {
50    #[default]
51    Default,
52    Inside,
53    Outside,
54    None,
55}
56
57impl AsClasses for ProgressMeasureLocation {
58    fn extend_classes(&self, classes: &mut Classes) {
59        match self {
60            Self::Default | Self::None => {}
61            Self::Outside => classes.push(classes!("pf-m-outside")),
62            Self::Inside => classes.push(classes!("pf-m-inside")),
63        }
64    }
65}
66
67#[derive(Copy, Clone, Eq, PartialEq, Default)]
68pub enum ProgressVariant {
69    #[default]
70    Default,
71    Success,
72    Warning,
73    Danger,
74}
75
76impl AsClasses for ProgressVariant {
77    fn extend_classes(&self, classes: &mut Classes) {
78        match self {
79            Self::Default => {}
80            Self::Success => classes.push(classes!("pf-m-success")),
81            Self::Warning => classes.push(classes!("pf-m-warning")),
82            Self::Danger => classes.push(classes!("pf-m-danger")),
83        }
84    }
85}
86
87impl ProgressVariant {
88    pub fn icon(&self) -> Option<Icon> {
89        match self {
90            Self::Default => None,
91            Self::Success => Some(Icon::CheckCircle),
92            Self::Warning => Some(Icon::ExclamationTriangle),
93            Self::Danger => Some(Icon::TimesCircle),
94        }
95    }
96}
97
98/// Properties for [`Progress`]
99#[derive(PartialEq, Properties)]
100pub struct ProgressProperties {
101    /// The value
102    pub value: f64,
103
104    #[prop_or_default]
105    pub format: ProgressValueFormat,
106
107    /// The possible value range
108    #[prop_or(0f64..1f64)]
109    pub range: Range<f64>,
110
111    #[prop_or_default]
112    pub description: OptionalHtml,
113
114    /// Base ID of the element
115    #[prop_or_default]
116    pub id: Option<AttrValue>,
117
118    #[prop_or_default]
119    pub size: ProgressSize,
120
121    /// Location of the measurement
122    #[prop_or_default]
123    pub location: ProgressMeasureLocation,
124
125    /// Truncate the description, when necessary
126    #[prop_or_default]
127    pub truncate: bool,
128
129    #[prop_or_default]
130    pub variant: ProgressVariant,
131
132    /// Always suppress the icon.
133    #[prop_or_default]
134    pub no_icon: bool,
135
136    /// Add some helper text.
137    #[prop_or_default]
138    pub helper_text: Option<VChild<HelperText>>,
139
140    /// Override the text used to represent the value.
141    #[prop_or_default]
142    pub value_text: Option<String>,
143
144    /// Additional classes for the main element.
145    #[prop_or_default]
146    pub class: Classes,
147
148    /// Additional classes for the measure element
149    #[prop_or_default]
150    pub measure_class: Classes,
151
152    /// Additional CSS style
153    #[prop_or_default]
154    pub style: Option<AttrValue>,
155}
156
157/// Progress component
158///
159/// > A **progress bar** informs users about the completion status of an ongoing process or task.
160///
161/// See: <https://www.patternfly.org/components/progress>
162///
163/// ## Values
164///
165/// By default, a progress bar work with percent, provided as a value between 0 and 1 (inclusive).
166/// Its value will be rendered as "0%" to "100%", with no decimal places. This behavior can be
167/// tweaked or fully overridden.
168///
169/// ## Properties
170///
171/// Defined by [`ProgressProperties`].
172#[function_component(Progress)]
173pub fn progress(props: &ProgressProperties) -> Html {
174    let id = use_id(props.id.clone());
175    let desc_id = use_suffixed_id(&id, "-description");
176
177    let percentage =
178        ((props.value / (props.range.end - props.range.start)) * 100f64).clamp(0f64, 100f64);
179    let style = format!("width: {percentage:.0}%;");
180
181    let mut class = classes!("pf-v5-c-progress");
182    class.extend_from(&props.size);
183    class.extend_from(&props.location);
184    class.extend_from(&props.variant);
185    if props.description.is_none() {
186        class.extend(classes!("pf-m-singleline"));
187    }
188    class.extend(&props.class);
189
190    let measure_class = classes!("pf-v5-c-progress__measure", props.measure_class.clone());
191    let mut measure = match props.location {
192        ProgressMeasureLocation::None => None,
193        _ => {
194            let measure = props
195                .value_text
196                .clone()
197                .unwrap_or_else(|| props.format.format(props.value));
198            Some(html!(<span class={measure_class}> { measure } </span>))
199        }
200    };
201
202    let mut description_class = classes!("pf-v5-c-progress__description");
203    if props.truncate {
204        description_class.push(classes!("pf-m-truncate"));
205    }
206
207    let icon = match props.no_icon {
208        true => None,
209        false => props.variant.icon(),
210    }
211    .map(|icon| {
212        html!(
213            <span class="pf-v5-c-progress__status-icon">
214                { icon }
215            </span>
216        )
217    });
218
219    html!(
220        <div {class} {id} style={props.style.clone()}>
221            if let Some(description) = &props.description.0 {
222                <div
223                    class={description_class}
224                    id={desc_id.clone()}
225                >
226                    { description.clone() }
227                </div>
228            }
229            <div class="pf-v5-c-progress__status" aria-hidden="true">
230                if matches!(props.location, ProgressMeasureLocation::Default | ProgressMeasureLocation::Outside) {
231                    { measure.take() }
232                }
233                { icon }
234            </div>
235            <div
236                class="pf-v5-c-progress__bar"
237                role="progressbar"
238                aria-valuemin={ props.range.start.to_string() }
239                aria-valuemax={ props.range.end.to_string() }
240                aria-valuenow={ props.value.to_string() }
241                aria-labelledby={desc_id}
242            >
243                <div class="pf-v5-c-progress__indicator" {style}>
244                    if matches!(props.location, ProgressMeasureLocation::Inside) {
245                        { measure }
246                    }
247                </div>
248            </div>
249            if let Some(helper_text) = &props.helper_text {
250                <div class="pf-v5-c-progress__helper-text">
251                    { helper_text.clone() }
252                </div>
253            }
254        </div>
255    )
256}