Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Modal adds mask_closeable and z_index prop #111

Merged
merged 1 commit into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions thaw/src/components/css_transition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down
110 changes: 85 additions & 25 deletions thaw/src/modal/mod.rs
Original file line number Diff line number Diff line change
@@ -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::*;

Expand All @@ -15,37 +13,99 @@ 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())
>
<div class="thaw-modal-mask"></div>
<div class="thaw-modal-body">
<Card>
<CardHeader slot>
<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
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"
>
<Card>
<CardHeader slot>
<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>
</div>
</CSSTransition>
</div>
</Teleport>
}
Expand Down
75 changes: 63 additions & 12 deletions thaw/src/modal/modal.css
Original file line number Diff line number Diff line change
@@ -1,31 +1,82 @@
.thaw-modal-container {
z-index: 2001;
}
.thaw-modal-mask {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: #0007;
z-index: 2000;
display: flex;
min-height: 100%;
pointer-events: none;
}
.thaw-modal-body {

.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;
right: 0;
bottom: 0;
left: 0;
display: flex;
min-height: 100%;
z-index: 2002;
bottom: 0;
background-color: #0007;
}

.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);
}
2 changes: 2 additions & 0 deletions thaw/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions thaw/src/utils/use_click_position.rs
Original file line number Diff line number Diff line change
@@ -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()
}
Loading