use leptos::prelude::*;
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
use orbital_base_components::ThemedPortal;
use orbital_core_components::overlay::FloatingPanel;
use orbital_core_components::MaterialElevation;
use orbital_motion::use_reduced_motion;
use crate::context::{
use_axis_tooltip, use_chart_context, use_item_tooltip, use_overlay_context, use_pointer_plot,
ChartOverlayContext, ChartTooltipContext,
};
use crate::{OverlayMount, TooltipConfig, TooltipTrigger};
use super::content::{ChartTooltipAxisContent, ChartTooltipItemContent};
#[component]
pub fn ChartTooltip(
#[prop(default = TooltipConfig::item())]
config: TooltipConfig,
) -> impl IntoView {
if matches!(config.trigger, TooltipTrigger::None) {
return ().into_view().into_any();
}
provide_tooltip_runtime(&config);
let item_data = use_item_tooltip();
let axis_data = use_axis_tooltip();
let pointer = use_pointer_plot();
let ctx = use_chart_context();
let overlay_ctx = use_overlay_context();
let prefers_reduced = use_reduced_motion();
let trigger = config.trigger;
let hide_header = config.hide_x_header;
let item_slot = config.slots.item_content.clone();
let axis_slot = config.slots.axis_content.clone();
let chart_w = ctx.width;
let chart_h = ctx.height;
let use_host_portal = matches!(overlay_ctx.overlay_mount, OverlayMount::HostElement { .. });
let has_data = Signal::derive(move || match trigger {
TooltipTrigger::Item => item_data.get().is_some(),
TooltipTrigger::Axis => axis_data.get().is_some(),
TooltipTrigger::None => false,
});
let delay_ms = Signal::derive(move || {
if prefers_reduced.get() {
0
} else {
ctx.charts_theme.tooltip_delay_ms
}
});
let tooltip_visible = use_delayed_tooltip_visible(has_data, delay_ms);
view! {
{move || {
if !tooltip_visible.get() {
return ().into_any();
}
let style = tooltip_viewport_style(
pointer.get(),
overlay_ctx.root_ref,
chart_w,
chart_h,
);
let panel = match trigger {
TooltipTrigger::Item => {
let Some(data) = item_data.get() else {
return ().into_any();
};
if let Some(slot) = item_slot.clone() {
tooltip_panel((slot)(), style)
} else {
tooltip_panel(
view! { <ChartTooltipItemContent data=data /> }.into_any(),
style,
)
}
}
TooltipTrigger::Axis => {
let Some(data) = axis_data.get() else {
return ().into_any();
};
if let Some(slot) = axis_slot.clone() {
tooltip_panel((slot)(), style)
} else {
tooltip_panel(
view! {
<ChartTooltipAxisContent data=data hide_header=hide_header />
}
.into_any(),
style,
)
}
}
TooltipTrigger::None => return ().into_any(),
};
if use_host_portal {
host_portal_tooltip(panel, &overlay_ctx)
} else {
panel
}
}}
}
.into_any()
}
fn use_delayed_tooltip_visible(has_data: Signal<bool>, delay_ms: Signal<u64>) -> ReadSignal<bool> {
let visible = RwSignal::new(false);
Effect::new(move |_| {
if !has_data.get() {
visible.set(false);
return;
}
let delay = delay_ms.get();
if delay == 0 {
visible.set(true);
return;
}
visible.set(false);
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
{
use wasm_bindgen::JsCast;
if let Some(win) = web_sys::window() {
let show = visible;
let closure = wasm_bindgen::closure::Closure::once(move || {
show.set(true);
});
let result = win.set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
delay as i32,
);
let handle = result.ok();
closure.forget();
on_cleanup(move || {
if let (Some(win), Some(handle)) = (web_sys::window(), handle) {
win.clear_timeout_with_handle(handle);
}
});
return;
}
}
visible.set(true);
});
visible.read_only()
}
fn tooltip_panel(content: AnyView, style: String) -> AnyView {
view! {
<div class="orb-chart-tooltip" style=style>
<FloatingPanel elevation=MaterialElevation::Raised role="tooltip">
{content}
</FloatingPanel>
</div>
}
.into_any()
}
fn host_portal_tooltip(content: AnyView, overlay_ctx: &ChartOverlayContext) -> AnyView {
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
{
let mount = overlay_ctx.resolve_portal_mount();
return match mount {
Some(el) => view! {
<ThemedPortal immediate=true mount=el>
{content}
</ThemedPortal>
}
.into_any(),
None => content,
};
}
#[cfg(not(all(not(feature = "ssr"), feature = "hydrate")))]
{
let _ = overlay_ctx;
content
}
}
fn provide_tooltip_runtime(config: &TooltipConfig) {
if leptos::prelude::use_context::<ChartTooltipContext>().is_none() {
crate::context::provide_tooltip_context(config.trigger, config.hide_x_header);
}
}
fn tooltip_viewport_style(
pointer: Option<(f64, f64)>,
root_ref: NodeRef<leptos::html::Div>,
chart_w: f64,
chart_h: f64,
) -> String {
#[cfg(not(all(not(feature = "ssr"), feature = "hydrate")))]
let _ = root_ref;
let (left, top) = pointer.unwrap_or((chart_w / 2.0, chart_h / 4.0));
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
{
if let Some(root) = root_ref.get() {
let rect = root.get_bounding_client_rect();
let fixed_left = rect.left() + left;
let fixed_top = rect.top() + top;
let viewport_w = web_sys::window()
.and_then(|w| w.inner_width().ok())
.and_then(|v| v.as_f64())
.unwrap_or(chart_w);
let viewport_h = web_sys::window()
.and_then(|w| w.inner_height().ok())
.and_then(|v| v.as_f64())
.unwrap_or(chart_h);
let left = fixed_left.clamp(8.0, viewport_w - 120.0);
let top = fixed_top.clamp(8.0, viewport_h - 80.0);
return format!("left: {left}px; top: {top}px;");
}
}
let left = left.clamp(8.0, chart_w - 120.0);
let top = top.clamp(8.0, chart_h - 80.0);
format!("left: {left}px; top: {top}px;")
}