use icondata::{BiChevronLeftRegular, BiChevronRightRegular};
use leptos::prelude::*;
use web_sys::HtmlDivElement;
use crate::components::actions::button::BasicButton;
#[derive(Clone, Default)]
pub struct TabLabel {
pub label: ViewFn,
}
impl TabLabel {
pub fn new(label: ViewFn) -> Self {
TabLabel { label: label }
}
}
#[slot]
pub struct Tab {
pub children: ChildrenFn,
}
#[component]
pub fn Tabs(
#[prop(default = vec![])] tab: Vec<Tab>,
#[prop(into)] tab_labels: RwSignal<Vec<TabLabel>>,
) -> impl IntoView {
let (current_tab, set_current_tab) = signal(0usize);
let tab_nav_ref: NodeRef<leptos::html::Div> = NodeRef::new();
let can_scroll_left = RwSignal::new(false);
let can_scroll_right = RwSignal::new(false);
let scroll_amount = 150.0_f64;
let update_caret = move || {
if let Some(el) = tab_nav_ref.get() as Option<HtmlDivElement> {
let scroll_left_val = el.scroll_left();
let max_scroll = el.scroll_width() - el.client_width();
can_scroll_left.set(scroll_left_val > 0);
can_scroll_right.set(scroll_left_val < max_scroll);
}
};
let scroll_left_click = move || {
if let Some(el) = tab_nav_ref.get() as Option<HtmlDivElement> {
el.scroll_by_with_x_and_y(-scroll_amount, 0.0);
}
update_caret();
};
let scroll_right_click = move || {
if let Some(el) = tab_nav_ref.get() as Option<HtmlDivElement> {
el.scroll_by_with_x_and_y(scroll_amount, 0.0);
}
update_caret();
};
view! {
<div class="w-full">
<div class="relative w-full flex flex-row items-center border-b border-mid-gray">
<BasicButton
icon=Some(BiChevronLeftRegular)
disabled=MaybeProp::derive(move || Some(!can_scroll_left.get()))
onclick=Callback::new(move |_| scroll_left_click())
/>
<div
node_ref=tab_nav_ref
class="flex flex-row gap-6 overflow-x-auto scrollbar-hidden scroll-smooth flex-1"
on:scroll=move |_| update_caret()
>
{move || {
let labels = tab_labels.get();
let current = current_tab.get();
labels.into_iter().enumerate().map(|(index, label)| {
let dynamic_class = move || {
if current_tab.get() == index {
"border-primary text-primary"
} else {
"border-transparent hover:border-mid-gray"
}
};
view! {
<span
class=move || format!(
"border-b-4 transition-all duration-200 ease-in-out pb-2 cursor-pointer shrink-0 {}",
dynamic_class()
)
on:click=move |_| set_current_tab.set(index)
>
{label.label.run()}
</span>
}
}).collect::<Vec<_>>()
}}
</div>
<BasicButton
icon=Some(BiChevronRightRegular)
disabled=MaybeProp::derive(move || Some(!can_scroll_right.get()))
onclick=Callback::new(move |_| scroll_right_click())
/>
</div>
<div class="relative min-h-[150px] mt-4">
{move || {
let _current = current_tab.get();
tab.iter().enumerate().map(|(i, child)| {
let dynamic_class = move || {
if current_tab.get() == i { "block" } else { "hidden" }
};
view! {
<div class=dynamic_class>
{(child.children)().into_any()}
</div>
}
}).collect_view()
}}
</div>
</div>
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tab_label_new_stores_label() {
let label = TabLabel::new(ViewFn::from(|| view! {}));
let _ = label;
}
#[test]
fn tab_label_default() {
let label = TabLabel::default();
let _ = label; }
fn tab_label_class(current: usize, index: usize) -> &'static str {
if current == index {
"border-primary text-primary"
} else {
"border-transparent"
}
}
#[test]
fn active_tab_gets_primary_class() {
assert_eq!(tab_label_class(1, 1), "border-primary text-primary");
}
#[test]
fn inactive_tab_gets_transparent_class() {
assert_eq!(tab_label_class(0, 1), "border-transparent");
assert_eq!(tab_label_class(2, 0), "border-transparent");
}
fn content_class(current: usize, index: usize) -> &'static str {
if current == index { "block" } else { "hidden" }
}
#[test]
fn active_tab_content_is_block() {
assert_eq!(content_class(0, 0), "block");
assert_eq!(content_class(2, 2), "block");
}
#[test]
fn inactive_tab_content_is_hidden() {
assert_eq!(content_class(0, 1), "hidden");
assert_eq!(content_class(1, 0), "hidden");
}
fn caret_state(scroll_left: f64, scroll_width: f64, client_width: f64) -> (bool, bool) {
let max_scroll = scroll_width - client_width;
let can_left = scroll_left > 0.0;
let can_right = scroll_left < max_scroll;
(can_left, can_right)
}
#[test]
fn at_start_cannot_scroll_left() {
let (can_left, _) = caret_state(0.0, 500.0, 300.0);
assert!(!can_left);
}
#[test]
fn at_start_can_scroll_right_when_overflow() {
let (_, can_right) = caret_state(0.0, 500.0, 300.0);
assert!(can_right);
}
#[test]
fn at_end_cannot_scroll_right() {
let (_, can_right) = caret_state(200.0, 500.0, 300.0);
assert!(!can_right);
}
#[test]
fn at_end_can_scroll_left() {
let (can_left, _) = caret_state(200.0, 500.0, 300.0);
assert!(can_left);
}
#[test]
fn no_overflow_neither_caret_active() {
let (can_left, can_right) = caret_state(0.0, 300.0, 300.0);
assert!(!can_left);
assert!(!can_right);
}
#[test]
fn mid_scroll_both_carets_active() {
let (can_left, can_right) = caret_state(100.0, 500.0, 300.0);
assert!(can_left);
assert!(can_right);
}
#[test]
fn clicking_tab_updates_current() {
let owner = Owner::new();
owner.with(|| {
let (current_tab, set_current_tab) = signal(0usize);
set_current_tab.set(2);
assert_eq!(current_tab.get(), 2);
});
}
#[test]
fn initial_tab_is_zero() {
let owner = Owner::new();
owner.with(|| {
let (current_tab, _) = signal(0usize);
assert_eq!(current_tab.get(), 0);
});
}
#[test]
fn caret_signals_update_reactively() {
let owner = Owner::new();
owner.with(|| {
let can_scroll_left = RwSignal::new(false);
let can_scroll_right = RwSignal::new(true);
assert!(!can_scroll_left.get());
assert!(can_scroll_right.get());
can_scroll_left.set(true);
can_scroll_right.set(false);
assert!(can_scroll_left.get());
assert!(!can_scroll_right.get());
});
}
}