refactor: tab

This commit is contained in:
luoxiao 2024-06-04 17:30:40 +08:00
parent 18bfea1731
commit c7da724cdc
7 changed files with 143 additions and 273 deletions

View file

@ -1,44 +1,17 @@
# Tabs
```rust demo
let value = create_rw_signal(String::from("apple"));
let selected_value = RwSignal::new(String::new());
view! {
<Tabs value>
<Tab key="apple" label="Apple">
"apple"
</Tab>
<Tab key="pear" label="Pear">
"pear"
</Tab>
</Tabs>
}
```
### Custom tab label
```rust demo
use leptos_meta::Style;
let value = create_rw_signal(String::from("apple"));
view! {
<Style id="demo-tab-label">
".p-0 { padding: 0 }"
</Style>
<Tabs value>
<Tab key="apple">
<TabLabel slot class="p-0">
<TabList selected_value>
<Tab value="apple">
"🍎 Apple"
</TabLabel>
"apple"
</Tab>
<Tab key="pear">
<TabLabel slot>
<Tab value="pear">
"🍐 Pear"
</TabLabel>
"pear"
</Tab>
</Tabs>
</TabList>
}
```

View file

@ -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<String>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
pub fn TabList(
#[prop(optional, into)] selected_value: Model<String>,
#[prop(optional, into)] class: MaybeProp<String>,
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! {
<Provider value=TabsInjection {
active_key: value,
tab_options_vec,
<Provider value=TabListInjection {
selected_value,
registered_tabs,
}>
<TabsInner class value tab_options_vec children/>
<div
class=class_list!["thaw-tab-list", class]
role="tablist"
>
{children()}
</div>
</Provider>
}
}
#[component]
fn TabsInner(
value: Model<String>,
tab_options_vec: RwSignal<Vec<TabOption>>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
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::<Option<TabsLabelLine>>(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::<html::Div>();
let children = children();
view! {
<div
class=class_list!["thaw-tabs", class.map(| c | move || c.get())]
style=move || css_vars.get()
>
<div class="thaw-tabs__label-list" ref=label_list_ref role="tablist">
<For
each=move || tab_options_vec.get()
key=move |v| v.key.clone()
children=move |option| {
let label_ref = create_node_ref::<html::Span>();
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! {
<span
class=class_list![
"thaw-tabs__label", ("thaw-tabs__label--active", move ||
is_active.get()), class.map(| c | move || c.get())
]
on:click={
let key = key.clone();
move |_| value.set(key.clone())
}
ref=label_ref
role="tab"
aria-selected=move || {
if is_active.get() { "true" } else { "false" }
}
>
{children}
</span>
}
} else {
view! {
<span
class="thaw-tabs__label"
class=("thaw-tabs__label--active", move || is_active.get())
on:click={
let key = key.clone();
move |_| value.set(key.clone())
}
ref=label_ref
role="tab"
aria-selected=move || {
if is_active.get() { "true" } else { "false" }
}
>
{if label.is_empty() { key } else { label }}
</span>
}
}
}
/>
<span class="thaw-tabs-label__line" style=move || label_line_style.get()></span>
</div>
<div>{children}</div>
</div>
}
}
#[derive(Clone)]
pub(crate) struct TabsLabelLine {
width: f64,
left: f64,
pub(crate) struct TabListInjection {
pub selected_value: Model<String>,
registered_tabs: RwSignal<HashMap<String, TabRegisterData>>,
}
#[derive(Clone)]
pub(crate) struct TabsInjection {
active_key: Model<String>,
tab_options_vec: RwSignal<Vec<TabOption>>,
}
impl Copy for TabListInjection {}
impl TabsInjection {
pub fn get_key(&self) -> String {
self.active_key.get()
}
pub(crate) fn push_tab_options(&self, options: TabOption) {
self.tab_options_vec.update(|v| {
v.push(options);
});
}
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(crate) fn use_tabs() -> TabsInjection {
impl TabListInjection {
pub fn use_() -> Self {
expect_context()
}
pub fn register(&self, data: TabRegisterData) {
self.registered_tabs.update(|map| {
map.insert(data.value.clone(), data);
});
}
pub fn unregister(&self, value: &String) {
self.registered_tabs.update(|map| {
map.remove(value);
});
}
}
pub(crate) struct TabRegisterData {
value: String,
tab_ref: NodeRef<html::Button>,
}

View file

@ -0,0 +1,8 @@
.thaw-tab-list {
position: relative;
display: flex;
flex-wrap: nowrap;
flex-direction: row;
flex-shrink: 0;
align-items: stretch;
}

View file

@ -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);
}

View file

@ -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<TabLabelView>,
}
#[derive(Clone)]
pub(crate) struct TabLabelView {
pub class: OptionalProp<MaybeSignal<String>>,
pub children: Fragment,
}
impl From<TabLabel> 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<MaybeSignal<String>>,
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<TabLabel>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional, into)] class: MaybeProp<String>,
#[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::<html::Button>::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! {
<div
<button
class=class_list![
"thaw-tab", ("thaw-tab--hidden", move || ! is_active.get()), class.map(| c | move ||
c.get())
"thaw-tab", ("thaw-tab--hidden", move || selected.get()), class
]
role="tabpanel"
aria-hidden=move || if is_active.get() { "false" } else { "true" }
role="tab"
aria-selected=move || if selected.get() { "true" } else { "false" }
ref=tab_ref
>
<span class="thaw-tab__content">
{children()}
</div>
</span>
</button>
}
}

View file

@ -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;
}

View file

@ -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(),