orbital-base-components 0.1.1

Headless base primitives for Orbital — structure, semantics, and accessibility
Documentation
mod resize;

use leptos::{either::Either, ev, html, leptos_dom::helpers::WindowListenerHandle, prelude::*};

use resize::{clamp_column_width, column_width_style};

#[component]
pub fn BaseTableHeaderCell(
    #[prop(optional, into)] class: MaybeProp<String>,
    #[prop(optional, into)] style: MaybeProp<String>,
    #[prop(optional, default = false)] resizable: bool,
    #[prop(default = None)] min_width: Option<f64>,
    #[prop(default = None)] max_width: Option<f64>,
    #[prop(default = None)] colspan: Option<u32>,
    #[prop(default = None)] rowspan: Option<u32>,
    #[prop(default = None)] on_resize_end: Option<Callback<f64, ()>>,
    #[prop(default = None)] on_autosize: Option<Callback<(), ()>>,
    #[prop(optional)] children: Option<Children>,
) -> impl IntoView {
    let th_ref = NodeRef::<html::Th>::new();
    let pointer_down_x = RwSignal::new(0.0);
    let pointer_down_th_width = RwSignal::new(0.0);
    let last_width = RwSignal::new(0.0);
    let resize_width = RwSignal::new(None::<f64>);
    let listeners = StoredValue::new(Vec::<WindowListenerHandle>::new());

    let th_style = Signal::derive(move || {
        if let Some(width) = resize_width.get() {
            column_width_style(width, min_width, max_width)
        } else {
            style.get().unwrap_or_default()
        }
    });

    let begin_resize = move |client_x: f64| {
        pointer_down_x.set(client_x);
        let Some(th_el) = th_ref.get_untracked() else {
            return;
        };
        let width = th_el.get_bounding_client_rect().width();
        let width = clamp_column_width(width, min_width, max_width);
        if width <= 0.0 {
            return;
        }
        pointer_down_th_width.set(width);
        last_width.set(width);
        resize_width.set(Some(width));

        let on_pointer_move = window_event_listener(ev::pointermove, move |e: ev::PointerEvent| {
            let delta = f64::from(e.client_x()) - pointer_down_x.get_untracked();
            let new_width = clamp_column_width(
                pointer_down_th_width.get_untracked() + delta,
                min_width,
                max_width,
            );
            last_width.set(new_width);
            resize_width.set(Some(new_width));
        });
        let on_mouse_move = window_event_listener(ev::mousemove, move |e: ev::MouseEvent| {
            let delta = f64::from(e.client_x()) - pointer_down_x.get_untracked();
            let new_width = clamp_column_width(
                pointer_down_th_width.get_untracked() + delta,
                min_width,
                max_width,
            );
            last_width.set(new_width);
            resize_width.set(Some(new_width));
        });
        let on_pointer_up = window_event_listener(ev::pointerup, move |_| {
            let width = last_width.get_untracked();
            if let Some(cb) = on_resize_end {
                cb.run(width);
            }
            resize_width.set(Some(width));
            listeners.update_value(|value| {
                for handle in value.drain(..) {
                    handle.remove();
                }
            });
        });
        let on_mouse_up = window_event_listener(ev::mouseup, move |_| {
            let width = last_width.get_untracked();
            if let Some(cb) = on_resize_end {
                cb.run(width);
            }
            resize_width.set(Some(width));
            listeners.update_value(|value| {
                for handle in value.drain(..) {
                    handle.remove();
                }
            });
        });
        listeners.update_value(|value| {
            value.push(on_pointer_move);
            value.push(on_mouse_move);
            value.push(on_pointer_up);
            value.push(on_mouse_up);
        });
    };

    let on_pointer_down = move |e: ev::PointerEvent| {
        if !resizable {
            return;
        }
        e.prevent_default();
        e.stop_propagation();
        begin_resize(f64::from(e.client_x()));
    };

    let on_mouse_down = move |e: ev::MouseEvent| {
        if !resizable {
            return;
        }
        e.prevent_default();
        e.stop_propagation();
        begin_resize(f64::from(e.client_x()));
    };

    let on_dblclick = move |_| {
        if let Some(cb) = on_autosize {
            cb.run(());
        }
    };

    on_cleanup(move || {
        listeners.update_value(|value| {
            for handle in value.drain(..) {
                handle.remove();
            }
        });
    });

    view! {
        <th
            class=move || {
                let extra = class.get().unwrap_or_default();
                if extra.is_empty() {
                    "orbital-table-header-cell".to_string()
                } else {
                    format!("orbital-table-header-cell {extra}")
                }
            }
            style=move || th_style.get()
            colspan=colspan
            rowspan=rowspan
            node_ref=th_ref
        >
            <div class="orbital-table-header-cell__button" role="presentation">
                {if let Some(children) = children {
                    Either::Left(children())
                } else {
                    Either::Right(())
                }}
            </div>
            {if resizable {
                Either::Left(view! {
                    <span
                        class="orbital-table-header-cell__aside"
                        on:pointerdown=on_pointer_down
                        on:mousedown=on_mouse_down
                        on:dblclick=on_dblclick
                    >
                        <div
                            class="orbital-table-resize-handle"
                            role="separator"
                            aria-hidden="true"
                            on:pointerdown=on_pointer_down
                            on:mousedown=on_mouse_down
                            on:dblclick=on_dblclick
                        ></div>
                    </span>
                })
            } else {
                Either::Right(())
            }}
        </th>
    }
}