From c1a9f84c9e89abc454a2216dc384116d788cfba4 Mon Sep 17 00:00:00 2001 From: luoxiaozero <48741584+luoxiaozero@users.noreply.github.com> Date: Wed, 6 Mar 2024 13:08:06 +0800 Subject: [PATCH] Feat/binder animation (#131) * feat: Popover adds animation * feat: ColorPicker adds animation * feat: AutoComplete adds animation * feat: Select adds animation * feat: DatePicker adds animation * feat: TimePicker adds animation --- thaw/src/auto_complete/auto-complete.css | 28 +++ thaw/src/auto_complete/mod.rs | 123 ++++++------ thaw/src/color_picker/color-picker.css | 24 +++ thaw/src/color_picker/mod.rs | 22 ++- .../components/binder/get_placement_style.rs | 17 ++ thaw/src/components/binder/mod.rs | 41 ++-- thaw/src/date_picker/date-picker.css | 24 +++ thaw/src/date_picker/mod.rs | 1 + thaw/src/date_picker/panel/mod.rs | 43 ++-- thaw/src/popover/mod.rs | 36 ++-- thaw/src/popover/popover.css | 29 +++ thaw/src/select/mod.rs | 63 +++--- thaw/src/select/select.css | 28 +++ thaw/src/time_picker/mod.rs | 187 ++++++++++-------- thaw/src/time_picker/time-picker.css | 24 +++ 15 files changed, 462 insertions(+), 228 deletions(-) diff --git a/thaw/src/auto_complete/auto-complete.css b/thaw/src/auto_complete/auto-complete.css index 4ca4cd6..16bb61c 100644 --- a/thaw/src/auto_complete/auto-complete.css +++ b/thaw/src/auto_complete/auto-complete.css @@ -18,3 +18,31 @@ .thaw-auto-complete__menu-item--selected { background-color: var(--thaw-background-color-hover); } + +.thaw-auto-complete__menu.fade-in-scale-up-transition-leave-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), + transform 0.2s cubic-bezier(0.4, 0, 1, 1), + background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.thaw-auto-complete__menu.fade-in-scale-up-transition-enter-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), + transform 0.2s cubic-bezier(0, 0, 0.2, 1), + background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.thaw-auto-complete__menu.fade-in-scale-up-transition-enter-from, +.thaw-auto-complete__menu.fade-in-scale-up-transition-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.thaw-auto-complete__menu.fade-in-scale-up-transition-leave-from, +.thaw-auto-complete__menu.fade-in-scale-up-transition-enter-to { + opacity: 1; + transform: scale(1); +} diff --git a/thaw/src/auto_complete/mod.rs b/thaw/src/auto_complete/mod.rs index 5c85f77..30e308c 100644 --- a/thaw/src/auto_complete/mod.rs +++ b/thaw/src/auto_complete/mod.rs @@ -1,7 +1,7 @@ mod theme; use crate::{ - components::{Binder, Follower, FollowerPlacement, FollowerWidth}, + components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth}, use_theme, utils::{class_list::class_list, mount_style, Model, OptionalProp, StoredMaybeSignal}, ComponentRef, Input, InputPrefix, InputRef, InputSuffix, Theme, @@ -196,69 +196,76 @@ pub fn AutoComplete( placement=FollowerPlacement::BottomStart width=FollowerWidth::Target > -
+
- {move || { - options - .get() - .into_iter() - .enumerate() - .map(|(index, v)| { - let AutoCompleteOption { value: option_value, label } = v; - let menu_item_ref = create_node_ref::(); - let on_click = move |_| { - select_value(option_value.clone()); - }; - let on_mouseenter = move |_| { - select_option_index.set(Some(index)); - }; - let on_mousedown = move |ev: ev::MouseEvent| { - ev.prevent_default(); - }; - create_effect(move |_| { - if Some(index) == select_option_index.get() { - if !is_show_menu.get() { - return; - } - if let Some(menu_item_ref) = menu_item_ref.get() { - let menu_ref = menu_ref.get().unwrap(); - let menu_rect = menu_ref.get_bounding_client_rect(); - let item_rect = menu_item_ref.get_bounding_client_rect(); - if item_rect.y() < menu_rect.y() { - menu_item_ref.scroll_into_view_with_bool(true); - } else if item_rect.y() + item_rect.height() - > menu_rect.y() + menu_rect.height() - { - menu_item_ref.scroll_into_view_with_bool(false); + {move || { + options + .get() + .into_iter() + .enumerate() + .map(|(index, v)| { + let AutoCompleteOption { value: option_value, label } = v; + let menu_item_ref = create_node_ref::(); + let on_click = move |_| { + select_value(option_value.clone()); + }; + let on_mouseenter = move |_| { + select_option_index.set(Some(index)); + }; + let on_mousedown = move |ev: ev::MouseEvent| { + ev.prevent_default(); + }; + create_effect(move |_| { + if Some(index) == select_option_index.get() { + if !is_show_menu.get() { + return; + } + if let Some(menu_item_ref) = menu_item_ref.get() { + let menu_ref = menu_ref.get().unwrap(); + let menu_rect = menu_ref.get_bounding_client_rect(); + let item_rect = menu_item_ref.get_bounding_client_rect(); + if item_rect.y() < menu_rect.y() { + menu_item_ref.scroll_into_view_with_bool(true); + } else if item_rect.y() + item_rect.height() + > menu_rect.y() + menu_rect.height() + { + menu_item_ref.scroll_into_view_with_bool(false); + } } } + }); + view! { +
+ {label} +
} - }); - view! { -
- {label} -
- } - }) - .collect_view() - }} - -
+
+ } diff --git a/thaw/src/color_picker/color-picker.css b/thaw/src/color_picker/color-picker.css index ad01a76..31e4528 100644 --- a/thaw/src/color_picker/color-picker.css +++ b/thaw/src/color_picker/color-picker.css @@ -89,3 +89,27 @@ border: 2px solid white; cursor: pointer; } + +.thaw-color-picker-popover.fade-in-scale-up-transition-leave-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), + transform 0.2s cubic-bezier(0.4, 0, 1, 1); +} + +.thaw-color-picker-popover.fade-in-scale-up-transition-enter-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), + transform 0.2s cubic-bezier(0, 0, 0.2, 1); +} + +.thaw-color-picker-popover.fade-in-scale-up-transition-enter-from, +.thaw-color-picker-popover.fade-in-scale-up-transition-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.thaw-color-picker-popover.fade-in-scale-up-transition-leave-from, +.thaw-color-picker-popover.fade-in-scale-up-transition-enter-to { + opacity: 1; + transform: scale(1); +} diff --git a/thaw/src/color_picker/mod.rs b/thaw/src/color_picker/mod.rs index 3fb09e0..bde1646 100644 --- a/thaw/src/color_picker/mod.rs +++ b/thaw/src/color_picker/mod.rs @@ -5,7 +5,7 @@ pub use color::*; pub use theme::ColorPickerTheme; use crate::{ - components::{Binder, Follower, FollowerPlacement}, + components::{Binder, CSSTransition, Follower, FollowerPlacement}, use_theme, utils::{class_list::class_list, mount_style, Model, OptionalProp}, Theme, @@ -152,15 +152,21 @@ pub fn ColorPicker( -
+
- - -
+ + +
+
} diff --git a/thaw/src/components/binder/get_placement_style.rs b/thaw/src/components/binder/get_placement_style.rs index 5bf341a..c5ba749 100644 --- a/thaw/src/components/binder/get_placement_style.rs +++ b/thaw/src/components/binder/get_placement_style.rs @@ -36,6 +36,23 @@ impl FollowerPlacement { Self::BottomEnd => "bottom-end", } } + + pub fn transform_origin(&self) -> &'static str { + match self { + Self::Top => "bottom center", + Self::Bottom => "top center", + Self::Left => "center right", + Self::Right => "center left", + Self::TopStart => "bottom left", + Self::TopEnd => "bottom right", + Self::LeftStart => "top right", + Self::LeftEnd => "bottom right", + Self::RightStart => "top left", + Self::RightEnd => "bottom left", + Self::BottomStart => "top left", + Self::BottomEnd => "top right", + } + } } pub struct FollowerPlacementOffset { diff --git a/thaw/src/components/binder/mod.rs b/thaw/src/components/binder/mod.rs index 020f15c..b73ff20 100644 --- a/thaw/src/components/binder/mod.rs +++ b/thaw/src/components/binder/mod.rs @@ -34,16 +34,15 @@ pub enum FollowerWidth { impl Copy for FollowerWidth {} - /// # Tracking popup /// /// ```rust /// use crate::components::{Binder, Follower, FollowerPlacement}; /// use leptos::*; -/// +/// /// let div_ref= NodeRef::new(); /// let show = RwSignal::new(false); -/// +/// /// view! { /// ///
@@ -58,7 +57,8 @@ impl Copy for FollowerWidth {} #[component] pub fn Binder( /// Used to track DOM locations - #[prop(into)] target_ref: NodeRef, + #[prop(into)] + target_ref: NodeRef, /// Content for pop-up display follower: Follower, children: Children, @@ -197,6 +197,10 @@ fn FollowerContainer( }) = get_follower_placement_offset(placement, target_rect, content_rect) { placement_str.set(placement.as_str()); + style.push_str(&format!( + "transform-origin: {};", + placement.transform_origin() + )); style.push_str(&format!( "transform: translateX({left}px) translateY({top}px) {transform};" )); @@ -207,15 +211,14 @@ fn FollowerContainer( content_style.set(style); }); - let is_show = create_memo(move |_| { + Effect::new(move |_| { if target_ref.get().is_none() { - return false; + return; } if content_ref.get().is_none() { - return false; + return; } - let is_show = show.get(); - if is_show { + if show.get() { request_animation_frame(move || { sync_position.call(()); }); @@ -225,21 +228,17 @@ fn FollowerContainer( remove_scroll_listener.call(()); remove_resize_listener.call(()); } - is_show }); let children = with_hydration_off(|| { - html::div() - .classes("thaw-binder-follower-container") - .style("display", move || (!is_show.get()).then_some("none")) - .child( - html::div() - .classes("thaw-binder-follower-content") - .attr("data-thaw-placement", move || placement_str.get()) - .node_ref(content_ref) - .attr("style", move || content_style.get()) - .child(children()), - ) + html::div().classes("thaw-binder-follower-container").child( + html::div() + .classes("thaw-binder-follower-content") + .attr("data-thaw-placement", move || placement_str.get()) + .node_ref(content_ref) + .attr("style", move || content_style.get()) + .child(children()), + ) }); view! { } diff --git a/thaw/src/date_picker/date-picker.css b/thaw/src/date_picker/date-picker.css index 2a90402..d00891b 100644 --- a/thaw/src/date_picker/date-picker.css +++ b/thaw/src/date_picker/date-picker.css @@ -172,3 +172,27 @@ text-align: center; border-radius: 3px; } + +.thaw-date-picker-panel.fade-in-scale-up-transition-leave-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), + transform 0.2s cubic-bezier(0.4, 0, 1, 1); +} + +.thaw-date-picker-panel.fade-in-scale-up-transition-enter-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), + transform 0.2s cubic-bezier(0, 0, 0.2, 1); +} + +.thaw-date-picker-panel.fade-in-scale-up-transition-enter-from, +.thaw-date-picker-panel.fade-in-scale-up-transition-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.thaw-date-picker-panel.fade-in-scale-up-transition-leave-from, +.thaw-date-picker-panel.fade-in-scale-up-transition-enter-to { + opacity: 1; + transform: scale(1); +} diff --git a/thaw/src/date_picker/mod.rs b/thaw/src/date_picker/mod.rs index b0b2872..b140f2d 100644 --- a/thaw/src/date_picker/mod.rs +++ b/thaw/src/date_picker/mod.rs @@ -87,6 +87,7 @@ pub fn DatePicker( close_panel selected_date=panel_selected_date comp_ref=panel_ref + is_show_panel /> diff --git a/thaw/src/date_picker/panel/mod.rs b/thaw/src/date_picker/panel/mod.rs index 591ece0..f5a6ac7 100644 --- a/thaw/src/date_picker/panel/mod.rs +++ b/thaw/src/date_picker/panel/mod.rs @@ -3,6 +3,7 @@ mod month_panel; mod year_panel; use crate::{ + components::CSSTransition, use_theme, utils::{now_date, ComponentRef}, Theme, @@ -18,6 +19,7 @@ pub fn Panel( selected_date: RwSignal>, date_picker_ref: NodeRef, close_panel: Callback>, + #[prop(into)] is_show_panel: MaybeSignal, #[prop(optional)] comp_ref: ComponentRef, ) -> impl IntoView { let theme = use_theme(Theme::light); @@ -91,25 +93,36 @@ pub fn Panel( }); view! { -
+ +
- {move || { - match panel_variant.get() { - PanelVariant::Date => { - view! { - + {move || { + match panel_variant.get() { + PanelVariant::Date => { + view! { + + } + } + PanelVariant::Month => { + view! { } + } + PanelVariant::Year => { + view! { } } } - PanelVariant::Month => { - view! { } - } - PanelVariant::Year => { - view! { } - } - } - }} + }} -
+
+ } } diff --git a/thaw/src/popover/mod.rs b/thaw/src/popover/mod.rs index ea870fd..43318ed 100644 --- a/thaw/src/popover/mod.rs +++ b/thaw/src/popover/mod.rs @@ -1,14 +1,15 @@ mod theme; +pub use theme::PopoverTheme; + use crate::{ - components::{Binder, Follower, FollowerPlacement}, + components::{Binder, CSSTransition, Follower, FollowerPlacement}, use_theme, utils::{add_event_listener, class_list::class_list, mount_style, OptionalProp}, Theme, }; use leptos::{leptos_dom::helpers::TimeoutHandle, *}; use std::time::Duration; -pub use theme::PopoverTheme; #[slot] pub struct PopoverTrigger { @@ -112,6 +113,7 @@ pub fn Popover( children: trigger_children, } = popover_trigger; + let follower_enabled = RwSignal::new(false); view! {
{trigger_children()}
- -
+ -
{children()}
-
-
+
+
{children()}
+
+
+
-
+
} diff --git a/thaw/src/popover/popover.css b/thaw/src/popover/popover.css index 57adf03..a9b955d 100644 --- a/thaw/src/popover/popover.css +++ b/thaw/src/popover/popover.css @@ -3,6 +3,7 @@ padding: 8px 14px; background-color: var(--thaw-background-color); border-radius: 3px; + transform-origin: inherit; } .thaw-popover-trigger { @@ -138,3 +139,31 @@ top: initial; bottom: 7px; } + +.thaw-popover.popover-transition-enter-from, +.thaw-popover.popover-transition-leave-to { + opacity: 0; + transform: scale(0.85); +} + +.thaw-popover.popover-transition-enter-to, +.thaw-popover.popover-transition-leave-from { + transform: scale(1); + opacity: 1; +} + +.thaw-popover.popover-transition-enter-active { + transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1), + background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.15s cubic-bezier(0, 0, 0.2, 1), + transform 0.15s cubic-bezier(0, 0, 0.2, 1); +} + +.thaw-popover.popover-transition-leave-active { + transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1), + background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.15s cubic-bezier(0.4, 0, 1, 1), + transform 0.15s cubic-bezier(0.4, 0, 1, 1); +} diff --git a/thaw/src/select/mod.rs b/thaw/src/select/mod.rs index f3026d6..659e85d 100644 --- a/thaw/src/select/mod.rs +++ b/thaw/src/select/mod.rs @@ -1,7 +1,7 @@ mod theme; use crate::{ - components::{Binder, Follower, FollowerPlacement, FollowerWidth}, + components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth}, theme::use_theme, utils::{class_list::class_list, mount_style, Model, OptionalProp}, Theme, @@ -126,34 +126,45 @@ where placement=FollowerPlacement::BottomStart width=FollowerWidth::Target > -
- +
+ - {item.get_value().label} -
+ on:click=onclick + > + {item.get_value().label} +
+ } } - } - /> + /> -
+
+
} diff --git a/thaw/src/select/select.css b/thaw/src/select/select.css index 702454f..5a61d03 100644 --- a/thaw/src/select/select.css +++ b/thaw/src/select/select.css @@ -42,3 +42,31 @@ .thaw-select-menu__item-selected { color: var(--thaw-font-color-selected); } + +.thaw-select-menu.fade-in-scale-up-transition-leave-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), + transform 0.2s cubic-bezier(0.4, 0, 1, 1), + background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.thaw-select-menu.fade-in-scale-up-transition-enter-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), + transform 0.2s cubic-bezier(0, 0, 0.2, 1), + background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.thaw-select-menu.fade-in-scale-up-transition-enter-from, +.thaw-select-menu.fade-in-scale-up-transition-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.thaw-select-menu.fade-in-scale-up-transition-leave-from, +.thaw-select-menu.fade-in-scale-up-transition-enter-to { + opacity: 1; + transform: scale(1); +} diff --git a/thaw/src/time_picker/mod.rs b/thaw/src/time_picker/mod.rs index fcb38b3..75054f8 100644 --- a/thaw/src/time_picker/mod.rs +++ b/thaw/src/time_picker/mod.rs @@ -3,7 +3,7 @@ mod theme; pub use theme::TimePickerTheme; use crate::{ - components::{Binder, Follower, FollowerPlacement}, + components::{Binder, CSSTransition, Follower, FollowerPlacement}, use_theme, utils::{mount_style, ComponentRef, Model, OptionalProp}, Button, ButtonSize, ButtonVariant, Icon, Input, InputSuffix, SignalWatch, Theme, @@ -86,6 +86,7 @@ pub fn TimePicker( selected_time=panel_selected_time close_panel time_picker_ref + is_show_panel comp_ref=panel_ref /> @@ -98,6 +99,7 @@ fn Panel( selected_time: RwSignal>, time_picker_ref: NodeRef, close_panel: Callback>, + #[prop(into)] is_show_panel: MaybeSignal, comp_ref: ComponentRef, ) -> impl IntoView { let theme = use_theme(Theme::light); @@ -173,96 +175,107 @@ fn Panel( }); view! { -
-
-
+ +
+
+
- {(0..24) - .map(|hour| { - let comp_ref = ComponentRef::::default(); - let on_click = move |_| { - selected_time - .update(move |time| { - *time = if let Some(time) = time { - time.with_hour(hour) - } else { - NaiveTime::from_hms_opt(hour, 0, 0) - } - }); - comp_ref.get_untracked().unwrap().scroll_into_view(); - }; - let is_selected = Memo::new(move |_| { - selected_time.get().map_or(false, |v| v.hour() == hour) - }); - view! { - - } - }) - .collect_view()} -
+ {(0..24) + .map(|hour| { + let comp_ref = ComponentRef::::default(); + let on_click = move |_| { + selected_time + .update(move |time| { + *time = if let Some(time) = time { + time.with_hour(hour) + } else { + NaiveTime::from_hms_opt(hour, 0, 0) + } + }); + comp_ref.get_untracked().unwrap().scroll_into_view(); + }; + let is_selected = Memo::new(move |_| { + selected_time.get().map_or(false, |v| v.hour() == hour) + }); + view! { + + } + }) + .collect_view()} +
+
+
+ + {(0..60) + .map(|minute| { + let comp_ref = ComponentRef::::default(); + let on_click = move |_| { + selected_time + .update(move |time| { + *time = if let Some(time) = time { + time.with_minute(minute) + } else { + NaiveTime::from_hms_opt(now_time().hour(), minute, 0) + } + }); + comp_ref.get_untracked().unwrap().scroll_into_view(); + }; + let is_selected = Memo::new(move |_| { + selected_time.get().map_or(false, |v| v.minute() == minute) + }); + view! { + + } + }) + .collect_view()} +
+
+
+ + {(0..60) + .map(|second| { + let comp_ref = ComponentRef::::default(); + let on_click = move |_| { + selected_time + .update(move |time| { + *time = if let Some(time) = time { + time.with_second(second) + } else { + now_time().with_second(second) + } + }); + comp_ref.get_untracked().unwrap().scroll_into_view(); + }; + let is_selected = Memo::new(move |_| { + selected_time.get().map_or(false, |v| v.second() == second) + }); + view! { + + } + }) + .collect_view()} +
+
-
- - {(0..60) - .map(|minute| { - let comp_ref = ComponentRef::::default(); - let on_click = move |_| { - selected_time - .update(move |time| { - *time = if let Some(time) = time { - time.with_minute(minute) - } else { - NaiveTime::from_hms_opt(now_time().hour(), minute, 0) - } - }); - comp_ref.get_untracked().unwrap().scroll_into_view(); - }; - let is_selected = Memo::new(move |_| { - selected_time.get().map_or(false, |v| v.minute() == minute) - }); - view! { - - } - }) - .collect_view()} -
-
-
- - {(0..60) - .map(|second| { - let comp_ref = ComponentRef::::default(); - let on_click = move |_| { - selected_time - .update(move |time| { - *time = if let Some(time) = time { - time.with_second(second) - } else { - now_time().with_second(second) - } - }); - comp_ref.get_untracked().unwrap().scroll_into_view(); - }; - let is_selected = Memo::new(move |_| { - selected_time.get().map_or(false, |v| v.second() == second) - }); - view! { - - } - }) - .collect_view()} -
+
- -
+
} } diff --git a/thaw/src/time_picker/time-picker.css b/thaw/src/time_picker/time-picker.css index 7c22948..7ec9a00 100644 --- a/thaw/src/time_picker/time-picker.css +++ b/thaw/src/time_picker/time-picker.css @@ -51,3 +51,27 @@ align-items: center; border-top: 1px solid var(--thaw-item-border-color); } + +.thaw-time-picker-panel.fade-in-scale-up-transition-leave-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), + transform 0.2s cubic-bezier(0.4, 0, 1, 1); +} + +.thaw-time-picker-panel.fade-in-scale-up-transition-enter-active { + transform-origin: inherit; + transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), + transform 0.2s cubic-bezier(0, 0, 0.2, 1); +} + +.thaw-time-picker-panel.fade-in-scale-up-transition-enter-from, +.thaw-time-picker-panel.fade-in-scale-up-transition-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.thaw-time-picker-panel.fade-in-scale-up-transition-leave-from, +.thaw-time-picker-panel.fade-in-scale-up-transition-enter-to { + opacity: 1; + transform: scale(1); +}