mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-02 08:34:15 -05:00
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:
parent
d2383445ff
commit
c1a9f84c9e
15 changed files with 462 additions and 228 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
>
|
||||
<div
|
||||
class="thaw-auto-complete__menu"
|
||||
style=move || menu_css_vars.get()
|
||||
ref=menu_ref
|
||||
<CSSTransition
|
||||
node_ref=menu_ref
|
||||
name="fade-in-scale-up-transition"
|
||||
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 || {
|
||||
options
|
||||
.get()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, v)| {
|
||||
let AutoCompleteOption { value: option_value, label } = v;
|
||||
let menu_item_ref = create_node_ref::<html::Div>();
|
||||
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::<html::Div>();
|
||||
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! {
|
||||
<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! {
|
||||
<div
|
||||
class="thaw-auto-complete__menu-item"
|
||||
class=(
|
||||
"thaw-auto-complete__menu-item--selected",
|
||||
move || Some(index) == select_option_index.get(),
|
||||
)
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
|
||||
on:click=on_click
|
||||
on:mousedown=on_mousedown
|
||||
on:mouseenter=on_mouseenter
|
||||
ref=menu_item_ref
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</Follower>
|
||||
</Binder>
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
|||
</div>
|
||||
</div>
|
||||
<Follower slot show=is_show_popover placement=FollowerPlacement::BottomStart>
|
||||
<div
|
||||
class="thaw-color-picker-popover"
|
||||
ref=popover_ref
|
||||
style=move || popover_css_vars.get()
|
||||
<CSSTransition
|
||||
node_ref=popover_ref name="fade-in-scale-up-transition"
|
||||
show=is_show_popover
|
||||
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/>
|
||||
<HueSlider hue/>
|
||||
</div>
|
||||
<ColorPanel hue=hue.read_only() sv/>
|
||||
<HueSlider hue/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</Follower>
|
||||
</Binder>
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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! {
|
||||
/// <Binder target_ref=div_ref>
|
||||
/// <div ref=div_ref>
|
||||
|
@ -58,7 +57,8 @@ impl Copy for FollowerWidth {}
|
|||
#[component]
|
||||
pub fn Binder<El: ElementDescriptor + Clone + 'static>(
|
||||
/// Used to track DOM locations
|
||||
#[prop(into)] target_ref: NodeRef<El>,
|
||||
#[prop(into)]
|
||||
target_ref: NodeRef<El>,
|
||||
/// Content for pop-up display
|
||||
follower: Follower,
|
||||
children: Children,
|
||||
|
@ -197,6 +197,10 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
|
|||
}) = 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<El: ElementDescriptor + Clone + 'static>(
|
|||
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<El: ElementDescriptor + Clone + 'static>(
|
|||
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! { <Teleport element=children/> }
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ pub fn DatePicker(
|
|||
close_panel
|
||||
selected_date=panel_selected_date
|
||||
comp_ref=panel_ref
|
||||
is_show_panel
|
||||
/>
|
||||
</Follower>
|
||||
</Binder>
|
||||
|
|
|
@ -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<Option<NaiveDate>>,
|
||||
date_picker_ref: NodeRef<html::Div>,
|
||||
close_panel: Callback<Option<NaiveDate>>,
|
||||
#[prop(into)] is_show_panel: MaybeSignal<bool>,
|
||||
#[prop(optional)] comp_ref: ComponentRef<PanelRef>,
|
||||
) -> impl IntoView {
|
||||
let theme = use_theme(Theme::light);
|
||||
|
@ -91,25 +93,36 @@ pub fn Panel(
|
|||
});
|
||||
|
||||
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 || {
|
||||
match panel_variant.get() {
|
||||
PanelVariant::Date => {
|
||||
view! {
|
||||
<DatePanel value=selected_date show_date close_panel panel_variant/>
|
||||
{move || {
|
||||
match panel_variant.get() {
|
||||
PanelVariant::Date => {
|
||||
view! {
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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! {
|
||||
<Binder target_ref>
|
||||
<div
|
||||
|
@ -122,19 +124,27 @@ pub fn Popover(
|
|||
>
|
||||
{trigger_children()}
|
||||
</div>
|
||||
<Follower slot show=is_show_popover placement>
|
||||
<div
|
||||
class="thaw-popover"
|
||||
style=move || css_vars.get()
|
||||
ref=popover_ref
|
||||
on:mouseenter=on_mouse_enter
|
||||
on:mouseleave=on_mouse_leave
|
||||
<Follower slot show=follower_enabled placement>
|
||||
<CSSTransition
|
||||
node_ref=popover_ref name="popover-transition"
|
||||
show=is_show_popover
|
||||
on_enter=move |_| follower_enabled.set(true)
|
||||
on_after_leave=move |_| follower_enabled.set(false)
|
||||
let:display
|
||||
>
|
||||
<div class=class.map(|c| move || c.get())>{children()}</div>
|
||||
<div class="thaw-popover__angle-container">
|
||||
<div class="thaw-popover__angle"></div>
|
||||
<div
|
||||
class="thaw-popover"
|
||||
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>
|
||||
</CSSTransition>
|
||||
</Follower>
|
||||
</Binder>
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
>
|
||||
<div class="thaw-select-menu" style=move || menu_css_vars.get() ref=menu_ref>
|
||||
<For
|
||||
each=move || options.get()
|
||||
key=move |item| item.value.clone()
|
||||
children=move |item| {
|
||||
let item = store_value(item);
|
||||
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),
|
||||
)
|
||||
<CSSTransition
|
||||
node_ref=menu_ref
|
||||
name="fade-in-scale-up-transition"
|
||||
show=is_show_menu
|
||||
let:display
|
||||
>
|
||||
<div
|
||||
class="thaw-select-menu"
|
||||
style=move || display.get().map(|d| d.to_string()).unwrap_or_else(|| menu_css_vars.get())
|
||||
ref=menu_ref
|
||||
>
|
||||
<For
|
||||
each=move || options.get()
|
||||
key=move |item| item.value.clone()
|
||||
children=move |item| {
|
||||
let item = store_value(item);
|
||||
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
|
||||
>
|
||||
{item.get_value().label}
|
||||
</div>
|
||||
on:click=onclick
|
||||
>
|
||||
{item.get_value().label}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</Follower>
|
||||
</Binder>
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
</Follower>
|
||||
|
@ -98,6 +99,7 @@ fn Panel(
|
|||
selected_time: RwSignal<Option<NaiveTime>>,
|
||||
time_picker_ref: NodeRef<html::Div>,
|
||||
close_panel: Callback<Option<NaiveTime>>,
|
||||
#[prop(into)] is_show_panel: MaybeSignal<bool>,
|
||||
comp_ref: ComponentRef<PanelRef>,
|
||||
) -> impl IntoView {
|
||||
let theme = use_theme(Theme::light);
|
||||
|
@ -173,96 +175,107 @@ fn Panel(
|
|||
});
|
||||
|
||||
view! {
|
||||
<div class="thaw-time-picker-panel" style=move || css_vars.get() ref=panel_ref>
|
||||
<div class="thaw-time-picker-panel__time">
|
||||
<div class="thaw-time-picker-panel__time-hour" ref=hour_ref>
|
||||
<CSSTransition
|
||||
node_ref=panel_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)
|
||||
.map(|hour| {
|
||||
let comp_ref = ComponentRef::<PanelTimeItemRef>::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! {
|
||||
<PanelTimeItem value=hour on:click=on_click is_selected comp_ref/>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
<div class="thaw-time-picker-panel__time-padding"></div>
|
||||
{(0..24)
|
||||
.map(|hour| {
|
||||
let comp_ref = ComponentRef::<PanelTimeItemRef>::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! {
|
||||
<PanelTimeItem value=hour 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-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 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 class="thaw-time-picker-panel__footer">
|
||||
<Button variant=ButtonVariant::Outlined size=ButtonSize::Tiny on_click=now>
|
||||
"Now"
|
||||
</Button>
|
||||
<Button size=ButtonSize::Tiny on_click=ok>
|
||||
"OK"
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="thaw-time-picker-panel__footer">
|
||||
<Button variant=ButtonVariant::Outlined size=ButtonSize::Tiny on_click=now>
|
||||
"Now"
|
||||
</Button>
|
||||
<Button size=ButtonSize::Tiny on_click=ok>
|
||||
"OK"
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue