leptos_shadcn_tabs/
default.rs1use leptos::{ev::MouseEvent, prelude::*};
2use leptos_node_ref::AnyNodeRef;
3use leptos_struct_component::{StructComponent, struct_component};
4use leptos_style::Style;
5
6#[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#[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#[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#[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}