feat: Modal adds mask_closeable and z_index prop

This commit is contained in:
luoxiao 2024-02-15 20:43:28 +08:00 committed by luoxiaozero
parent 396e654f0b
commit e62d156068
5 changed files with 198 additions and 39 deletions

View file

@ -6,6 +6,7 @@ pub fn CSSTransition<T, CF, IV>(
node_ref: NodeRef<T>,
#[prop(into)] show: MaybeSignal<bool>,
#[prop(into)] name: MaybeSignal<String>,
#[prop(optional, into)] on_enter: Option<Callback<()>>,
#[prop(optional, into)] on_after_leave: Option<Callback<()>>,
children: CF,
) -> impl IntoView
@ -71,6 +72,9 @@ where
let _ = class_list.add_1(&enter_to);
remove_class_name
.set_value(Some(RemoveClassName::Enter(enter_active, enter_to)));
if let Some(on_enter) = on_enter {
on_enter.call(());
}
});
} else if !show && prev {
let leave_from = format!("{name}-leave-from");

View file

@ -1,9 +1,7 @@
use crate::icon::*;
use crate::utils::Model;
use crate::{
components::{OptionComp, Teleport},
utils::mount_style,
Card, CardFooter, CardHeader, CardHeaderExtra,
components::{CSSTransition, OptionComp, Teleport},
utils::{mount_style, use_click_position, Model},
Card, CardFooter, CardHeader, CardHeaderExtra, Icon,
};
use leptos::*;
@ -15,20 +13,80 @@ pub struct ModalFooter {
#[component]
pub fn Modal(
#[prop(into)] show: Model<bool>,
#[prop(default = true.into(), into)] mask_closeable: MaybeSignal<bool>,
#[prop(default = 2000.into(), into)] z_index: MaybeSignal<i16>,
#[prop(optional, into)] title: MaybeSignal<String>,
children: Children,
#[prop(optional)] modal_footer: Option<ModalFooter>,
) -> impl IntoView {
mount_style("modal", include_str!("./modal.css"));
let on_mask_click = move |_| {
if mask_closeable.get_untracked() {
show.set(false);
}
};
let mask_ref = NodeRef::<html::Div>::new();
let scroll_ref = NodeRef::<html::Div>::new();
let modal_ref = NodeRef::<html::Div>::new();
let click_position = use_click_position();
let on_enter = move |_| {
let Some(position) = click_position.get_untracked() else {
return;
};
let Some(scroll_el) = scroll_ref.get_untracked() else {
return;
};
let scroll_top = scroll_el.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>
<div
class="thaw-modal-container"
style=move || if show.get() { "" } else { "display: none" }
style=move || format!("z-index: {};", z_index.get())
>
<CSSTransition
node_ref=mask_ref
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=scroll_ref
show=show.signal()
name="fade-in-scale-up-transition"
on_enter
let:display
>
<div
class="thaw-modal-scroll"
style=move || display.get()
ref=scroll_ref
>
<div
class="thaw-modal-body"
ref=modal_ref
role="dialog"
aria-modal="true"
>
<div class="thaw-modal-mask"></div>
<div class="thaw-modal-body">
<Card>
<CardHeader slot>
<span class="thaw-model-title">{move || title.get()}</span>
@ -47,6 +105,8 @@ pub fn Modal(
</Card>
</div>
</div>
</CSSTransition>
</div>
</Teleport>
}
}

View file

@ -1,6 +1,25 @@
.thaw-modal-container {
z-index: 2001;
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-scroll {
display: flex;
min-height: 100%;
min-width: 100%;
overflow: scroll;
}
.thaw-modal-mask {
position: fixed;
top: 0;
@ -8,24 +27,56 @@
left: 0;
bottom: 0;
background-color: #0007;
z-index: 2000;
}
.thaw-modal-body {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
min-height: 100%;
z-index: 2002;
}
.thaw-modal-body .thaw-card {
width: 600px;
.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: 600px;
}
.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);
}

View file

@ -8,6 +8,7 @@ mod optional_prop;
mod signal;
mod stored_maybe_signal;
mod time;
mod use_click_position;
mod use_lock_html_scroll;
// pub use callback::AsyncCallback;
@ -19,6 +20,7 @@ pub(crate) use optional_prop::OptionalProp;
pub use signal::SignalWatch;
pub(crate) use stored_maybe_signal::*;
pub(crate) use time::*;
pub(crate) use use_click_position::*;
pub(crate) use use_lock_html_scroll::*;
pub(crate) fn with_hydration_off<T>(f: impl FnOnce() -> T) -> T {

View file

@ -0,0 +1,42 @@
use leptos::{ev, on_cleanup, window_event_listener, ReadSignal, RwSignal, SignalSet};
use wasm_bindgen::JsCast;
use web_sys::MouseEvent;
fn click_handler(event: MouseEvent) -> Option<(i32, i32)> {
if event.client_x() > 0 || event.client_y() > 0 {
return Some((event.client_x(), event.client_y()));
}
let Some(target) = event.target() else {
return None;
};
let Ok(target) = target.dyn_into::<web_sys::Element>() else {
return None;
};
let rect = target.get_bounding_client_rect();
let left = rect.left() as i32;
let top = rect.top() as i32;
let width = rect.width() as i32;
let height = rect.height() as i32;
if left > 0 || top > 0 {
Some((left + width / 2, top + height / 2))
} else {
Some((0, 0))
}
}
pub fn use_click_position() -> ReadSignal<Option<(i32, i32)>> {
let mouse_position = RwSignal::new(None);
#[cfg(any(feature = "csr", feature = "hydrate"))]
{
let handle = window_event_listener(ev::click, move |event| {
let position = click_handler(event);
mouse_position.set(position);
});
on_cleanup(move || {
handle.remove();
});
}
mouse_position.read_only()
}