leptos_shadcn_tabs/
default.rs

1use leptos::{ev::MouseEvent, prelude::*};
2use leptos_node_ref::AnyNodeRef;
3use leptos_struct_component::{StructComponent, struct_component};
4use leptos_style::Style;
5
6// Tabs Root Provider
7#[component]
8pub fn Tabs(
9    #[prop(into, optional)] value: Signal<String>,
10    #[prop(into, optional)] on_value_change: Option<Callback<String>>,
11    #[prop(into, optional)] default_value: MaybeProp<String>,
12    #[prop(into, optional)] class: MaybeProp<String>,
13    #[prop(optional)] children: Option<Children>,
14) -> impl IntoView {
15    let internal_value = RwSignal::new(default_value.get().unwrap_or_default());
16    
17    let value_state = Signal::derive(move || {
18        if !value.get().is_empty() && value.get() != internal_value.get() {
19            value.get()
20        } else {
21            internal_value.get()
22        }
23    });
24
25    let set_value = Callback::new(move |new_value: String| {
26        internal_value.set(new_value.clone());
27        if let Some(callback) = &on_value_change {
28            callback.run(new_value);
29        }
30    });
31
32    provide_context(TabsContextValue {
33        value: value_state,
34        set_value,
35    });
36
37    let tabs_class = Signal::derive(move || {
38        format!("{}", class.get().unwrap_or_default())
39    });
40
41    view! {
42        <div class={tabs_class}>
43            {children.map(|c| c())}
44        </div>
45    }
46}
47
48#[derive(Clone, Copy)]
49pub struct TabsContextValue {
50    pub value: Signal<String>,
51    pub set_value: Callback<String>,
52}
53
54// Tabs List
55#[component]
56pub fn TabsList(
57    #[prop(into, optional)] class: MaybeProp<String>,
58    #[prop(optional)] children: Option<Children>,
59) -> impl IntoView {
60    let list_class = Signal::derive(move || {
61        format!("inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground {}", class.get().unwrap_or_default())
62    });
63
64    view! {
65        <div class={list_class} role="tablist">
66            {children.map(|c| c())}
67        </div>
68    }
69}
70
71// Tabs Trigger
72#[derive(Clone, StructComponent)]
73#[struct_component(tag = "button")]
74pub struct TabsTriggerChildProps {
75    pub node_ref: AnyNodeRef,
76    pub class: Signal<String>,
77    pub id: MaybeProp<String>,
78    pub style: Signal<Style>,
79    pub disabled: Signal<bool>,
80    pub r#type: MaybeProp<String>,
81    pub role: Signal<String>,
82    pub aria_selected: Signal<String>,
83    pub onclick: Option<Callback<MouseEvent>>,
84}
85
86#[component]
87pub fn TabsTrigger(
88    #[prop(into)] value: MaybeProp<String>,
89    #[prop(into, optional)] class: MaybeProp<String>,
90    #[prop(into, optional)] id: MaybeProp<String>,
91    #[prop(into, optional)] style: Signal<Style>,
92    #[prop(into, optional)] node_ref: AnyNodeRef,
93    #[prop(into, optional)] as_child: Option<Callback<TabsTriggerChildProps, AnyView>>,
94    #[prop(optional)] children: Option<Children>,
95) -> impl IntoView {
96    let ctx = expect_context::<TabsContextValue>();
97    
98    let is_selected = Signal::derive(move || {
99        ctx.value.get() == value.get().unwrap_or_default()
100    });
101
102    let trigger_class = Signal::derive(move || {
103        let base_class = "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
104        let selected_class = if is_selected.get() {
105            "bg-background text-foreground shadow-sm"
106        } else {
107            "hover:bg-background hover:text-foreground"
108        };
109        format!("{} {} {}", base_class, selected_class, class.get().unwrap_or_default())
110    });
111
112    let child_props = TabsTriggerChildProps {
113        node_ref,
114        class: trigger_class,
115        id,
116        style,
117        disabled: Signal::derive(|| false),
118        r#type: "button".to_string().into(),
119        role: "tab".to_string().into(),
120        aria_selected: Signal::derive(move || is_selected.get().to_string()).into(),
121        onclick: Some(Callback::new({
122            let ctx = ctx.clone();
123            let value = value.clone();
124            move |_: MouseEvent| {
125                let val = value.get().unwrap_or_default();
126                ctx.set_value.run(val);
127            }
128        })),
129    };
130
131    if let Some(as_child) = as_child.as_ref() {
132        as_child.run(child_props)
133    } else {
134        child_props.render(children)
135    }
136}
137
138// Tabs Content
139#[component]
140pub fn TabsContent(
141    #[prop(into)] value: MaybeProp<String>,
142    #[prop(into, optional)] class: MaybeProp<String>,
143    #[prop(optional)] children: Option<Children>,
144) -> impl IntoView {
145    let ctx = expect_context::<TabsContextValue>();
146    
147    let is_selected = Signal::derive(move || {
148        ctx.value.get() == value.get().unwrap_or_default()
149    });
150
151    let content_class = Signal::derive(move || {
152        format!("mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 {}", class.get().unwrap_or_default())
153    });
154
155    view! {
156        <div
157            class={content_class}
158            role="tabpanel"
159            aria-selected={move || is_selected.get().to_string()}
160            style={move || if is_selected.get() { "" } else { "display: none;" }}
161        >
162            {children.map(|c| c())}
163        </div>
164    }
165}