diff --git a/demo_markdown/docs/tabs/mod.md b/demo_markdown/docs/tabs/mod.md index fdc30dc..f473ad7 100644 --- a/demo_markdown/docs/tabs/mod.md +++ b/demo_markdown/docs/tabs/mod.md @@ -1,44 +1,17 @@ # Tabs ```rust demo -let value = create_rw_signal(String::from("apple")); +let selected_value = RwSignal::new(String::new()); view! { - - - "apple" + + + "🍎 Apple" - - "pear" + + "🍐 Pear" - -} -``` - -### Custom tab label - -```rust demo -use leptos_meta::Style; -let value = create_rw_signal(String::from("apple")); - -view! { - - - - - "🍎 Apple" - - "apple" - - - - "🍐 Pear" - - "pear" - - + } ``` diff --git a/thaw/src/tabs/mod.rs b/thaw/src/tabs/mod.rs index 6da1e0a..68cc5c7 100644 --- a/thaw/src/tabs/mod.rs +++ b/thaw/src/tabs/mod.rs @@ -2,189 +2,72 @@ mod tab; pub use tab::*; -use crate::{theme::use_theme, Theme}; use leptos::*; -use thaw_utils::{class_list, mount_style, Model, OptionalProp}; +use std::collections::HashMap; +use thaw_utils::{class_list, mount_style, Model}; #[component] -pub fn Tabs( - #[prop(optional, into)] value: Model, - #[prop(optional, into)] class: OptionalProp>, +pub fn TabList( + #[prop(optional, into)] selected_value: Model, + #[prop(optional, into)] class: MaybeProp, children: Children, ) -> impl IntoView { - mount_style("tabs", include_str!("./tabs.css")); - let tab_options_vec = create_rw_signal(vec![]); + mount_style("tab-list", include_str!("./tab-list.css")); + let registered_tabs = RwSignal::new(HashMap::new()); + // request_animation_frame(move || { + // let list_rect = label_list.get_bounding_client_rect(); + // let rect = label.get_bounding_client_rect(); + // label_line + // .set( + // Some(TabsLabelLine { + // width: rect.width(), + // left: rect.left() - list_rect.left(), + // }), + // ); + // }); view! { - - +
+ {children()} +
} } -#[component] -fn TabsInner( - value: Model, - tab_options_vec: RwSignal>, - #[prop(optional, into)] class: OptionalProp>, - children: Children, -) -> impl IntoView { - mount_style("tabs", include_str!("./tabs.css")); - let theme = use_theme(Theme::light); - let css_vars = create_memo(move |_| { - let mut css_vars = String::new(); - theme.with(|theme| { - let color_primary = theme.common.color_primary.clone(); - css_vars.push_str(&format!( - "--thaw-label-active-background-color: {color_primary};" - )); - }); - css_vars - }); - - let label_line = create_rw_signal::>(None); - let label_line_style = create_memo(move |_| { - let mut style = String::new(); - if let Some(line) = label_line.get() { - style.push_str(&format!("width: {}px; left: {}px", line.width, line.left)) - } - style - }); - let label_list_ref = create_node_ref::(); - - let children = children(); - - view! { -
-
- (); - let TabOption { key, label, label_view } = option; - create_effect({ - let key = key.clone(); - move |_| { - let Some(label) = label_ref.get() else { - return; - }; - let Some(label_list) = label_list_ref.get() else { - return; - }; - if key.clone() == value.get() { - request_animation_frame(move || { - let list_rect = label_list.get_bounding_client_rect(); - let rect = label.get_bounding_client_rect(); - label_line - .set( - Some(TabsLabelLine { - width: rect.width(), - left: rect.left() - list_rect.left(), - }), - ); - }); - } - } - }); - let is_active = create_memo({ - let key = key.clone(); - move |_| key == value.get() - }); - if let Some(label_view) = label_view { - let TabLabelView { class, children } = label_view; - view! { - - - {children} - - } - } else { - view! { - - - {if label.is_empty() { key } else { label }} - - } - } - } - /> - - -
-
{children}
-
- } -} - #[derive(Clone)] -pub(crate) struct TabsLabelLine { - width: f64, - left: f64, +pub(crate) struct TabListInjection { + pub selected_value: Model, + registered_tabs: RwSignal>, } -#[derive(Clone)] -pub(crate) struct TabsInjection { - active_key: Model, - tab_options_vec: RwSignal>, -} +impl Copy for TabListInjection {} -impl TabsInjection { - pub fn get_key(&self) -> String { - self.active_key.get() +impl TabListInjection { + pub fn use_() -> Self { + expect_context() } - pub(crate) fn push_tab_options(&self, options: TabOption) { - self.tab_options_vec.update(|v| { - v.push(options); + pub fn register(&self, data: TabRegisterData) { + self.registered_tabs.update(|map| { + map.insert(data.value.clone(), data); }); } - pub(crate) fn remove_tab_options(&self, key: &String) { - self.tab_options_vec.update(|v| { - if let Some(index) = v.iter().position(|tab| &tab.key == key) { - v.remove(index); - } + pub fn unregister(&self, value: &String) { + self.registered_tabs.update(|map| { + map.remove(value); }); } } -pub(crate) fn use_tabs() -> TabsInjection { - expect_context() +pub(crate) struct TabRegisterData { + value: String, + tab_ref: NodeRef, } diff --git a/thaw/src/tabs/tab-list.css b/thaw/src/tabs/tab-list.css new file mode 100644 index 0000000..74eefac --- /dev/null +++ b/thaw/src/tabs/tab-list.css @@ -0,0 +1,8 @@ +.thaw-tab-list { + position: relative; + display: flex; + flex-wrap: nowrap; + flex-direction: row; + flex-shrink: 0; + align-items: stretch; +} \ No newline at end of file diff --git a/thaw/src/tabs/tab.css b/thaw/src/tabs/tab.css index 4360944..b934239 100644 --- a/thaw/src/tabs/tab.css +++ b/thaw/src/tabs/tab.css @@ -1,7 +1,61 @@ .thaw-tab { - padding-top: 12px; + flex-shrink: 0; + justify-content: center; + align-items: center; + outline-style: none; + position: relative; + display: grid; + grid-template-rows: auto; + grid-template-columns: auto; + grid-auto-flow: column; + column-gap: var(--spacingHorizontalSNudge); + background-color: var(--colorTransparentBackground); + line-height: var(--lineHeightBase300); + font-family: var(--fontFamilyBase); + text-transform: none; + padding: var(--spacingVerticalM) var(--spacingHorizontalMNudge); + cursor: pointer; + overflow: hidden; + border-radius: var(--borderRadiusMedium); + border: none; } -.thaw-tab--hidden { - display: none; +.thaw-tab::before { + right: var(--spacingHorizontalM); + left: var(--spacingHorizontalM); + height: var(--strokeWidthThicker); + bottom: 0px; +} + +.thaw-tab:hover::before { + position: absolute; + content: ""; + background-color: var(--colorNeutralStroke1Hover); + border-radius: var(--borderRadiusCircular); +} + +.thaw-tab:active::before { + position: absolute; + content: ""; + background-color: var(--colorNeutralStroke1Pressed); + border-radius: var(--borderRadiusCircular); +} + +.thaw-tab__content { + grid-row-start: 1; + grid-column-start: 1; + padding: var(--spacingVerticalNone) var(--spacingHorizontalXXS); + color: var(--colorNeutralForeground2); + line-height: var(--lineHeightBase300); + font-weight: var(--fontWeightRegular); + font-size: var(--fontSizeBase300); + overflow: hidden; +} + +.thaw-tab:hover .thaw-tab__content { + color: var(--colorNeutralForeground2Hover); +} + +.thaw-tab:active .thaw-tab__content { + color: var(--colorNeutralForeground2Pressed); } diff --git a/thaw/src/tabs/tab.rs b/thaw/src/tabs/tab.rs index 3332ad6..8781775 100644 --- a/thaw/src/tabs/tab.rs +++ b/thaw/src/tabs/tab.rs @@ -1,74 +1,44 @@ -use super::use_tabs; +use super::{TabListInjection, TabRegisterData}; use leptos::*; -use thaw_utils::{class_list, mount_style, OptionalProp}; - -#[derive(Clone)] -pub(crate) struct TabOption { - pub key: String, - pub label: String, - pub label_view: Option, -} - -#[derive(Clone)] -pub(crate) struct TabLabelView { - pub class: OptionalProp>, - pub children: Fragment, -} - -impl From for TabLabelView { - fn from(tab_label: TabLabel) -> Self { - let TabLabel { class, children } = tab_label; - Self { - class, - children: children(), - } - } -} - -#[slot] -pub struct TabLabel { - #[prop(optional, into)] - class: OptionalProp>, - children: Children, -} +use thaw_utils::{class_list, mount_style}; #[component] pub fn Tab( - #[prop(into)] key: String, - #[prop(optional, into)] label: String, - #[prop(optional)] tab_label: Option, - #[prop(optional, into)] class: OptionalProp>, + #[prop(optional, into)] class: MaybeProp, + #[prop(into)] value: String, children: Children, ) -> impl IntoView { mount_style("tab", include_str!("./tab.css")); - let tabs = use_tabs(); - tabs.push_tab_options(TabOption { - key: key.clone(), - label, - label_view: tab_label.map(|label| label.into()), - }); - let is_active = create_memo({ - let key = key.clone(); - let tabs = tabs.clone(); - move |_| key == tabs.get_key() + let tab_ref = NodeRef::::new(); + let tab_list = TabListInjection::use_(); + let value = StoredValue::new(value); + tab_list.register(TabRegisterData { + value: value.get_value(), + tab_ref, }); - on_cleanup(move || { - tabs.remove_tab_options(&key); + value.with_value(|v| tab_list.unregister(v)); + }); + + let selected = Memo::new(move |_| { + tab_list + .selected_value + .with(|selected_value| value.with_value(|value| value == selected_value)) }); view! { -
- {children()} -
+ + {children()} + + } } diff --git a/thaw/src/tabs/tabs.css b/thaw/src/tabs/tabs.css deleted file mode 100644 index 173c331..0000000 --- a/thaw/src/tabs/tabs.css +++ /dev/null @@ -1,20 +0,0 @@ -.thaw-tabs__label-list { - position: relative; -} - -.thaw-tabs__label { - padding: 0 20px; - display: inline-block; - height: 40px; - line-height: 40px; - cursor: pointer; -} - -.thaw-tabs-label__line { - position: absolute; - height: 3px; - background-color: var(--thaw-label-active-background-color); - border-radius: 3px; - bottom: 0; - left: 0; -} diff --git a/thaw/src/theme/common.rs b/thaw/src/theme/common.rs index 2baa548..64eadd0 100644 --- a/thaw/src/theme/common.rs +++ b/thaw/src/theme/common.rs @@ -59,6 +59,7 @@ pub struct CommonTheme { pub spacing_horizontal_l: String, pub spacing_vertical_s: String, pub spacing_vertical_m_nudge: String, + pub spacing_vertical_m: String, pub spacing_vertical_l: String, pub duration_ultra_fast: String, @@ -136,6 +137,7 @@ impl CommonTheme { spacing_horizontal_l: "16px".into(), spacing_vertical_s: "8px".into(), spacing_vertical_m_nudge: "10px".into(), + spacing_vertical_m: "12px".into(), spacing_vertical_l: "16px".into(), duration_ultra_fast: "50ms".into(),