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
This commit is contained in:
luoxiaozero 2024-03-06 13:08:06 +08:00 committed by GitHub
parent d2383445ff
commit c1a9f84c9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 462 additions and 228 deletions

View file

@ -18,3 +18,31 @@
.thaw-auto-complete__menu-item--selected { .thaw-auto-complete__menu-item--selected {
background-color: var(--thaw-background-color-hover); 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);
}

View file

@ -1,7 +1,7 @@
mod theme; mod theme;
use crate::{ use crate::{
components::{Binder, Follower, FollowerPlacement, FollowerWidth}, components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth},
use_theme, use_theme,
utils::{class_list::class_list, mount_style, Model, OptionalProp, StoredMaybeSignal}, utils::{class_list::class_list, mount_style, Model, OptionalProp, StoredMaybeSignal},
ComponentRef, Input, InputPrefix, InputRef, InputSuffix, Theme, ComponentRef, Input, InputPrefix, InputRef, InputSuffix, Theme,
@ -196,69 +196,76 @@ pub fn AutoComplete(
placement=FollowerPlacement::BottomStart placement=FollowerPlacement::BottomStart
width=FollowerWidth::Target width=FollowerWidth::Target
> >
<div <CSSTransition
class="thaw-auto-complete__menu" node_ref=menu_ref
style=move || menu_css_vars.get() name="fade-in-scale-up-transition"
ref=menu_ref show=is_show_menu
let:display
> >
<div
class="thaw-auto-complete__menu"
style=move || display.get().map(|d| d.to_string()).unwrap_or_else(|| menu_css_vars.get())
ref=menu_ref
>
{move || { {move || {
options options
.get() .get()
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(index, v)| { .map(|(index, v)| {
let AutoCompleteOption { value: option_value, label } = v; let AutoCompleteOption { value: option_value, label } = v;
let menu_item_ref = create_node_ref::<html::Div>(); let menu_item_ref = create_node_ref::<html::Div>();
let on_click = move |_| { let on_click = move |_| {
select_value(option_value.clone()); select_value(option_value.clone());
}; };
let on_mouseenter = move |_| { let on_mouseenter = move |_| {
select_option_index.set(Some(index)); select_option_index.set(Some(index));
}; };
let on_mousedown = move |ev: ev::MouseEvent| { let on_mousedown = move |ev: ev::MouseEvent| {
ev.prevent_default(); ev.prevent_default();
}; };
create_effect(move |_| { create_effect(move |_| {
if Some(index) == select_option_index.get() { if Some(index) == select_option_index.get() {
if !is_show_menu.get() { if !is_show_menu.get() {
return; return;
} }
if let Some(menu_item_ref) = menu_item_ref.get() { if let Some(menu_item_ref) = menu_item_ref.get() {
let menu_ref = menu_ref.get().unwrap(); let menu_ref = menu_ref.get().unwrap();
let menu_rect = menu_ref.get_bounding_client_rect(); let menu_rect = menu_ref.get_bounding_client_rect();
let item_rect = menu_item_ref.get_bounding_client_rect(); let item_rect = menu_item_ref.get_bounding_client_rect();
if item_rect.y() < menu_rect.y() { if item_rect.y() < menu_rect.y() {
menu_item_ref.scroll_into_view_with_bool(true); menu_item_ref.scroll_into_view_with_bool(true);
} else if item_rect.y() + item_rect.height() } else if item_rect.y() + item_rect.height()
> menu_rect.y() + menu_rect.height() > menu_rect.y() + menu_rect.height()
{ {
menu_item_ref.scroll_into_view_with_bool(false); menu_item_ref.scroll_into_view_with_bool(false);
}
} }
} }
});
view! {
<div
class="thaw-auto-complete__menu-item"
class=(
"thaw-auto-complete__menu-item--selected",
move || Some(index) == select_option_index.get(),
)
on:click=on_click
on:mousedown=on_mousedown
on:mouseenter=on_mouseenter
ref=menu_item_ref
>
{label}
</div>
} }
}); })
view! { .collect_view()
<div }}
class="thaw-auto-complete__menu-item"
class=(
"thaw-auto-complete__menu-item--selected",
move || Some(index) == select_option_index.get(),
)
on:click=on_click </div>
on:mousedown=on_mousedown </CSSTransition>
on:mouseenter=on_mouseenter
ref=menu_item_ref
>
{label}
</div>
}
})
.collect_view()
}}
</div>
</Follower> </Follower>
</Binder> </Binder>
} }

View file

@ -89,3 +89,27 @@
border: 2px solid white; border: 2px solid white;
cursor: pointer; 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);
}

View file

@ -5,7 +5,7 @@ pub use color::*;
pub use theme::ColorPickerTheme; pub use theme::ColorPickerTheme;
use crate::{ use crate::{
components::{Binder, Follower, FollowerPlacement}, components::{Binder, CSSTransition, Follower, FollowerPlacement},
use_theme, use_theme,
utils::{class_list::class_list, mount_style, Model, OptionalProp}, utils::{class_list::class_list, mount_style, Model, OptionalProp},
Theme, Theme,
@ -152,15 +152,21 @@ pub fn ColorPicker(
</div> </div>
</div> </div>
<Follower slot show=is_show_popover placement=FollowerPlacement::BottomStart> <Follower slot show=is_show_popover placement=FollowerPlacement::BottomStart>
<div <CSSTransition
class="thaw-color-picker-popover" node_ref=popover_ref name="fade-in-scale-up-transition"
ref=popover_ref show=is_show_popover
style=move || popover_css_vars.get() let:display
> >
<div
class="thaw-color-picker-popover"
ref=popover_ref
style=move || display.get().map(|d| d.to_string()).unwrap_or_else(|| popover_css_vars.get())
>
<ColorPanel hue=hue.read_only() sv/> <ColorPanel hue=hue.read_only() sv/>
<HueSlider hue/> <HueSlider hue/>
</div> </div>
</CSSTransition>
</Follower> </Follower>
</Binder> </Binder>
} }

View file

@ -36,6 +36,23 @@ impl FollowerPlacement {
Self::BottomEnd => "bottom-end", 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 { pub struct FollowerPlacementOffset {

View file

@ -34,7 +34,6 @@ pub enum FollowerWidth {
impl Copy for FollowerWidth {} impl Copy for FollowerWidth {}
/// # Tracking popup /// # Tracking popup
/// ///
/// ```rust /// ```rust
@ -58,7 +57,8 @@ impl Copy for FollowerWidth {}
#[component] #[component]
pub fn Binder<El: ElementDescriptor + Clone + 'static>( pub fn Binder<El: ElementDescriptor + Clone + 'static>(
/// Used to track DOM locations /// Used to track DOM locations
#[prop(into)] target_ref: NodeRef<El>, #[prop(into)]
target_ref: NodeRef<El>,
/// Content for pop-up display /// Content for pop-up display
follower: Follower, follower: Follower,
children: Children, children: Children,
@ -197,6 +197,10 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
}) = get_follower_placement_offset(placement, target_rect, content_rect) }) = get_follower_placement_offset(placement, target_rect, content_rect)
{ {
placement_str.set(placement.as_str()); placement_str.set(placement.as_str());
style.push_str(&format!(
"transform-origin: {};",
placement.transform_origin()
));
style.push_str(&format!( style.push_str(&format!(
"transform: translateX({left}px) translateY({top}px) {transform};" "transform: translateX({left}px) translateY({top}px) {transform};"
)); ));
@ -207,15 +211,14 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
content_style.set(style); content_style.set(style);
}); });
let is_show = create_memo(move |_| { Effect::new(move |_| {
if target_ref.get().is_none() { if target_ref.get().is_none() {
return false; return;
} }
if content_ref.get().is_none() { if content_ref.get().is_none() {
return false; return;
} }
let is_show = show.get(); if show.get() {
if is_show {
request_animation_frame(move || { request_animation_frame(move || {
sync_position.call(()); sync_position.call(());
}); });
@ -225,21 +228,17 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
remove_scroll_listener.call(()); remove_scroll_listener.call(());
remove_resize_listener.call(()); remove_resize_listener.call(());
} }
is_show
}); });
let children = with_hydration_off(|| { let children = with_hydration_off(|| {
html::div() html::div().classes("thaw-binder-follower-container").child(
.classes("thaw-binder-follower-container") html::div()
.style("display", move || (!is_show.get()).then_some("none")) .classes("thaw-binder-follower-content")
.child( .attr("data-thaw-placement", move || placement_str.get())
html::div() .node_ref(content_ref)
.classes("thaw-binder-follower-content") .attr("style", move || content_style.get())
.attr("data-thaw-placement", move || placement_str.get()) .child(children()),
.node_ref(content_ref) )
.attr("style", move || content_style.get())
.child(children()),
)
}); });
view! { <Teleport element=children/> } view! { <Teleport element=children/> }

View file

@ -172,3 +172,27 @@
text-align: center; text-align: center;
border-radius: 3px; 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);
}

View file

@ -87,6 +87,7 @@ pub fn DatePicker(
close_panel close_panel
selected_date=panel_selected_date selected_date=panel_selected_date
comp_ref=panel_ref comp_ref=panel_ref
is_show_panel
/> />
</Follower> </Follower>
</Binder> </Binder>

View file

@ -3,6 +3,7 @@ mod month_panel;
mod year_panel; mod year_panel;
use crate::{ use crate::{
components::CSSTransition,
use_theme, use_theme,
utils::{now_date, ComponentRef}, utils::{now_date, ComponentRef},
Theme, Theme,
@ -18,6 +19,7 @@ pub fn Panel(
selected_date: RwSignal<Option<NaiveDate>>, selected_date: RwSignal<Option<NaiveDate>>,
date_picker_ref: NodeRef<html::Div>, date_picker_ref: NodeRef<html::Div>,
close_panel: Callback<Option<NaiveDate>>, close_panel: Callback<Option<NaiveDate>>,
#[prop(into)] is_show_panel: MaybeSignal<bool>,
#[prop(optional)] comp_ref: ComponentRef<PanelRef>, #[prop(optional)] comp_ref: ComponentRef<PanelRef>,
) -> impl IntoView { ) -> impl IntoView {
let theme = use_theme(Theme::light); let theme = use_theme(Theme::light);
@ -91,25 +93,36 @@ pub fn Panel(
}); });
view! { view! {
<div class="thaw-date-picker-panel" style=move || css_vars.get() ref=panel_ref> <CSSTransition
node_ref=panel_ref
name="fade-in-scale-up-transition"
show=is_show_panel
let:display
>
<div
class="thaw-date-picker-panel"
style=move || display.get().map(|d| d.to_string()).unwrap_or_else(|| css_vars.get())
ref=panel_ref
>
{move || { {move || {
match panel_variant.get() { match panel_variant.get() {
PanelVariant::Date => { PanelVariant::Date => {
view! { view! {
<DatePanel value=selected_date show_date close_panel panel_variant/> <DatePanel value=selected_date show_date close_panel panel_variant/>
}
}
PanelVariant::Month => {
view! { <MonthPanel date_panel_show_date=show_date panel_variant/> }
}
PanelVariant::Year => {
view! { <YearPanel date_panel_show_date=show_date panel_variant/> }
} }
} }
PanelVariant::Month => { }}
view! { <MonthPanel date_panel_show_date=show_date panel_variant/> }
}
PanelVariant::Year => {
view! { <YearPanel date_panel_show_date=show_date panel_variant/> }
}
}
}}
</div> </div>
</CSSTransition>
} }
} }

View file

@ -1,14 +1,15 @@
mod theme; mod theme;
pub use theme::PopoverTheme;
use crate::{ use crate::{
components::{Binder, Follower, FollowerPlacement}, components::{Binder, CSSTransition, Follower, FollowerPlacement},
use_theme, use_theme,
utils::{add_event_listener, class_list::class_list, mount_style, OptionalProp}, utils::{add_event_listener, class_list::class_list, mount_style, OptionalProp},
Theme, Theme,
}; };
use leptos::{leptos_dom::helpers::TimeoutHandle, *}; use leptos::{leptos_dom::helpers::TimeoutHandle, *};
use std::time::Duration; use std::time::Duration;
pub use theme::PopoverTheme;
#[slot] #[slot]
pub struct PopoverTrigger { pub struct PopoverTrigger {
@ -112,6 +113,7 @@ pub fn Popover(
children: trigger_children, children: trigger_children,
} = popover_trigger; } = popover_trigger;
let follower_enabled = RwSignal::new(false);
view! { view! {
<Binder target_ref> <Binder target_ref>
<div <div
@ -122,19 +124,27 @@ pub fn Popover(
> >
{trigger_children()} {trigger_children()}
</div> </div>
<Follower slot show=is_show_popover placement> <Follower slot show=follower_enabled placement>
<div <CSSTransition
class="thaw-popover" node_ref=popover_ref name="popover-transition"
style=move || css_vars.get() show=is_show_popover
ref=popover_ref on_enter=move |_| follower_enabled.set(true)
on:mouseenter=on_mouse_enter on_after_leave=move |_| follower_enabled.set(false)
on:mouseleave=on_mouse_leave let:display
> >
<div class=class.map(|c| move || c.get())>{children()}</div> <div
<div class="thaw-popover__angle-container"> class="thaw-popover"
<div class="thaw-popover__angle"></div> style=move || display.get().map(|d| d.to_string()).unwrap_or_else(|| css_vars.get())
ref=popover_ref
on:mouseenter=on_mouse_enter
on:mouseleave=on_mouse_leave
>
<div class=class.map(|c| move || c.get())>{children()}</div>
<div class="thaw-popover__angle-container">
<div class="thaw-popover__angle"></div>
</div>
</div> </div>
</div> </CSSTransition>
</Follower> </Follower>
</Binder> </Binder>
} }

View file

@ -3,6 +3,7 @@
padding: 8px 14px; padding: 8px 14px;
background-color: var(--thaw-background-color); background-color: var(--thaw-background-color);
border-radius: 3px; border-radius: 3px;
transform-origin: inherit;
} }
.thaw-popover-trigger { .thaw-popover-trigger {
@ -138,3 +139,31 @@
top: initial; top: initial;
bottom: 7px; 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);
}

View file

@ -1,7 +1,7 @@
mod theme; mod theme;
use crate::{ use crate::{
components::{Binder, Follower, FollowerPlacement, FollowerWidth}, components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth},
theme::use_theme, theme::use_theme,
utils::{class_list::class_list, mount_style, Model, OptionalProp}, utils::{class_list::class_list, mount_style, Model, OptionalProp},
Theme, Theme,
@ -126,34 +126,45 @@ where
placement=FollowerPlacement::BottomStart placement=FollowerPlacement::BottomStart
width=FollowerWidth::Target width=FollowerWidth::Target
> >
<div class="thaw-select-menu" style=move || menu_css_vars.get() ref=menu_ref> <CSSTransition
<For node_ref=menu_ref
each=move || options.get() name="fade-in-scale-up-transition"
key=move |item| item.value.clone() show=is_show_menu
children=move |item| { let:display
let item = store_value(item); >
let onclick = move |_| { <div
let SelectOption { value: item_value, label: _ } = item.get_value(); class="thaw-select-menu"
value.set(Some(item_value)); style=move || display.get().map(|d| d.to_string()).unwrap_or_else(|| menu_css_vars.get())
is_show_menu.set(false); ref=menu_ref
}; >
view! { <For
<div each=move || options.get()
class="thaw-select-menu__item" key=move |item| item.value.clone()
class=( children=move |item| {
"thaw-select-menu__item-selected", let item = store_value(item);
move || value.get() == Some(item.get_value().value), let onclick = move |_| {
) let SelectOption { value: item_value, label: _ } = item.get_value();
value.set(Some(item_value));
is_show_menu.set(false);
};
view! {
<div
class="thaw-select-menu__item"
class=(
"thaw-select-menu__item-selected",
move || value.get() == Some(item.get_value().value),
)
on:click=onclick on:click=onclick
> >
{item.get_value().label} {item.get_value().label}
</div> </div>
}
} }
} />
/>
</div> </div>
</CSSTransition>
</Follower> </Follower>
</Binder> </Binder>
} }

View file

@ -42,3 +42,31 @@
.thaw-select-menu__item-selected { .thaw-select-menu__item-selected {
color: var(--thaw-font-color-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);
}

View file

@ -3,7 +3,7 @@ mod theme;
pub use theme::TimePickerTheme; pub use theme::TimePickerTheme;
use crate::{ use crate::{
components::{Binder, Follower, FollowerPlacement}, components::{Binder, CSSTransition, Follower, FollowerPlacement},
use_theme, use_theme,
utils::{mount_style, ComponentRef, Model, OptionalProp}, utils::{mount_style, ComponentRef, Model, OptionalProp},
Button, ButtonSize, ButtonVariant, Icon, Input, InputSuffix, SignalWatch, Theme, Button, ButtonSize, ButtonVariant, Icon, Input, InputSuffix, SignalWatch, Theme,
@ -86,6 +86,7 @@ pub fn TimePicker(
selected_time=panel_selected_time selected_time=panel_selected_time
close_panel close_panel
time_picker_ref time_picker_ref
is_show_panel
comp_ref=panel_ref comp_ref=panel_ref
/> />
</Follower> </Follower>
@ -98,6 +99,7 @@ fn Panel(
selected_time: RwSignal<Option<NaiveTime>>, selected_time: RwSignal<Option<NaiveTime>>,
time_picker_ref: NodeRef<html::Div>, time_picker_ref: NodeRef<html::Div>,
close_panel: Callback<Option<NaiveTime>>, close_panel: Callback<Option<NaiveTime>>,
#[prop(into)] is_show_panel: MaybeSignal<bool>,
comp_ref: ComponentRef<PanelRef>, comp_ref: ComponentRef<PanelRef>,
) -> impl IntoView { ) -> impl IntoView {
let theme = use_theme(Theme::light); let theme = use_theme(Theme::light);
@ -173,96 +175,107 @@ fn Panel(
}); });
view! { view! {
<div class="thaw-time-picker-panel" style=move || css_vars.get() ref=panel_ref> <CSSTransition
<div class="thaw-time-picker-panel__time"> node_ref=panel_ref
<div class="thaw-time-picker-panel__time-hour" ref=hour_ref> name="fade-in-scale-up-transition"
show=is_show_panel
let:display
>
<div
class="thaw-time-picker-panel"
style=move || display.get().map(|d| d.to_string()).unwrap_or_else(|| css_vars.get())
ref=panel_ref
>
<div class="thaw-time-picker-panel__time">
<div class="thaw-time-picker-panel__time-hour" ref=hour_ref>
{(0..24) {(0..24)
.map(|hour| { .map(|hour| {
let comp_ref = ComponentRef::<PanelTimeItemRef>::default(); let comp_ref = ComponentRef::<PanelTimeItemRef>::default();
let on_click = move |_| { let on_click = move |_| {
selected_time selected_time
.update(move |time| { .update(move |time| {
*time = if let Some(time) = time { *time = if let Some(time) = time {
time.with_hour(hour) time.with_hour(hour)
} else { } else {
NaiveTime::from_hms_opt(hour, 0, 0) NaiveTime::from_hms_opt(hour, 0, 0)
} }
}); });
comp_ref.get_untracked().unwrap().scroll_into_view(); comp_ref.get_untracked().unwrap().scroll_into_view();
}; };
let is_selected = Memo::new(move |_| { let is_selected = Memo::new(move |_| {
selected_time.get().map_or(false, |v| v.hour() == hour) selected_time.get().map_or(false, |v| v.hour() == hour)
}); });
view! { view! {
<PanelTimeItem value=hour on:click=on_click is_selected comp_ref/> <PanelTimeItem value=hour on:click=on_click is_selected comp_ref/>
} }
}) })
.collect_view()} .collect_view()}
<div class="thaw-time-picker-panel__time-padding"></div> <div class="thaw-time-picker-panel__time-padding"></div>
</div>
<div class="thaw-time-picker-panel__time-minute" ref=minute_ref>
{(0..60)
.map(|minute| {
let comp_ref = ComponentRef::<PanelTimeItemRef>::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! {
<PanelTimeItem value=minute on:click=on_click is_selected comp_ref/>
}
})
.collect_view()}
<div class="thaw-time-picker-panel__time-padding"></div>
</div>
<div class="thaw-time-picker-panel__time-second" ref=second_ref>
{(0..60)
.map(|second| {
let comp_ref = ComponentRef::<PanelTimeItemRef>::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! {
<PanelTimeItem value=second on:click=on_click is_selected comp_ref/>
}
})
.collect_view()}
<div class="thaw-time-picker-panel__time-padding"></div>
</div>
</div> </div>
<div class="thaw-time-picker-panel__time-minute" ref=minute_ref> <div class="thaw-time-picker-panel__footer">
<Button variant=ButtonVariant::Outlined size=ButtonSize::Tiny on_click=now>
{(0..60) "Now"
.map(|minute| { </Button>
let comp_ref = ComponentRef::<PanelTimeItemRef>::default(); <Button size=ButtonSize::Tiny on_click=ok>
let on_click = move |_| { "OK"
selected_time </Button>
.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! {
<PanelTimeItem value=minute on:click=on_click is_selected comp_ref/>
}
})
.collect_view()}
<div class="thaw-time-picker-panel__time-padding"></div>
</div>
<div class="thaw-time-picker-panel__time-second" ref=second_ref>
{(0..60)
.map(|second| {
let comp_ref = ComponentRef::<PanelTimeItemRef>::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! {
<PanelTimeItem value=second on:click=on_click is_selected comp_ref/>
}
})
.collect_view()}
<div class="thaw-time-picker-panel__time-padding"></div>
</div> </div>
</div> </div>
<div class="thaw-time-picker-panel__footer"> </CSSTransition>
<Button variant=ButtonVariant::Outlined size=ButtonSize::Tiny on_click=now>
"Now"
</Button>
<Button size=ButtonSize::Tiny on_click=ok>
"OK"
</Button>
</div>
</div>
} }
} }

View file

@ -51,3 +51,27 @@
align-items: center; align-items: center;
border-top: 1px solid var(--thaw-item-border-color); 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);
}