radix_leptos_primitives/components/
tabs.rs1use 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}