diff --git a/thaw/src/components/css_transition/mod.rs b/thaw/src/components/css_transition/mod.rs index 7ad07e8..7f79a72 100644 --- a/thaw/src/components/css_transition/mod.rs +++ b/thaw/src/components/css_transition/mod.rs @@ -6,6 +6,7 @@ pub fn CSSTransition( node_ref: NodeRef, #[prop(into)] show: MaybeSignal, #[prop(into)] name: MaybeSignal, + #[prop(optional, into)] on_enter: Option>, #[prop(optional, into)] on_after_leave: Option>, 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"); diff --git a/thaw/src/modal/mod.rs b/thaw/src/modal/mod.rs index d4855c1..caeaa18 100644 --- a/thaw/src/modal/mod.rs +++ b/thaw/src/modal/mod.rs @@ -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,37 +13,99 @@ pub struct ModalFooter { #[component] pub fn Modal( #[prop(into)] show: Model, + #[prop(default = true.into(), into)] mask_closeable: MaybeSignal, + #[prop(default = 2000.into(), into)] z_index: MaybeSignal, #[prop(optional, into)] title: MaybeSignal, children: Children, #[prop(optional)] modal_footer: Option, ) -> 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::::new(); + let scroll_ref = NodeRef::::new(); + let modal_ref = NodeRef::::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! {
-
-
- - - {move || title.get()} - - - - - - - {children()} - - - {(footer.children)()} - - - -
+ +
+
+ +
+ +
+
} diff --git a/thaw/src/modal/modal.css b/thaw/src/modal/modal.css index 745b4bf..fc01fb8 100644 --- a/thaw/src/modal/modal.css +++ b/thaw/src/modal/modal.css @@ -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); +} diff --git a/thaw/src/utils/mod.rs b/thaw/src/utils/mod.rs index 59dc9ac..703e0b9 100644 --- a/thaw/src/utils/mod.rs +++ b/thaw/src/utils/mod.rs @@ -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(f: impl FnOnce() -> T) -> T { diff --git a/thaw/src/utils/use_click_position.rs b/thaw/src/utils/use_click_position.rs new file mode 100644 index 0000000..8c8cb43 --- /dev/null +++ b/thaw/src/utils/use_click_position.rs @@ -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::() 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> { + 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() +}