mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09: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
|
||||
|
||||
```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"
|
||||
<TabList selected_value>
|
||||
<Tab value="apple">
|
||||
"🍎 Apple"
|
||||
</Tab>
|
||||
<Tab key="pear" label="Pear">
|
||||
"pear"
|
||||
<Tab value="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"
|
||||
</TabLabel>
|
||||
"apple"
|
||||
</Tab>
|
||||
<Tab key="pear">
|
||||
<TabLabel slot>
|
||||
"🍐 Pear"
|
||||
</TabLabel>
|
||||
"pear"
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TabList>
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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()
|
||||
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<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 {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
>
|
||||
{children()}
|
||||
</div>
|
||||
<span class="thaw-tab__content">
|
||||
{children()}
|
||||
</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_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(),
|
||||
|
|
Loading…
Add table
Reference in a new issue