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}