mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-23 06:19:22 -05:00
refactor: tab
This commit is contained in:
parent
18bfea1731
commit
c7da724cdc
7 changed files with 143 additions and 273 deletions
|
@ -1,44 +1,17 @@
|
||||||
# Tabs
|
# Tabs
|
||||||
|
|
||||||
```rust demo
|
```rust demo
|
||||||
let value = create_rw_signal(String::from("apple"));
|
let selected_value = RwSignal::new(String::new());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Tabs value>
|
<TabList selected_value>
|
||||||
<Tab key="apple" label="Apple">
|
<Tab value="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">
|
|
||||||
"🍎 Apple"
|
"🍎 Apple"
|
||||||
</TabLabel>
|
|
||||||
"apple"
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key="pear">
|
<Tab value="pear">
|
||||||
<TabLabel slot>
|
|
||||||
"🍐 Pear"
|
"🍐 Pear"
|
||||||
</TabLabel>
|
|
||||||
"pear"
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</TabList>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -2,189 +2,72 @@ mod tab;
|
||||||
|
|
||||||
pub use tab::*;
|
pub use tab::*;
|
||||||
|
|
||||||
use crate::{theme::use_theme, Theme};
|
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use thaw_utils::{class_list, mount_style, Model, OptionalProp};
|
use std::collections::HashMap;
|
||||||
|
use thaw_utils::{class_list, mount_style, Model};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Tabs(
|
pub fn TabList(
|
||||||
#[prop(optional, into)] value: Model<String>,
|
#[prop(optional, into)] selected_value: Model<String>,
|
||||||
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style("tabs", include_str!("./tabs.css"));
|
mount_style("tab-list", include_str!("./tab-list.css"));
|
||||||
let tab_options_vec = create_rw_signal(vec![]);
|
|
||||||
|
|
||||||
|
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! {
|
view! {
|
||||||
<Provider value=TabsInjection {
|
<Provider value=TabListInjection {
|
||||||
active_key: value,
|
selected_value,
|
||||||
tab_options_vec,
|
registered_tabs,
|
||||||
}>
|
}>
|
||||||
<TabsInner class value tab_options_vec children/>
|
<div
|
||||||
|
class=class_list!["thaw-tab-list", class]
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
|
{children()}
|
||||||
|
</div>
|
||||||
</Provider>
|
</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)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct TabsLabelLine {
|
pub(crate) struct TabListInjection {
|
||||||
width: f64,
|
pub selected_value: Model<String>,
|
||||||
left: f64,
|
registered_tabs: RwSignal<HashMap<String, TabRegisterData>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
impl Copy for TabListInjection {}
|
||||||
pub(crate) struct TabsInjection {
|
|
||||||
active_key: Model<String>,
|
|
||||||
tab_options_vec: RwSignal<Vec<TabOption>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TabsInjection {
|
impl TabListInjection {
|
||||||
pub fn get_key(&self) -> String {
|
pub fn use_() -> Self {
|
||||||
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 {
|
|
||||||
expect_context()
|
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>,
|
||||||
}
|
}
|
||||||
|
|
8
thaw/src/tabs/tab-list.css
Normal file
8
thaw/src/tabs/tab-list.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.thaw-tab-list {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
|
@ -1,7 +1,61 @@
|
||||||
.thaw-tab {
|
.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 {
|
.thaw-tab::before {
|
||||||
display: none;
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +1,44 @@
|
||||||
use super::use_tabs;
|
use super::{TabListInjection, TabRegisterData};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use thaw_utils::{class_list, mount_style, OptionalProp};
|
use thaw_utils::{class_list, mount_style};
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Tab(
|
pub fn Tab(
|
||||||
#[prop(into)] key: String,
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
#[prop(optional, into)] label: String,
|
#[prop(into)] value: String,
|
||||||
#[prop(optional)] tab_label: Option<TabLabel>,
|
|
||||||
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
|
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style("tab", include_str!("./tab.css"));
|
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 tab_ref = NodeRef::<html::Button>::new();
|
||||||
let key = key.clone();
|
let tab_list = TabListInjection::use_();
|
||||||
let tabs = tabs.clone();
|
let value = StoredValue::new(value);
|
||||||
move |_| key == tabs.get_key()
|
tab_list.register(TabRegisterData {
|
||||||
|
value: value.get_value(),
|
||||||
|
tab_ref,
|
||||||
});
|
});
|
||||||
|
|
||||||
on_cleanup(move || {
|
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! {
|
view! {
|
||||||
<div
|
<button
|
||||||
class=class_list![
|
class=class_list![
|
||||||
"thaw-tab", ("thaw-tab--hidden", move || ! is_active.get()), class.map(| c | move ||
|
"thaw-tab", ("thaw-tab--hidden", move || selected.get()), class
|
||||||
c.get())
|
|
||||||
]
|
]
|
||||||
|
role="tab"
|
||||||
role="tabpanel"
|
aria-selected=move || if selected.get() { "true" } else { "false" }
|
||||||
aria-hidden=move || if is_active.get() { "false" } else { "true" }
|
ref=tab_ref
|
||||||
>
|
>
|
||||||
|
<span class="thaw-tab__content">
|
||||||
{children()}
|
{children()}
|
||||||
</div>
|
</span>
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -59,6 +59,7 @@ pub struct CommonTheme {
|
||||||
pub spacing_horizontal_l: String,
|
pub spacing_horizontal_l: String,
|
||||||
pub spacing_vertical_s: String,
|
pub spacing_vertical_s: String,
|
||||||
pub spacing_vertical_m_nudge: String,
|
pub spacing_vertical_m_nudge: String,
|
||||||
|
pub spacing_vertical_m: String,
|
||||||
pub spacing_vertical_l: String,
|
pub spacing_vertical_l: String,
|
||||||
|
|
||||||
pub duration_ultra_fast: String,
|
pub duration_ultra_fast: String,
|
||||||
|
@ -136,6 +137,7 @@ impl CommonTheme {
|
||||||
spacing_horizontal_l: "16px".into(),
|
spacing_horizontal_l: "16px".into(),
|
||||||
spacing_vertical_s: "8px".into(),
|
spacing_vertical_s: "8px".into(),
|
||||||
spacing_vertical_m_nudge: "10px".into(),
|
spacing_vertical_m_nudge: "10px".into(),
|
||||||
|
spacing_vertical_m: "12px".into(),
|
||||||
spacing_vertical_l: "16px".into(),
|
spacing_vertical_l: "16px".into(),
|
||||||
|
|
||||||
duration_ultra_fast: "50ms".into(),
|
duration_ultra_fast: "50ms".into(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue