refactor: Modal

This commit is contained in:
luoxiao 2024-06-28 15:54:20 +08:00
parent b52d826073
commit 568e629377
18 changed files with 290 additions and 256 deletions

View file

@ -61,6 +61,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
<Route path="/combobox" view=ComboboxMdPage/>
<Route path="/config-provider" view=ConfigProviderMdPage/>
<Route path="/date-picker" view=DatePickerMdPage/>
<Route path="/dialog" view=DialogMdPage/>
<Route path="/divider" view=DividerMdPage/>
<Route path="/drawer" view=DrawerMdPage/>
<Route path="/grid" view=GridMdPage/>
@ -71,7 +72,6 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
<Route path="/loading-bar" view=LoadingBarMdPage/>
// <Route path="/message" view=MessageMdPage/>
<Route path="/message-bar" view=MessageBarMdPage/>
<Route path="/modal" view=ModalMdPage/>
<Route path="/popover" view=PopoverMdPage/>
<Route path="/progress-bar" view=ProgressBarMdPage/>
<Route path="/radio" view=RadioMdPage/>

View file

@ -195,6 +195,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
value: "/components/date-picker".into(),
label: "Date Picker".into(),
},
MenuItemOption {
value: "/components/dialog".into(),
label: "Dialog".into(),
},
MenuItemOption {
value: "/components/divider".into(),
label: "Divider".into(),
@ -235,10 +239,6 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
value: "/components/message-bar".into(),
label: "Message Bar".into(),
},
MenuItemOption {
value: "/components/modal".into(),
label: "Modal".into(),
},
MenuItemOption {
value: "/components/popover".into(),
label: "Popover".into(),

View file

@ -1,13 +1,23 @@
# Modal
# Dialog
```rust demo
let show = create_rw_signal(false);
let open = RwSignal::new(false);
view! {
<Button on_click=move |_| show.set(true)>"Open Modal"</Button>
<Modal title="title" show>
"hello"
</Modal>
<Button on_click=move |_| open.set(true)>"Open Dialog"</Button>
<Dialog open>
<DialogSurface>
<DialogBody>
<DialogTitle>"Dialog title"</DialogTitle>
<DialogContent>
"Dialog body"
</DialogContent>
<DialogActions>
<Button appearance=ButtonAppearance::Primary>"Do Something"</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
}
```

View file

@ -44,6 +44,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"ComboboxMdPage" => "../docs/combobox/mod.md",
"ConfigProviderMdPage" => "../docs/config_provider/mod.md",
"DatePickerMdPage" => "../docs/date_picker/mod.md",
"DialogMdPage" => "../docs/dialog/mod.md",
"DividerMdPage" => "../docs/divider/mod.md",
"DrawerMdPage" => "../docs/drawer/mod.md",
"GridMdPage" => "../docs/grid/mod.md",
@ -54,7 +55,6 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"LoadingBarMdPage" => "../docs/loading_bar/mod.md",
// "MessageMdPage" => "../docs/message/mod.md",
"MessageBarMdPage" => "../docs/message_bar/mod.md",
"ModalMdPage" => "../docs/modal/mod.md",
"PopoverMdPage" => "../docs/popover/mod.md",
"ProgressBarMdPage" => "../docs/progress_bar/mod.md",
"RadioMdPage" => "../docs/radio/mod.md",

View file

@ -45,15 +45,6 @@ impl ButtonShape {
}
}
#[derive(Default, Clone)]
pub enum ButtonColor {
#[default]
Primary,
Success,
Warning,
Error,
}
#[derive(Default, PartialEq, Clone)]
pub enum ButtonSize {
Small,
@ -78,7 +69,6 @@ pub fn Button(
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional, into)] appearance: MaybeSignal<ButtonAppearance>,
#[prop(optional, into)] shape: MaybeSignal<ButtonShape>,
#[prop(optional, into)] color: MaybeSignal<ButtonColor>,
#[prop(optional, into)] size: MaybeSignal<ButtonSize>,
#[prop(optional, into)] block: MaybeSignal<bool>,
#[prop(optional, into)] icon: OptionalMaybeSignal<icondata_core::Icon>,

View file

@ -9,25 +9,10 @@ pub use card_preview::*;
use leptos::*;
use thaw_utils::{class_list, mount_style, OptionalProp};
#[slot]
pub struct CardHeaderExtra {
children: Children,
}
#[slot]
pub struct CardFooter {
#[prop(default = leptos::MaybeSignal::Static(true), into)]
if_: MaybeSignal<bool>,
children: ChildrenFn,
}
#[component]
pub fn Card(
#[prop(optional, into)] title: OptionalProp<MaybeSignal<String>>,
#[prop(optional)] card_header_extra: Option<CardHeaderExtra>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
children: Children,
#[prop(optional)] card_footer: Option<CardFooter>,
) -> impl IntoView {
mount_style("card", include_str!("./card.css"));

117
thaw/src/dialog/dialog.css Normal file
View file

@ -0,0 +1,117 @@
.thaw-dialog {
z-index: 1000000;
position: absolute;
top: 0px;
left: 0px;
right: 0px;
text-align: left;
}
.thaw-dialog-surface__backdrop {
inset: 0px;
background-color: rgba(0, 0, 0, 0.4);
position: fixed;
}
.thaw-dialog-surface__backdrop.fade-in-transition-enter-active {
transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1);
}
.thaw-dialog-surface__backdrop.fade-in-transition-leave-active {
transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1);
}
.thaw-dialog-surface__backdrop.fade-in-transition-enter-from,
.thaw-dialog-surface__backdrop.fade-in-transition-leave-to {
opacity: 0;
}
.thaw-dialog-surface__backdrop.fade-in-transition-leave-from,
.thaw-dialog-surface__backdrop.fade-in-transition-enter-to {
opacity: 1;
}
.thaw-dialog-surface {
inset: 0px;
padding: 24px;
margin: auto;
overflow: unset;
border: 1px solid var(--colorTransparentStroke);
border-radius: var(--borderRadiusXLarge);
display: block;
user-select: unset;
visibility: unset;
position: fixed;
height: fit-content;
max-width: 600px;
max-height: 100vh;
box-sizing: border-box;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
}
.thaw-dialog-surface.fade-in-scale-up-transition-leave-active {
transition: opacity 0.25s cubic-bezier(0.4, 0, 1, 1),
transform 0.25s cubic-bezier(0.4, 0, 1, 1);
}
.thaw-dialog-surface.fade-in-scale-up-transition-enter-active {
transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1),
transform 0.25s cubic-bezier(0, 0, 0.2, 1);
}
.thaw-dialog-surface.fade-in-scale-up-transition-enter-from,
.thaw-dialog-surface.fade-in-scale-up-transition-leave-to {
opacity: 0;
transform: scale(0.5);
}
.thaw-dialog-surface.fade-in-scale-up-transition-leave-from,
.thaw-dialog-surface.fade-in-scale-up-transition-enter-to {
opacity: 1;
transform: scale(1);
}
.thaw-dialog-body {
overflow: unset;
gap: 8px;
display: grid;
max-height: calc(-48px + 100vh);
box-sizing: border-box;
grid-template-rows: auto 1fr;
grid-template-columns: 1fr 1fr auto;
}
.thaw-dialog-title {
grid-area: 1 / 1 / 1 / 3;
grid-column-end: 4;
font-family: var(--fontFamilyBase);
font-size: var(--fontSizeBase500);
font-weight: var(--fontWeightSemibold);
line-height: var(--lineHeightBase500);
margin: 0px;
}
.thaw-dialog-content {
padding: var(--strokeWidthThick);
margin: calc(var(--strokeWidthThick) * -1);
font-family: var(--fontFamilyBase);
font-size: var(--fontSizeBase300);
font-weight: var(--fontWeightRegular);
line-height: var(--lineHeightBase300);
overflow-y: auto;
min-height: 32px;
box-sizing: border-box;
grid-area: 2 / 1 / 2 / 4;
}
.thaw-dialog-actions {
grid-column-start: 2;
justify-self: end;
grid-column-end: 4;
gap: 8px;
height: fit-content;
box-sizing: border-box;
display: flex;
grid-row: 3 / 3;
}

66
thaw/src/dialog/dialog.rs Normal file
View file

@ -0,0 +1,66 @@
use crate::ConfigInjection;
use leptos::*;
use thaw_components::{CSSTransition, FocusTrap, Teleport};
use thaw_utils::{mount_style, Model};
#[component]
pub fn Dialog(
#[prop(into)] open: Model<bool>,
#[prop(default = true.into(), into)] mask_closeable: MaybeSignal<bool>,
#[prop(default = true, into)] close_on_esc: bool,
children: Children,
) -> impl IntoView {
mount_style("dialog", include_str!("./dialog.css"));
let config_provider = ConfigInjection::use_();
let mask_ref = NodeRef::<html::Div>::new();
let on_mask_click = move |_| {
if mask_closeable.get_untracked() {
open.set(false);
}
};
let on_esc = Callback::new(move |_: ev::KeyboardEvent| {
open.set(false);
});
view! {
<Teleport immediate=open.signal()>
<FocusTrap disabled=!close_on_esc active=open.signal() on_esc>
<div
class="thaw-config-provider thaw-dialog"
data-thaw-id=config_provider.id().clone()
>
<CSSTransition
node_ref=mask_ref
appear=open.get_untracked()
show=open.signal()
name="fade-in-transition"
let:display
>
<div
class="thaw-dialog-surface__backdrop"
style=move || display.get()
on:click=on_mask_click
ref=mask_ref
aria-hidden="true"
></div>
</CSSTransition>
<Provider value=DialogInjection{open}>
{children()}
</Provider>
</div>
</FocusTrap>
</Teleport>
}
}
#[derive(Clone)]
pub(super) struct DialogInjection {
pub open: Model<bool>,
}
impl DialogInjection {
pub fn expect_use() -> Self {
expect_context::<Self>()
}
}

View file

@ -0,0 +1,10 @@
use leptos::*;
#[component]
pub fn DialogActions(children: Children) -> impl IntoView {
view! {
<div class="thaw-dialog-actions">
{children()}
</div>
}
}

View file

@ -0,0 +1,10 @@
use leptos::*;
#[component]
pub fn DialogBody(children: Children) -> impl IntoView {
view! {
<div class="thaw-dialog-body">
{children()}
</div>
}
}

View file

@ -0,0 +1,10 @@
use leptos::*;
#[component]
pub fn DialogContent(children: Children) -> impl IntoView {
view! {
<h2 class="thaw-dialog-content">
{children()}
</h2>
}
}

View file

@ -0,0 +1,29 @@
use super::dialog::DialogInjection;
use leptos::*;
use thaw_components::CSSTransition;
#[component]
pub fn DialogSurface(children: Children) -> impl IntoView {
let dialog = DialogInjection::expect_use();
let surface_ref = NodeRef::<html::Div>::new();
view! {
<CSSTransition
node_ref=surface_ref
appear=dialog.open.get_untracked()
show=dialog.open.signal()
name="fade-in-scale-up-transition"
let:display
>
<div
class="thaw-dialog-surface"
ref=surface_ref
role="dialog"
aria-modal="true"
style:display=move || display.get().map(|_| "none")
>
{children()}
</div>
</CSSTransition>
}
}

View file

@ -0,0 +1,10 @@
use leptos::*;
#[component]
pub fn DialogTitle(children: Children) -> impl IntoView {
view! {
<h2 class="thaw-dialog-title">
{children()}
</h2>
}
}

13
thaw/src/dialog/mod.rs Normal file
View file

@ -0,0 +1,13 @@
mod dialog;
mod dialog_actions;
mod dialog_body;
mod dialog_content;
mod dialog_surface;
mod dialog_title;
pub use dialog::*;
pub use dialog_actions::*;
pub use dialog_body::*;
pub use dialog_content::*;
pub use dialog_surface::*;
pub use dialog_title::*;

View file

@ -122,7 +122,7 @@ pub fn Drawer(
role="dialog"
aria-modal="true"
>
<Card title>
<Card>
<Scrollbar content_style="padding: 20px 28px;">
{children()}
</Scrollbar>

View file

@ -14,6 +14,7 @@ mod color_picker;
mod combobox;
mod config_provider;
mod date_picker;
mod dialog;
mod divider;
mod drawer;
mod grid;
@ -24,7 +25,6 @@ mod layout;
mod loading_bar;
mod message;
mod message_bar;
mod modal;
mod nav;
mod popover;
mod progress_bar;
@ -61,6 +61,7 @@ pub use color_picker::*;
pub use combobox::*;
pub use config_provider::*;
pub use date_picker::*;
pub use dialog::*;
pub use divider::*;
pub use drawer::*;
pub use grid::*;
@ -71,7 +72,6 @@ pub use layout::*;
pub use loading_bar::*;
pub use message::*;
pub use message_bar::*;
pub use modal::*;
pub use nav::*;
pub use popover::*;
pub use progress_bar::*;

View file

@ -1,141 +0,0 @@
use crate::{Card, CardFooter, CardHeader, CardHeaderExtra, Icon, Scrollbar, ScrollbarRef};
use leptos::*;
use thaw_components::{CSSTransition, FocusTrap, OptionComp, Teleport};
use thaw_utils::{mount_style, use_click_position, ComponentRef, Model};
#[slot]
pub struct ModalFooter {
children: ChildrenFn,
}
#[component]
pub fn Modal(
#[prop(into)] show: Model<bool>,
#[prop(default = true.into(), into)] mask_closeable: MaybeSignal<bool>,
#[prop(default = true, into)] close_on_esc: bool,
#[prop(default = 2000.into(), into)] z_index: MaybeSignal<i16>,
#[prop(default = MaybeSignal::Static("600px".to_string()), into)] width: MaybeSignal<String>,
#[prop(optional, into)] title: MaybeSignal<String>,
children: Children,
#[prop(optional)] modal_footer: Option<ModalFooter>,
) -> impl IntoView {
mount_style("modal", include_str!("./modal.css"));
let displayed = RwSignal::new(show.get_untracked());
Effect::new(move |prev| {
let is_show = show.get();
if prev.is_some() && is_show {
displayed.set(true);
}
});
let on_mask_click = move |_| {
if mask_closeable.get_untracked() {
show.set(false);
}
};
let on_esc = Callback::new(move |_: ev::KeyboardEvent| {
show.set(false);
});
let mask_ref = NodeRef::<html::Div>::new();
let scrollbar_ref = ComponentRef::<ScrollbarRef>::new();
let modal_ref = NodeRef::<html::Div>::new();
let click_position = use_click_position();
let on_enter = Callback::new(move |_| {
let Some(position) = click_position.get_untracked() else {
return;
};
let Some(scroll_el) = scrollbar_ref.get_untracked() else {
return;
};
let scroll_top = scroll_el.container_scroll_top();
let Some(modal_el) = modal_ref.get_untracked() else {
return;
};
let x = -(modal_el.offset_left() - position.0);
let y = -(modal_el.offset_top() - position.1 - scroll_top);
let _ = modal_el.attr("style", format!("transform-origin: {}px {}px", x, y));
});
view! {
<Teleport immediate=show.signal()>
<FocusTrap disabled=!close_on_esc active=show.signal() on_esc>
<div
class="thaw-modal-container"
style:z-index=move || z_index.get()
style=("--thaw-width", move || width.get())
>
<Scrollbar
content_style="min-height: 100%; display: flex;"
style=Signal::derive(move || {
if displayed.get() {
String::new()
} else {
String::from("display: none")
}
})
comp_ref=scrollbar_ref
>
<CSSTransition
node_ref=mask_ref
appear=show.get_untracked()
show=show.signal()
name="fade-in-transition"
let:display
>
<div
class="thaw-modal-mask"
style=move || display.get()
on:click=on_mask_click
ref=mask_ref
></div>
</CSSTransition>
<CSSTransition
node_ref=modal_ref
appear=show.get_untracked()
show=show.signal()
name="fade-in-scale-up-transition"
on_enter
on_after_leave=move |_| displayed.set(false)
let:display
>
<div
class="thaw-modal-body"
ref=modal_ref
role="dialog"
aria-modal="true"
style=move || display.get()
>
<Card>
<CardHeader>
<span class="thaw-model-title">{move || title.get()}</span>
</CardHeader>
<CardHeaderExtra slot>
<span
style="cursor: pointer;"
on:click=move |_| show.set(false)
>
<Icon icon=icondata_ai::AiCloseOutlined/>
</span>
</CardHeaderExtra>
{children()}
<CardFooter slot if_=modal_footer.is_some()>
<OptionComp value=modal_footer.as_ref() let:footer>
{(footer.children)()}
</OptionComp>
</CardFooter>
</Card>
</div>
</CSSTransition>
</Scrollbar>
</div>
</FocusTrap>
</Teleport>
}
}

View file

@ -1,75 +0,0 @@
.thaw-modal-container {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
display: flex;
min-height: 100%;
pointer-events: none;
}
.thaw-modal-container > * {
pointer-events: auto;
}
.thaw-modal-mask {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: #0007;
}
.thaw-modal-mask.fade-in-transition-enter-active {
transition: all 0.25s cubic-bezier(0, 0, 0.2, 1);
}
.thaw-modal-mask.fade-in-transition-leave-active {
transition: all 0.25s cubic-bezier(0, 0, 0.2, 1);
}
.thaw-modal-mask.fade-in-transition-enter-from,
.thaw-modal-mask.fade-in-transition-leave-to {
opacity: 0;
}
.thaw-modal-mask.fade-in-transition-leave-from,
.thaw-modal-mask.fade-in-transition-enter-to {
opacity: 1;
}
.thaw-modal-body {
position: relative;
margin: auto;
width: var(--thaw-width);
}
.thaw-model-title {
font-size: 16px;
}
.fade-in-scale-up-transition-leave-active.thaw-modal-body {
transform-origin: inherit;
transition: opacity 0.25s cubic-bezier(0.4, 0, 1, 1),
transform 0.25s cubic-bezier(0.4, 0, 1, 1);
}
.fade-in-scale-up-transition-enter-active.thaw-modal-body {
transform-origin: inherit;
transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1),
transform 0.25s cubic-bezier(0, 0, 0.2, 1);
}
.fade-in-scale-up-transition-enter-from.thaw-modal-body,
.fade-in-scale-up-transition-leave-to.thaw-modal-body {
opacity: 0;
transform: scale(0.5);
}
.fade-in-scale-up-transition-leave-from.thaw-modal-body,
.fade-in-scale-up-transition-enter-to.thaw-modal-body {
opacity: 1;
transform: scale(1);
}