orbital-base-components 0.1.1

Headless base primitives for Orbital — structure, semantics, and accessibility
Documentation
use ::wasm_bindgen::{prelude::Closure, JsCast};
use leptos::ev;
use web_sys::EventTarget;

pub fn add_event_listener<E>(
    target: impl Into<EventTarget>,
    event: E,
    cb: impl Fn(E::EventType) + 'static,
) -> EventListenerHandle
where
    E: ev::EventDescriptor + 'static,
    E::EventType: JsCast,
{
    add_event_listener_untyped(target, &event.name(), move |e| {
        cb(e.unchecked_into::<E::EventType>())
    })
}

pub fn add_event_listener_capture<E>(
    target: impl Into<EventTarget>,
    event: E,
    cb: impl Fn(E::EventType) + 'static,
) -> EventListenerHandle
where
    E: ev::EventDescriptor + 'static,
    E::EventType: JsCast,
{
    add_event_listener_untyped_capture(target, &event.name(), move |e| {
        cb(e.unchecked_into::<E::EventType>())
    })
}

pub struct EventListenerHandle(Box<dyn FnOnce() + Send + Sync>);

impl std::fmt::Debug for EventListenerHandle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("EventListenerHandle").finish()
    }
}

impl EventListenerHandle {
    pub fn remove(self) {
        (self.0)();
    }
}

fn add_event_listener_untyped(
    target: impl Into<EventTarget>,
    event_name: &str,
    cb: impl Fn(web_sys::Event) + 'static,
) -> EventListenerHandle {
    fn wel(
        target: EventTarget,
        cb: Box<dyn FnMut(web_sys::Event)>,
        event_name: &str,
    ) -> EventListenerHandle {
        let cb = Closure::wrap(cb);
        _ = target.add_event_listener_with_callback(event_name, cb.as_ref().unchecked_ref());

        EventListenerHandle({
            let event_name = event_name.to_string();
            let cb = send_wrapper::SendWrapper::new(cb);
            let target = send_wrapper::SendWrapper::new(target);
            Box::new(move || {
                let _ = target
                    .remove_event_listener_with_callback(&event_name, cb.as_ref().unchecked_ref());
            })
        })
    }

    wel(target.into(), Box::new(cb), event_name)
}

fn add_event_listener_untyped_capture(
    target: impl Into<EventTarget>,
    event_name: &str,
    cb: impl Fn(web_sys::Event) + 'static,
) -> EventListenerHandle {
    fn wel(
        target: EventTarget,
        cb: Box<dyn FnMut(web_sys::Event)>,
        event_name: &str,
    ) -> EventListenerHandle {
        let cb = Closure::wrap(cb);
        _ = target.add_event_listener_with_callback_and_bool(
            event_name,
            cb.as_ref().unchecked_ref(),
            true,
        );

        EventListenerHandle({
            let event_name = event_name.to_string();
            let cb = send_wrapper::SendWrapper::new(cb);
            let target = send_wrapper::SendWrapper::new(target);
            Box::new(move || {
                let _ = target.remove_event_listener_with_callback_and_bool(
                    &event_name,
                    cb.as_ref().unchecked_ref(),
                    true,
                );
            })
        })
    }

    wel(target.into(), Box::new(cb), event_name)
}

pub fn get_scroll_parent_node(node: &web_sys::Node) -> Option<web_sys::Node> {
    use leptos::prelude::*;
    use wasm_bindgen::JsCast;

    let parent_node = node.parent_node()?;

    let node_type = parent_node.node_type();
    if node_type == web_sys::Node::ELEMENT_NODE {
        let el = parent_node.clone().dyn_into::<web_sys::Element>().unwrap();
        if let Some((overflow, overflow_x, overflow_y)) = get_overflow(&el) {
            let overflow = format!("{overflow}{overflow_x}{overflow_y}");
            if overflow.contains("auto")
                || overflow.contains("scroll")
                || overflow.contains("overlay")
            {
                return Some(parent_node);
            }
        }
    } else if node_type == web_sys::Node::DOCUMENT_NODE {
        return Some(document().into());
    }

    get_scroll_parent_node(&parent_node)
}

fn get_overflow(parent_element: &web_sys::Element) -> Option<(String, String, String)> {
    use leptos::prelude::*;

    let Ok(Some(css_style_declaration)) = window().get_computed_style(parent_element) else {
        return None;
    };
    let Ok(overflow) = css_style_declaration.get_property_value("overflow") else {
        return None;
    };
    let Ok(overflow_x) = css_style_declaration.get_property_value("overflowX") else {
        return None;
    };
    let Ok(overflow_y) = css_style_declaration.get_property_value("overflowY") else {
        return None;
    };
    Some((overflow, overflow_x, overflow_y))
}

pub fn on_click_outside<EF, CF>(els: EF, on_click: CF)
where
    EF: Fn() -> Option<Vec<web_sys::Element>> + 'static,
    CF: Fn() + 'static,
{
    #[cfg(any(feature = "hydrate", not(feature = "ssr")))]
    {
        use leptos::prelude::*;
        use std::rc::Rc;

        let els = Rc::new(els);
        let on_click = Rc::new(on_click);

        let handle_pointer = {
            let els = els.clone();
            let on_click = on_click.clone();
            window_event_listener(::leptos::ev::pointerdown, move |ev| {
                dispatch_outside_click(&ev, els.as_ref(), on_click.as_ref());
            })
        };
        let handle_mouse = {
            let els = els.clone();
            let on_click = on_click.clone();
            window_event_listener(::leptos::ev::mousedown, move |ev| {
                dispatch_outside_click(&ev, els.as_ref(), on_click.as_ref());
            })
        };
        on_cleanup(move || {
            handle_pointer.remove();
            handle_mouse.remove();
        });
    }
    #[cfg(all(feature = "ssr", not(feature = "hydrate")))]
    {
        let _ = els;
        let _ = on_click;
    }
}

#[cfg(any(feature = "hydrate", not(feature = "ssr")))]
fn dispatch_outside_click<EF, CF>(ev: &web_sys::Event, els: &EF, on_click: &CF)
where
    EF: Fn() -> Option<Vec<web_sys::Element>>,
    CF: Fn(),
{
    let Some(els) = els() else {
        return;
    };
    if !els.is_empty() {
        let composed_path = ev.composed_path();
        if els.iter().any(|el| composed_path.includes(el, 0)) {
            return;
        }
    }
    on_click();
}