radix_leptos_primitives/components/
tabs.rs

1use leptos::prelude::*;
2
3#[derive(Clone, Debug, PartialEq)]
4pub enum TabsOrientation {
5    Horizontal,
6    Vertical,
7}
8
9impl Default for TabsOrientation {
10    fn default() -> Self {
11        TabsOrientation::Horizontal
12    }
13}
14
15#[derive(Clone, Debug, PartialEq)]
16pub enum TabsSize {
17    Small,
18    Medium,
19    Large,
20}
21
22impl Default for TabsSize {
23    fn default() -> Self {
24        TabsSize::Medium
25    }
26}
27
28#[derive(Clone, Debug, PartialEq)]
29pub enum TabsVariant {
30    Default,
31    Pills,
32    Underlined,
33}
34
35impl Default for TabsVariant {
36    fn default() -> Self {
37        TabsVariant::Default
38    }
39}
40
41fn merge_classes(base: &str, custom: Option<String>) -> String {
42    let class_value = custom.unwrap_or_default();
43    let final_class = format!("{} {}", base, class_value);
44    final_class.trim().to_string()
45}
46
47#[component]
48pub fn Tabs(
49    #[prop(optional)] orientation: Option<TabsOrientation>,
50    #[prop(optional)] size: Option<TabsSize>,
51    #[prop(optional)] variant: Option<TabsVariant>,
52    #[prop(optional)] _default_value: Option<String>,
53    #[prop(optional)] _value: Option<ReadSignal<String>>,
54    #[prop(optional)] _on_change: Option<Callback<String>>,
55    #[prop(optional)] class: Option<String>,
56    #[prop(optional)] style: Option<String>,
57    children: Children,
58) -> impl IntoView {
59    let orientation = orientation.unwrap_or_default();
60    let _size = size.unwrap_or_default();
61    let variant = variant.unwrap_or_default();
62    
63    let base_classes = match (orientation, variant) {
64        (TabsOrientation::Horizontal, TabsVariant::Default) => "flex flex-col",
65        (TabsOrientation::Horizontal, TabsVariant::Pills) => "flex flex-col",
66        (TabsOrientation::Horizontal, TabsVariant::Underlined) => "flex flex-col",
67        (TabsOrientation::Vertical, TabsVariant::Default) => "flex flex-row",
68        (TabsOrientation::Vertical, TabsVariant::Pills) => "flex flex-row",
69        (TabsOrientation::Vertical, TabsVariant::Underlined) => "flex flex-row",
70    };
71    
72    let final_class = merge_classes(base_classes, class);
73    let style_attr = style.unwrap_or_default();
74
75    view! {
76        <div class={final_class} style={style_attr}>
77            {children()}
78        </div>
79    }
80}
81
82#[component]
83pub fn TabsList(
84    #[prop(optional)] orientation: Option<TabsOrientation>,
85    #[prop(optional)] size: Option<TabsSize>,
86    #[prop(optional)] variant: Option<TabsVariant>,
87    #[prop(optional)] class: Option<String>,
88    #[prop(optional)] style: Option<String>,
89    children: Children,
90) -> impl IntoView {
91    let orientation = orientation.unwrap_or_default();
92    let _size = size.unwrap_or_default();
93    let variant = variant.unwrap_or_default();
94    
95    let base_classes = match (orientation, variant, _size) {
96        (TabsOrientation::Horizontal, TabsVariant::Default, TabsSize::Small) => "inline-flex h-8 items-center justify-center rounded-md bg-gray-100 p-1 text-gray-600",
97        (TabsOrientation::Horizontal, TabsVariant::Default, TabsSize::Medium) => "inline-flex h-10 items-center justify-center rounded-md bg-gray-100 p-1 text-gray-600",
98        (TabsOrientation::Horizontal, TabsVariant::Default, TabsSize::Large) => "inline-flex h-12 items-center justify-center rounded-md bg-gray-100 p-1 text-gray-600",
99        (TabsOrientation::Horizontal, TabsVariant::Pills, TabsSize::Small) => "inline-flex h-8 items-center justify-center rounded-lg bg-gray-100 p-1 text-gray-600",
100        (TabsOrientation::Horizontal, TabsVariant::Pills, TabsSize::Medium) => "inline-flex h-10 items-center justify-center rounded-lg bg-gray-100 p-1 text-gray-600",
101        (TabsOrientation::Horizontal, TabsVariant::Pills, TabsSize::Large) => "inline-flex h-12 items-center justify-center rounded-lg bg-gray-100 p-1 text-gray-600",
102        (TabsOrientation::Horizontal, TabsVariant::Underlined, TabsSize::Small) => "inline-flex h-8 items-center justify-center border-b border-gray-300 text-gray-600",
103        (TabsOrientation::Horizontal, TabsVariant::Underlined, TabsSize::Medium) => "inline-flex h-10 items-center justify-center border-b border-gray-300 text-gray-600",
104        (TabsOrientation::Horizontal, TabsVariant::Underlined, TabsSize::Large) => "inline-flex h-12 items-center justify-center border-b border-gray-300 text-gray-600",
105        (TabsOrientation::Vertical, TabsVariant::Default, TabsSize::Small) => "inline-flex w-8 flex-col items-center justify-center rounded-md bg-gray-100 p-1 text-gray-600",
106        (TabsOrientation::Vertical, TabsVariant::Default, TabsSize::Medium) => "inline-flex w-10 flex-col items-center justify-center rounded-md bg-gray-100 p-1 text-gray-600",
107        (TabsOrientation::Vertical, TabsVariant::Default, TabsSize::Large) => "inline-flex w-12 flex-col items-center justify-center rounded-md bg-gray-100 p-1 text-gray-600",
108        (TabsOrientation::Vertical, TabsVariant::Pills, TabsSize::Small) => "inline-flex w-8 flex-col items-center justify-center rounded-lg bg-gray-100 p-1 text-gray-600",
109        (TabsOrientation::Vertical, TabsVariant::Pills, TabsSize::Medium) => "inline-flex w-10 flex-col items-center justify-center rounded-lg bg-gray-100 p-1 text-gray-600",
110        (TabsOrientation::Vertical, TabsVariant::Pills, TabsSize::Large) => "inline-flex w-12 flex-col items-center justify-center rounded-lg bg-gray-100 p-1 text-gray-600",
111        (TabsOrientation::Vertical, TabsVariant::Underlined, TabsSize::Small) => "inline-flex w-8 flex-col items-center justify-center border-r border-gray-300 text-gray-600",
112        (TabsOrientation::Vertical, TabsVariant::Underlined, TabsSize::Medium) => "inline-flex w-10 flex-col items-center justify-center border-r border-gray-300 text-gray-600",
113        (TabsOrientation::Vertical, TabsVariant::Underlined, TabsSize::Large) => "inline-flex w-12 flex-col items-center justify-center border-r border-gray-300 text-gray-600",
114    };
115    
116    let final_class = merge_classes(base_classes, class);
117    let style_attr = style.unwrap_or_default();
118
119    view! {
120        <div class={final_class} style={style_attr} role="tablist">
121            {children()}
122        </div>
123    }
124}
125
126#[component]
127pub fn TabsTrigger(
128    #[prop(optional)] value: Option<String>,
129    #[prop(optional)] disabled: Option<bool>,
130    #[prop(optional)] size: Option<TabsSize>,
131    #[prop(optional)] variant: Option<TabsVariant>,
132    #[prop(optional)] class: Option<String>,
133    #[prop(optional)] style: Option<String>,
134    #[prop(optional)] on_click: Option<Callback<String>>,
135    children: Children,
136) -> impl IntoView {
137    let value = value.unwrap_or_default();
138    let disabled = disabled.unwrap_or(false);
139    let _size = size.unwrap_or_default();
140    let variant = variant.unwrap_or_default();
141    
142    let base_classes = match (variant, _size) {
143        (TabsVariant::Default, TabsSize::Small) => "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-gray-200 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm",
144        (TabsVariant::Default, TabsSize::Medium) => "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-gray-200 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm",
145        (TabsVariant::Default, TabsSize::Large) => "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-4 py-2 text-base font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-gray-200 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm",
146        (TabsVariant::Pills, TabsSize::Small) => "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-gray-200 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm",
147        (TabsVariant::Pills, TabsSize::Medium) => "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-gray-200 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm",
148        (TabsVariant::Pills, TabsSize::Large) => "inline-flex items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-base font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-gray-200 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm",
149        (TabsVariant::Underlined, TabsSize::Small) => "inline-flex items-center justify-center whitespace-nowrap border-b-2 border-transparent px-3 py-1 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:border-gray-300 data-[state=active]:border-blue-500 data-[state=active]:text-gray-900",
150        (TabsVariant::Underlined, TabsSize::Medium) => "inline-flex items-center justify-center whitespace-nowrap border-b-2 border-transparent px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:border-gray-300 data-[state=active]:border-blue-500 data-[state=active]:text-gray-900",
151        (TabsVariant::Underlined, TabsSize::Large) => "inline-flex items-center justify-center whitespace-nowrap border-b-2 border-transparent px-4 py-2 text-base font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:border-gray-300 data-[state=active]:border-blue-500 data-[state=active]:text-gray-900",
152    };
153    
154    let final_class = merge_classes(base_classes, class);
155    let style_attr = style.unwrap_or_default();
156
157    let value_clone = value.clone();
158    let handle_click = move |_| {
159        if !disabled {
160            if let Some(callback) = on_click {
161                callback.run(value_clone.clone());
162            }
163        }
164    };
165
166    let value_clone = value.clone();
167    let handle_keydown = move |event: web_sys::KeyboardEvent| {
168        if !disabled && (event.key() == "Enter" || event.key() == " ") {
169            event.prevent_default();
170            if let Some(callback) = on_click {
171                callback.run(value_clone.clone());
172            }
173        }
174    };
175
176    view! {
177        <button
178            class={final_class}
179            style={style_attr}
180            disabled={disabled}
181            data-value={value}
182            role="tab"
183            on:click=handle_click
184            on:keydown=handle_keydown
185        >
186            {children()}
187        </button>
188    }
189}
190
191#[component]
192pub fn TabsContent(
193    #[prop(optional)] value: Option<String>,
194    #[prop(optional)] class: Option<String>,
195    #[prop(optional)] style: Option<String>,
196    children: Children,
197) -> impl IntoView {
198    let value = value.unwrap_or_default();
199    let base_classes = "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
200    let final_class = merge_classes(base_classes, class);
201    let style_attr = style.unwrap_or_default();
202
203    view! {
204        <div
205            class={final_class}
206            style={style_attr}
207            data-value={value}
208            role="tabpanel"
209            tabindex="0"
210        >
211            {children()}
212        </div>
213    }
214}