radix_leptos_primitives/components/
progress.rs

1use leptos::children::Children;
2use leptos::prelude::*;
3use crate::utils::{merge_optional_classes, generate_id};
4
5/// Progress component with proper accessibility and styling variants
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum ProgressVariant {
8    Default,
9    Destructive,
10    Success,
11    Warning,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum ProgressSize {
16    Default,
17    Sm,
18    Lg,
19}
20
21impl ProgressVariant {
22    pub fn as_str(&self) -> &'static str {
23        match self {
24            ProgressVariant::Default => "default",
25            ProgressVariant::Destructive => "destructive",
26            ProgressVariant::Success => "success",
27            ProgressVariant::Warning => "warning",
28        }
29    }
30}
31
32impl ProgressSize {
33    pub fn as_str(&self) -> &'static str {
34        match self {
35            ProgressSize::Default => "default",
36            ProgressSize::Sm => "sm",
37            ProgressSize::Lg => "lg",
38        }
39    }
40}
41
42
43/// Progress root component
44#[component]
45pub fn Progress(
46    /// Current progress value (0-100)
47    #[prop(optional, default = 0.0)]
48    value: f64,
49    /// Maximum value
50    #[prop(optional, default = 100.0)]
51    max: f64,
52    /// Whether the progress is indeterminate
53    #[prop(optional, default = false)]
54    indeterminate: bool,
55    /// Progress styling variant
56    #[prop(optional, default = ProgressVariant::Default)]
57    variant: ProgressVariant,
58    /// Progress size
59    #[prop(optional, default = ProgressSize::Default)]
60    size: ProgressSize,
61    /// CSS classes
62    #[prop(optional)]
63    class: Option<String>,
64    /// CSS styles
65    #[prop(optional)]
66    style: Option<String>,
67    /// Child content
68    children: Children,
69) -> impl IntoView {
70    let __progress_id = generate_id("progress");
71
72    // Build data attributes for styling
73    let data_variant = variant.as_str();
74    let data_size = size.as_str();
75
76    // Merge classes with data attributes for CSS targeting
77    let base_classes = "radix-progress";
78    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
79        .unwrap_or_else(|| base_classes.to_string());
80
81    // Calculate percentage for visual representation
82    let percentage = if max > 0.0 && !indeterminate {
83        (value / max * 100.0).clamp(0.0, 100.0)
84    } else {
85        0.0
86    };
87
88    view! {
89        <div
90            class=combined_class
91            style=style
92            data-variant=data_variant
93            data-size=data_size
94            data-value=value
95            data-max=max
96            data-indeterminate=indeterminate
97            data-percentage=percentage
98            role="progressbar"
99            aria-valuemin=0.0
100            aria-valuemax=max
101        >
102        </div>
103    }
104}
105
106/// Progress Track component
107#[component]
108pub fn ProgressTrack(
109    /// CSS classes
110    #[prop(optional)]
111    class: Option<String>,
112    /// CSS styles
113    #[prop(optional)]
114    style: Option<String>,
115) -> impl IntoView {
116    let base_classes = "radix-progress-track";
117    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
118        .unwrap_or_else(|| base_classes.to_string());
119
120    view! {
121        <div
122            class=combined_class
123            style=style
124        >
125        </div>
126    }
127}
128
129/// Progress Indicator component
130#[component]
131pub fn ProgressIndicator(
132    /// CSS classes
133    #[prop(optional)]
134    class: Option<String>,
135    /// CSS styles
136    #[prop(optional)]
137    style: Option<String>,
138) -> impl IntoView {
139    let base_classes = "radix-progress-indicator";
140    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
141        .unwrap_or_else(|| base_classes.to_string());
142
143    view! {
144        <div
145            class=combined_class
146            style=style
147        >
148        </div>
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use crate::{ProgressSize, ProgressVariant};
155
156    use proptest::prelude::*;
157use crate::utils::{merge_optional_classes, generate_id};
158
159    // 1. Basic Rendering Tests
160    #[test]
161    fn test_progress_variants() {
162        run_test(|| {
163            let variants = [
164                ProgressVariant::Default,
165                ProgressVariant::Destructive,
166                ProgressVariant::Success,
167                ProgressVariant::Warning,
168            ];
169
170            for variant in variants {
171                assert!(!variant.as_str().is_empty());
172            }
173        });
174    }
175
176    #[test]
177    fn test_progress_sizes() {
178        run_test(|| {
179            let sizes = [ProgressSize::Default, ProgressSize::Sm, ProgressSize::Lg];
180
181            for size in sizes {
182                assert!(!size.as_str().is_empty());
183            }
184        });
185    }
186
187    // 2. Props Validation Tests
188    #[test]
189    fn test_progress_default_values() {
190        run_test(|| {
191            let value = 0.0;
192            let _max = 100.0;
193            let indeterminate = false;
194            let variant = ProgressVariant::Default;
195            let size = ProgressSize::Default;
196
197            assert_eq!(value, 0.0);
198            assert_eq!(_max, 100.0);
199            assert!(!indeterminate);
200            assert_eq!(variant, ProgressVariant::Default);
201            assert_eq!(size, ProgressSize::Default);
202        });
203    }
204
205    #[test]
206    fn test_progress_custom_values() {
207        run_test(|| {
208            let value = 50.0;
209            let _max = 200.0;
210            let indeterminate = false;
211            let variant = ProgressVariant::Success;
212            let size = ProgressSize::Lg;
213
214            assert_eq!(value, 50.0);
215            assert_eq!(_max, 200.0);
216            assert!(!indeterminate);
217            assert_eq!(variant, ProgressVariant::Success);
218            assert_eq!(size, ProgressSize::Lg);
219        });
220    }
221
222    #[test]
223    fn test_progressindeterminate_state() {
224        run_test(|| {
225            let value = 0.0;
226            let _max = 100.0;
227            let indeterminate = true;
228            let variant = ProgressVariant::Warning;
229            let size = ProgressSize::Sm;
230
231            assert_eq!(value, 0.0);
232            assert_eq!(_max, 100.0);
233            assert!(indeterminate);
234            assert_eq!(variant, ProgressVariant::Warning);
235            assert_eq!(size, ProgressSize::Sm);
236        });
237    }
238
239    // 3. State Management Tests
240    #[test]
241    fn test_progress_value_calculation() {
242        run_test(|| {
243            let value = 50.0;
244            let _max = 100.0;
245            let indeterminate = false;
246
247            // Test percentage calculation
248            let percentage = if _max > 0.0 && !indeterminate {
249                (value / _max * 100.0f64).clamp(0.0f64, 100.0f64)
250            } else {
251                0.0
252            };
253
254            assert_eq!(percentage, 50.0);
255        });
256    }
257
258    #[test]
259    fn test_progress_value_bounds() {
260        run_test(|| {
261            let _max = 100.0;
262            let indeterminate = false;
263
264            // Test value clamping
265            let value_below_min = -10.0;
266            let value_above_max = 150.0;
267            let value_in_range = 50.0;
268
269            let percentage_below = if _max > 0.0 && !indeterminate {
270                (value_below_min / _max * 100.0f64).clamp(0.0f64, 100.0f64)
271            } else {
272                0.0
273            };
274
275            let percentage_above = if _max > 0.0 && !indeterminate {
276                (value_above_max / _max * 100.0f64).clamp(0.0f64, 100.0f64)
277            } else {
278                0.0
279            };
280
281            let percentage_in_range = if _max > 0.0 && !indeterminate {
282                (value_in_range / _max * 100.0f64).clamp(0.0f64, 100.0f64)
283            } else {
284                0.0
285            };
286
287            assert_eq!(percentage_below, 0.0);
288            assert_eq!(percentage_above, 100.0);
289            assert_eq!(percentage_in_range, 50.0);
290        });
291    }
292
293    // 4. Indeterminate State Tests
294    #[test]
295    fn test_progressindeterminate_calculation() {
296        run_test(|| {
297            let value = 50.0;
298            let _max = 100.0;
299            let indeterminate = true;
300
301            // Test percentage calculation for indeterminate state
302            let percentage = if _max > 0.0 && !indeterminate {
303                (value / _max * 100.0f64).clamp(0.0f64, 100.0f64)
304            } else {
305                0.0
306            };
307
308            assert_eq!(percentage, 0.0);
309        });
310    }
311
312    #[test]
313    fn test_progressindeterminate_aria() {
314        run_test(|| {
315            let value = 50.0;
316            let _max = 100.0;
317            let indeterminate = true;
318
319            // Test ARIA attributes for indeterminate state
320            assert!(indeterminate);
321        });
322    }
323
324    // 5. Accessibility Tests
325    #[test]
326    fn test_progress_accessibility() {
327        run_test(|| {
328            let role = "progressbar";
329            let aria_valuemin = 0.0;
330            let aria_valuemax = 100.0;
331            let aria_valuenow = Some(50.0);
332            let aria_label = "Progress";
333
334            assert_eq!(role, "progressbar");
335            assert_eq!(aria_valuemin, 0.0);
336            assert_eq!(aria_valuemax, 100.0);
337            assert_eq!(aria_valuenow, Some(50.0));
338            assert_eq!(aria_label, "Progress");
339        });
340    }
341
342    // 6. Edge Case Tests
343    #[test]
344    fn test_progress_edge_cases() {
345        run_test(|| {
346            // Test zero max value
347            let value = 50.0;
348            let _max = 0.0;
349            let indeterminate = false;
350
351            let percentage = if _max > 0.0 && !indeterminate {
352                (value / _max * 100.0f64).clamp(0.0f64, 100.0f64)
353            } else {
354                0.0
355            };
356
357            assert_eq!(percentage, 0.0);
358        });
359    }
360
361    #[test]
362    fn test_progress_negative_values() {
363        run_test(|| {
364            let value = -25.0;
365            let _max = 100.0;
366            let indeterminate = false;
367
368            let percentage = if _max > 0.0 && !indeterminate {
369                (value / _max * 100.0f64).clamp(0.0f64, 100.0f64)
370            } else {
371                0.0
372            };
373
374            assert_eq!(percentage, 0.0);
375        });
376    }
377
378    #[test]
379    fn test_progress_completion_state() {
380        run_test(|| {
381            let value = 100.0;
382            let _max = 100.0;
383            let indeterminate = false;
384
385            let percentage = if _max > 0.0 && !indeterminate {
386                (value / _max * 100.0f64).clamp(0.0f64, 100.0f64)
387            } else {
388                0.0
389            };
390
391            assert_eq!(percentage, 100.0);
392        });
393    }
394
395    // 7. Property-Based Tests
396    proptest! {
397        #[test]
398        fn test_progress_properties(
399            variant in prop::sample::select(&[
400                ProgressVariant::Default,
401                ProgressVariant::Destructive,
402                ProgressVariant::Success,
403                ProgressVariant::Warning,
404            ]),
405            size in prop::sample::select(&[
406                ProgressSize::Default,
407                ProgressSize::Sm,
408                ProgressSize::Lg,
409            ]),
410            value in -100.0..1000.0f64,
411            __max in 0.1..1000.0f64,
412            indeterminate in prop::bool::ANY
413        ) {
414            assert!(!variant.as_str().is_empty());
415            assert!(!size.as_str().is_empty());
416
417            // Test that indeterminate property is properly typed
418            assert!(matches!(indeterminate, true | false));
419            assert!(__max > 0.0);
420
421            // Test percentage calculation
422            let percentage = if __max > 0.0 && !indeterminate {
423                (value / __max * 100.0f64).clamp(0.0f64, 100.0f64)
424            } else {
425                0.0
426            };
427
428            assert!((0.0..=100.0).contains(&percentage));
429
430            // Test ARIA attributes
431            // ARIA attributes are properly set in the component
432        }
433    }
434
435    // Helper function for running tests
436    fn run_test<F>(f: F)
437    where
438        F: FnOnce(),
439    {
440        f();
441    }
442}