feat: optimize Toast component

This commit is contained in:
luoxiao 2024-07-11 23:30:35 +08:00
parent 0839db8ec4
commit 1d7237e6de
10 changed files with 320 additions and 66 deletions

View file

@ -1,14 +1,64 @@
# Toast # Toast
```rust demo ```rust demo
let toaster = ToasterInjection::use_(); let toaster = ToasterInjection::expect_context();
let on_click = move |_| { let on_click = move |_| {
toaster.dispatch_toast(view! { <span>"Hello"</span> }.into_any(), Default::default()); toaster.dispatch_toast(view! {
<Toast>
<ToastTitle>"Email sent"</ToastTitle>
<ToastBody>
"This is a toast body"
<ToastBodySubtitle slot>
"Subtitle"
</ToastBodySubtitle>
</ToastBody>
<ToastFooter>
"Footer"
// <Link>Action</Link>
// <Link>Action</Link>
</ToastFooter>
</Toast>
}.into_any(), Default::default());
}; };
view! { view! {
<Button on_click=on_click>"Make toast"</Button> <Button on_click=on_click>"Make toast"</Button>
} }
```
### Toast Positions
```rust demo
let toaster = ToasterInjection::expect_context();
let dispatch_toast = Callback::new(move |position| {
toaster.dispatch_toast(view! {
<Toast>
<ToastTitle>"Email sent"</ToastTitle>
<ToastBody>
"This is a toast body"
<ToastBodySubtitle slot>
"Subtitle"
</ToastBodySubtitle>
</ToastBody>
<ToastFooter>
"Footer"
// <Link>Action</Link>
// <Link>Action</Link>
</ToastFooter>
</Toast>
}.into_any(), ToastOptions::default().with_position(position));
});
view! {
<Space>
<Button on_click=move |_| dispatch_toast.call(ToastPosition::Bottom)>"Bottom"</Button>
<Button on_click=move |_| dispatch_toast.call(ToastPosition::BottomStart)>"BottomStart"</Button>
<Button on_click=move |_| dispatch_toast.call(ToastPosition::BottomEnd)>"BottomEnd"</Button>
<Button on_click=move |_| dispatch_toast.call(ToastPosition::Top)>"Top"</Button>
<Button on_click=move |_| dispatch_toast.call(ToastPosition::TopStart)>"Topstart"</Button>
<Button on_click=move |_| dispatch_toast.call(ToastPosition::TopEnd)>"TopEnd"</Button>
</Space>
}
``` ```

View file

@ -71,6 +71,10 @@ impl ConfigInjection {
pub fn use_() -> ConfigInjection { pub fn use_() -> ConfigInjection {
expect_context() expect_context()
} }
pub fn expect_context() -> Self {
expect_context()
}
} }
#[derive(Clone)] #[derive(Clone)]

View file

@ -109,6 +109,7 @@ pub struct ColorTheme {
pub color_transparent_stroke: String, pub color_transparent_stroke: String,
pub shadow4: String, pub shadow4: String,
pub shadow8: String,
pub shadow16: String, pub shadow16: String,
} }
@ -223,6 +224,7 @@ impl ColorTheme {
color_transparent_stroke: "transparent".into(), color_transparent_stroke: "transparent".into(),
shadow4: "0 0 2px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.14)".into(), shadow4: "0 0 2px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.14)".into(),
shadow8: "0 0 2px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.14)".into(),
shadow16: "0 0 2px rgba(0,0,0,0.12), 0 8px 16px rgba(0,0,0,0.14)".into(), shadow16: "0 0 2px rgba(0,0,0,0.12), 0 8px 16px rgba(0,0,0,0.14)".into(),
} }
} }
@ -337,6 +339,7 @@ impl ColorTheme {
color_transparent_stroke: "transparent".into(), color_transparent_stroke: "transparent".into(),
shadow4: "0 0 2px rgba(0,0,0,0.24), 0 2px 4px rgba(0,0,0,0.28)".into(), shadow4: "0 0 2px rgba(0,0,0,0.24), 0 2px 4px rgba(0,0,0,0.28)".into(),
shadow8: "0 0 2px rgba(0,0,0,0.24), 0 4px 8px rgba(0,0,0,0.28)".into(),
shadow16: "0 0 2px rgba(0,0,0,0.24), 0 8px 16px rgba(0,0,0,0.28)".into(), shadow16: "0 0 2px rgba(0,0,0,0.24), 0 8px 16px rgba(0,0,0,0.28)".into(),
} }
} }

View file

@ -22,7 +22,7 @@ pub struct ToasterInjection {
} }
impl ToasterInjection { impl ToasterInjection {
pub fn use_() -> Self { pub fn expect_context() -> Self {
expect_context() expect_context()
} }

View file

@ -25,20 +25,29 @@ impl ToastPosition {
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
match self { match self {
Self::Top => "top", Self::Top => "top",
Self::TopStart => "top-left", Self::TopStart => "top-start",
Self::TopEnd => "top-right", Self::TopEnd => "top-dnc",
Self::Bottom => "bottom", Self::Bottom => "bottom",
Self::BottomStart => "bottom-left", Self::BottomStart => "bottom-start",
Self::BottomEnd => "bottom-right", Self::BottomEnd => "bottom-end",
} }
} }
} }
#[derive(Debug, Clone)]
pub enum ToastIntent {
Success,
Info,
Warning,
Error,
}
#[derive(Clone)] #[derive(Clone)]
pub struct ToastOptions { pub struct ToastOptions {
pub(crate) id: uuid::Uuid, pub(crate) id: uuid::Uuid,
pub(crate) position: Option<ToastPosition>, pub(crate) position: Option<ToastPosition>,
pub(crate) timeout: Option<Duration>, pub(crate) timeout: Option<Duration>,
pub(crate) intent: Option<ToastIntent>,
} }
impl Default for ToastOptions { impl Default for ToastOptions {
@ -47,6 +56,24 @@ impl Default for ToastOptions {
id: uuid::Uuid::new_v4(), id: uuid::Uuid::new_v4(),
position: None, position: None,
timeout: None, timeout: None,
intent: None,
} }
} }
} }
impl ToastOptions {
pub fn with_position(mut self, position: ToastPosition) -> Self {
self.position = Some(position);
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_intent(mut self, intent: ToastIntent) -> Self {
self.intent = Some(intent);
self
}
}

View file

@ -7,9 +7,13 @@ pub fn ToastBody(
children: Children, children: Children,
) -> impl IntoView { ) -> impl IntoView {
view! { view! {
<div class="thaw-toast-body">
{children()} {children()}
</div>
<OptionComp value=toast_body_subtitle let:subtitle> <OptionComp value=toast_body_subtitle let:subtitle>
<div class="thaw-toast-body__subtitle">
{(subtitle.children)()} {(subtitle.children)()}
</div>
</OptionComp> </OptionComp>
} }
} }

View file

@ -2,5 +2,9 @@ use leptos::prelude::*;
#[component] #[component]
pub fn ToastFooter(children: Children) -> impl IntoView { pub fn ToastFooter(children: Children) -> impl IntoView {
children() view! {
<div class="thaw-toast-footer">
{children()}
</div>
}
} }

View file

@ -1,4 +1,4 @@
use leptos::prelude::*; use leptos::{either::Either, prelude::*};
use thaw_components::OptionComp; use thaw_components::OptionComp;
#[component] #[component]
@ -9,13 +9,25 @@ pub fn ToastTitle(
) -> impl IntoView { ) -> impl IntoView {
view! { view! {
<div class="thaw-toast-title__media"> <div class="thaw-toast-title__media">
{
if let Some(media) = toast_title_media {
Either::Left((media.children)())
} else {
Either::Right(view! {
<svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20"> <svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20">
<path d="M18 10a8 8 0 1 0-16 0 8 8 0 0 0 16 0ZM9.5 8.91a.5.5 0 0 1 1 0V13.6a.5.5 0 0 1-1 0V8.9Zm-.25-2.16a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Z" fill="currentColor"></path> <path d="M18 10a8 8 0 1 0-16 0 8 8 0 0 0 16 0ZM9.5 8.91a.5.5 0 0 1 1 0V13.6a.5.5 0 0 1-1 0V8.9Zm-.25-2.16a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Z" fill="currentColor"></path>
</svg> </svg>
})
}
}
</div> </div>
<div class="thaw-toast-title">
{children()} {children()}
</div>
<OptionComp value=toast_title_action let:action> <OptionComp value=toast_title_action let:action>
<div class="thaw-toast-title__action">
{(action.children)()} {(action.children)()}
</div>
</OptionComp> </OptionComp>
} }
} }

View file

@ -1,4 +1,4 @@
div.thaw-toaster { div.thaw-toaster-wrapper {
z-index: 1000000; z-index: 1000000;
position: absolute; position: absolute;
top: 0px; top: 0px;
@ -13,15 +13,51 @@ div.thaw-toaster {
color: var(--colorNeutralForeground1); color: var(--colorNeutralForeground1);
} }
.thaw-toaster-container { .thaw-toaster {
bottom: 16px;
right: 20px;
position: fixed; position: fixed;
width: 292px; width: 292px;
pointer-events: none; pointer-events: none;
} }
.thaw-toaster--top {
top: 16px;
left: calc(50% + 20px);
transform: translateX(-50%);
}
.thaw-toaster--top-start {
top: 16px;
left: 20px;
}
.thaw-toaster--top-end {
top: 16px;
right: 20px;
}
.thaw-toaster--bottom {
bottom: 16px;
left: calc(50% + 20px);
transform: translateX(-50%);
}
.thaw-toaster--bottom-start {
bottom: 16px;
left: 20px;
}
.thaw-toaster--bottom-end {
bottom: 16px;
right: 20px;
}
.thaw-toaster-container {
box-sizing: border-box;
margin-top: 16px;
pointer-events: all;
border-radius: var(--borderRadiusMedium);
}
.thaw-toaster-container.fade-in-height-expand-transition-leave-from, .thaw-toaster-container.fade-in-height-expand-transition-leave-from,
.thaw-toaster-container.fade-in-height-expand-transition-enter-to { .thaw-toaster-container.fade-in-height-expand-transition-enter-to {
transform: scale(1); transform: scale(1);
@ -95,3 +131,33 @@ div.thaw-toaster {
grid-column-end: -1; grid-column-end: -1;
color: var(--colorBrandForeground1); color: var(--colorBrandForeground1);
} }
.thaw-toast-body {
grid-column-start: 2;
grid-column-end: 3;
padding-top: 6px;
font-size: var(--fontSizeBase300);
line-height: var(--fontSizeBase300);
font-weight: var(--fontWeightRegular);
color: var(--colorNeutralForeground1);
word-break: break-word;
}
.thaw-toast-body__subtitle {
padding-top: 4px;
grid-column-start: 2;
grid-column-end: 3;
font-size: var(--fontSizeBase200);
line-height: var(--fontSizeBase200);
font-weight: var(--fontWeightRegular);
color: var(--colorNeutralForeground2);
}
.thaw-toast-footer {
padding-top: 16px;
grid-column-start: 2;
grid-column-end: 3;
display: flex;
align-items: center;
gap: 14px;
}

View file

@ -1,4 +1,5 @@
use super::{ToastOptions, ToastPosition, ToasterReceiver}; use super::{ToastOptions, ToastPosition, ToasterReceiver};
use crate::ConfigInjection;
use leptos::{either::Either, html, prelude::*, tachys::view::any_view::AnyView}; use leptos::{either::Either, html, prelude::*, tachys::view::any_view::AnyView};
use send_wrapper::SendWrapper; use send_wrapper::SendWrapper;
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, time::Duration};
@ -9,14 +10,29 @@ use thaw_utils::mount_style;
pub fn Toaster( pub fn Toaster(
receiver: ToasterReceiver, receiver: ToasterReceiver,
#[prop(optional)] position: ToastPosition, #[prop(optional)] position: ToastPosition,
#[prop(default = Duration::from_secs(3))] timeout: Duration, #[prop(default = Duration::from_secs(3000))] timeout: Duration,
) -> impl IntoView { ) -> impl IntoView {
mount_style("toaster", include_str!("./toaster.css")); mount_style("toaster", include_str!("./toaster.css"));
let config_provider = ConfigInjection::expect_context();
let top_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
let top_start_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
let top_end_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
let bottom_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
let bottom_start_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default()); let bottom_start_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
let bottom_end_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
let toasts = StoredValue::<HashMap<uuid::Uuid, (SendWrapper<AnyView<Dom>>, ToastOptions)>>::new( let toasts = StoredValue::<HashMap<uuid::Uuid, (SendWrapper<AnyView<Dom>>, ToastOptions)>>::new(
Default::default(), Default::default(),
); );
let id_list = move |position: &ToastPosition| match position {
ToastPosition::Top => top_id_list,
ToastPosition::TopStart => top_start_id_list,
ToastPosition::TopEnd => top_end_id_list,
ToastPosition::Bottom => bottom_id_list,
ToastPosition::BottomStart => bottom_start_id_list,
ToastPosition::BottomEnd => bottom_end_id_list,
};
Effect::new(move |_| { Effect::new(move |_| {
for (view, mut options) in receiver.try_recv() { for (view, mut options) in receiver.try_recv() {
if options.position.is_none() { if options.position.is_none() {
@ -25,44 +41,95 @@ pub fn Toaster(
if options.timeout.is_none() { if options.timeout.is_none() {
options.timeout = Some(timeout); options.timeout = Some(timeout);
} }
match options.position.unwrap() {
ToastPosition::Top => todo!(), let list = id_list(&options.position.unwrap());
ToastPosition::TopStart => todo!(),
ToastPosition::TopEnd => todo!(),
ToastPosition::Bottom => todo!(),
ToastPosition::BottomStart => {
let id = options.id; let id = options.id;
toasts.update_value(|map| { toasts.update_value(|map| {
map.insert(id, (SendWrapper::new(view), options)); map.insert(id, (SendWrapper::new(view), options));
}); });
bottom_start_id_list.update(|list| { list.update(|list| {
list.push(id); list.push(id);
}); });
} }
ToastPosition::BottomEnd => todo!(),
}
}
}); });
let on_close = move |(id, position)| match position { let on_close = move |(id, position)| {
ToastPosition::Top => todo!(), let list = id_list(&position);
ToastPosition::TopStart => todo!(), list.update(move |list| {
ToastPosition::TopEnd => todo!(),
ToastPosition::Bottom => todo!(),
ToastPosition::BottomStart => {
bottom_start_id_list.update(move |list| {
let Some(index) = list.iter().position(|item_id| &id == item_id) else { let Some(index) = list.iter().position(|item_id| &id == item_id) else {
return; return;
}; };
list.remove(index); list.remove(index);
}); });
}
ToastPosition::BottomEnd => todo!(),
}; };
view! { view! {
<Teleport> <Teleport>
<div class="thaw-config-provider thaw-toaster"> <div
class="thaw-config-provider thaw-toaster-wrapper"
data-thaw-id=config_provider.id().clone()
>
<div class="thaw-toaster thaw-toaster--top">
<For
each=move || top_id_list.get()
key=|id| id.clone()
let:id
>
{
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
} else {
Either::Right(())
}
}
</For>
</div>
<div class="thaw-toaster thaw-toaster--top-start">
<For
each=move || top_start_id_list.get()
key=|id| id.clone()
let:id
>
{
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
} else {
Either::Right(())
}
}
</For>
</div>
<div class="thaw-toaster thaw-toaster--top-end">
<For
each=move || top_end_id_list.get()
key=|id| id.clone()
let:id
>
{
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
} else {
Either::Right(())
}
}
</For>
</div>
<div class="thaw-toaster thaw-toaster--bottom">
<For
each=move || bottom_id_list.get()
key=|id| id.clone()
let:id
>
{
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
} else {
Either::Right(())
}
}
</For>
</div>
<div class="thaw-toaster thaw-toaster--bottom-start">
<For <For
each=move || bottom_start_id_list.get() each=move || bottom_start_id_list.get()
key=|id| id.clone() key=|id| id.clone()
@ -77,6 +144,22 @@ pub fn Toaster(
} }
</For> </For>
</div> </div>
<div class="thaw-toaster thaw-toaster--bottom-end">
<For
each=move || bottom_end_id_list.get()
key=|id| id.clone()
let:id
>
{
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
} else {
Either::Right(())
}
}
</For>
</div>
</div>
</Teleport> </Teleport>
} }
} }
@ -93,6 +176,7 @@ fn ToasterContainer(
id, id,
timeout, timeout,
position, position,
..
} = options; } = options;
let timeout = timeout.unwrap(); let timeout = timeout.unwrap();
let position = position.unwrap(); let position = position.unwrap();