orbital-base-components 0.1.1

Headless base primitives for Orbital — structure, semantics, and accessibility
Documentation
use std::sync::Arc;

use leptos::wasm_bindgen::JsCast;
use leptos::{ev, html, prelude::*};
use orbital_style::inject_style;
use web_sys::HtmlElement;

use super::styles::loading_bar_styles;
use crate::ComponentRef;

#[derive(Clone)]
pub(crate) struct LoadingBarRef {
    start: Arc<dyn Fn() + Send + Sync + 'static>,
    finish: Arc<dyn Fn() + Send + Sync + 'static>,
    error: Arc<dyn Fn() + Send + Sync + 'static>,
}

impl LoadingBarRef {
    pub fn start(&self) {
        (self.start)();
    }

    pub fn finish(&self) {
        (self.finish)();
    }

    pub fn error(&self) {
        (self.error)();
    }
}

fn set_style(el: &HtmlElement, property: &str, value: &str) {
    let _ = el.style().set_property(property, value);
}

fn set_style_important(el: &HtmlElement, property: &str, value: &str) {
    let _ = el
        .style()
        .set_property_with_priority(property, value, "important");
}

fn force_reflow(el: &HtmlElement) {
    let _ = el.offset_width();
}

fn freeze_then_fill(el: &HtmlElement, container: &HtmlElement) {
    let current = el.offset_width();
    let parent = container.offset_width();
    if parent > 0 && current > 0 {
        let pct = (current as f64 / parent as f64) * 100.0;
        set_style(el, "transition", "none");
        set_style(el, "max-width", &format!("{pct}%"));
        force_reflow(el);
    } else {
        set_style(el, "transition", "none");
        force_reflow(el);
    }
    set_style_important(el, "max-width", "100%");
    set_style_important(el, "width", "100%");
    set_style(el, "transition", "none");
    force_reflow(el);
}

#[component]
pub(crate) fn BaseLoadingBar(
    #[prop(optional)] comp_ref: ComponentRef<LoadingBarRef>,
) -> impl IntoView {
    inject_style("orbital-loading-bar", loading_bar_styles());

    let container_ref = NodeRef::<html::Div>::new();
    let loading_bar_ref = NodeRef::<html::Div>::new();

    let start = Arc::new(move || {
        let Some(container_el) = container_ref.get_untracked() else {
            return;
        };
        let Some(loading_bar_el) = loading_bar_ref.get_untracked() else {
            return;
        };
        let container_el: HtmlElement = container_el.unchecked_into();
        let loading_bar_el: HtmlElement = loading_bar_el.unchecked_into();

        let _ = container_el.style().set_property("display", "block");
        let _ = loading_bar_el.style().remove_property("background-color");
        set_style(&loading_bar_el, "transition", "none");
        set_style(&loading_bar_el, "max-width", "0");
        force_reflow(&loading_bar_el);
        set_style(&loading_bar_el, "transition", "max-width 4s linear");
        set_style(&loading_bar_el, "max-width", "80%");
    });

    let on_transitionend = move |_: ev::TransitionEvent| {};

    let finish = Arc::new(move || {
        let Some(container_el) = container_ref.get_untracked() else {
            return;
        };
        let Some(loading_bar_el) = loading_bar_ref.get_untracked() else {
            return;
        };
        let container_el: HtmlElement = container_el.unchecked_into();
        let loading_bar_el: HtmlElement = loading_bar_el.unchecked_into();

        let _ = container_el.style().set_property("display", "block");
        freeze_then_fill(&loading_bar_el, &container_el);
    });

    let error = Arc::new(move || {
        let Some(container_el) = container_ref.get_untracked() else {
            return;
        };
        let Some(loading_bar_el) = loading_bar_ref.get_untracked() else {
            return;
        };
        let container_el: HtmlElement = container_el.unchecked_into();
        let loading_bar_el: HtmlElement = loading_bar_el.unchecked_into();

        let _ = container_el.style().set_property("display", "block");
        if loading_bar_el
            .style()
            .get_property_value("max-width")
            .ok()
            .as_deref()
            != Some("100%")
        {
            set_style(&loading_bar_el, "transition", "none");
            set_style(&loading_bar_el, "max-width", "0");
            force_reflow(&loading_bar_el);
        }
        set_style(
            &loading_bar_el,
            "background-color",
            "var(--orb-color-status-danger-fg)",
        );
        freeze_then_fill(&loading_bar_el, &container_el);
    });

    comp_ref.load(LoadingBarRef {
        start,
        finish,
        error,
    });

    view! {
        <div
            class="orbital-loading-bar-container"
            style="display: none"
            node_ref=container_ref
        >
            <div
                class="orbital-loading-bar"
                node_ref=loading_bar_ref
                on:transitionend=on_transitionend
            ></div>
        </div>
    }
}