Skip to main content

hypersig_ui/
progress.rs

1//! Progress bars and loading placeholders.
2
3use hypersig::html::{El, div};
4use hypersig::style::value::{
5    AnimationIterationCount, BorderRadius, Opacity, Overflow, ms, pct, px, rem,
6};
7use hypersig::style::{Keyframes, Media, Property, Style};
8use hypersig::{Expr, IntoExpr};
9
10use crate::look;
11use crate::token;
12
13/// A bar filled up to `percent`, a value from 0 to 100, named by `label`.
14///
15/// It is a `role="progressbar"` with `aria-valuenow` kept equal to the value,
16/// so a screen reader reads the label and the percentage. A value below 0 or
17/// above 100 is drawn and announced as 0 or 100.
18///
19/// ```
20/// use hypersig::Signals;
21/// use hypersig_ui::progress;
22/// use serde::{Deserialize, Serialize};
23///
24/// #[derive(Signals, Serialize, Deserialize)]
25/// struct Upload {
26///     percent: i32,
27/// }
28///
29/// let bar = progress::<Upload>("Uploading report.pdf", Upload::percent());
30/// assert!(bar.render().contains(r#"role="progressbar""#));
31/// ```
32#[must_use]
33pub fn progress<S>(label: impl Into<String>, percent: impl IntoExpr<S, i32>) -> El<S> {
34    let percent: Expr<S, i32> = percent.into_expr().max(0).min(100);
35    div()
36        .attr("role", "progressbar")
37        .attr("aria-label", label)
38        .attr("aria-valuemin", "0")
39        .attr("aria-valuemax", "100")
40        .bind_attr("aria-valuenow", percent.clone().to_text())
41        .css(
42            Style::new()
43                .position(hypersig::style::value::Position::Relative)
44                .height(rem(0.5))
45                .width(pct(100))
46                .overflow(Overflow::Hidden)
47                .border_radius(BorderRadius::from(px(999)))
48                .background_color(token::primary().var().opacity(20)),
49        )
50        .child(
51            div()
52                .css(
53                    Style::new()
54                        .height(pct(100))
55                        .background_color(token::primary().var())
56                        .merge(look::transition())
57                        .transition_property(hypersig::style::value::raw("width")),
58                )
59                .bind_style(Property::Width, percent.to_text().concat("%")),
60        )
61}
62
63/// A pulsing block in [`token::accent`] that stands in for content still
64/// loading, hidden from assistive technology.
65///
66/// Size it with `.css`. Tell a screen reader that the content is loading
67/// elsewhere, such as with `aria-busy` on the region it fills.
68///
69/// ```
70/// use hypersig::Signals;
71/// use hypersig::style::Style;
72/// use hypersig::style::value::rem;
73/// use hypersig_ui::skeleton;
74/// use serde::{Deserialize, Serialize};
75///
76/// #[derive(Signals, Serialize, Deserialize)]
77/// struct App {}
78///
79/// let line = skeleton::<App>().css(Style::new().height(rem(1)).width(rem(12)));
80/// assert!(line.render().contains(r#"aria-hidden="true""#));
81/// ```
82#[must_use]
83pub fn skeleton<S>() -> El<S> {
84    let pulse = Keyframes::new("hs-ui-pulse").at(50, Style::new().opacity(Opacity::Number(0.5)));
85    div().attr("aria-hidden", "true").css(
86        Style::new()
87            .border_radius(px(6))
88            .background_color(token::accent().var())
89            .keyframes(&pulse)
90            .animation_duration(ms(2000))
91            .animation_iteration_count(AnimationIterationCount::Infinite)
92            .on(
93                Media::ReducedMotion,
94                Style::new().animation_iteration_count(AnimationIterationCount::Number(0.0)),
95            ),
96    )
97}