From 0304523c24f49ba8f007c4aaf4d6761056e3ffa2 Mon Sep 17 00:00:00 2001 From: Krzysztof Andrelczyk Date: Sun, 23 Jun 2024 00:06:07 +0200 Subject: [PATCH 1/4] dropdown with icon --- demo/src/app.rs | 1 + demo/src/pages/components.rs | 4 + demo_markdown/docs/dropdown/mod.md | 172 ++++++++++++++++++++++++ demo_markdown/src/lib.rs | 3 +- thaw/src/dropdown/dropdown-item.css | 16 +++ thaw/src/dropdown/dropdown.css | 71 ++++++++++ thaw/src/dropdown/dropdown_item.rs | 73 ++++++++++ thaw/src/dropdown/mod.rs | 201 ++++++++++++++++++++++++++++ thaw/src/dropdown/theme.rs | 26 ++++ thaw/src/lib.rs | 2 + thaw/src/theme/mod.rs | 5 +- 11 files changed, 572 insertions(+), 2 deletions(-) create mode 100644 demo_markdown/docs/dropdown/mod.md create mode 100644 thaw/src/dropdown/dropdown-item.css create mode 100644 thaw/src/dropdown/dropdown.css create mode 100644 thaw/src/dropdown/dropdown_item.rs create mode 100644 thaw/src/dropdown/mod.rs create mode 100644 thaw/src/dropdown/theme.rs diff --git a/demo/src/app.rs b/demo/src/app.rs index a2437706..191a8261 100644 --- a/demo/src/app.rs +++ b/demo/src/app.rs @@ -62,6 +62,7 @@ fn TheRouter(is_routing: RwSignal) -> impl IntoView { + diff --git a/demo/src/pages/components.rs b/demo/src/pages/components.rs index 1cf7946e..4864780d 100644 --- a/demo/src/pages/components.rs +++ b/demo/src/pages/components.rs @@ -130,6 +130,10 @@ pub(crate) fn gen_menu_data() -> Vec { value: "divider".into(), label: "Divider".into(), }, + MenuItemOption { + value: "dropdown".into(), + label: "Dropdown".into(), + }, MenuItemOption { value: "icon".into(), label: "Icon".into(), diff --git a/demo_markdown/docs/dropdown/mod.md b/demo_markdown/docs/dropdown/mod.md new file mode 100644 index 00000000..9b20affd --- /dev/null +++ b/demo_markdown/docs/dropdown/mod.md @@ -0,0 +1,172 @@ +# Dropdown + +```rust demo +let value = create_rw_signal(None::); +let message = use_message(); +let facebook = move |_| { + message.create( + "Facebook".into(), + MessageVariant::Success, + Default::default(), + ); +}; +let twitter = move |_| { + message.create( + "Twitter".into(), + MessageVariant::Warning, + Default::default(), + ); +}; +view! { + + + + + + + + + + + + + + + + + +} +``` + +### Placement + +```rust demo +use leptos_meta::Style; + +view! { + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + + + + + + "Content" + + + +} +``` + +### Select Props + +| Name | Type | Default | Description | +| ------- | ----------------------------------- | -------------------- | ----------------------------------------- | +| class | `OptionalProp>` | `Default::default()` | Addtional classes for the select element. | +| value | `Model>` | `None` | Checked value. | +| options | `MaybeSignal>>` | `vec![]` | Options that can be selected. | + +### Multiple Select Props + +| Name | Type | Default | Description | +| --------- | ----------------------------------- | -------------------- | ----------------------------------------- | +| class | `OptionalProp>` | `Default::default()` | Addtional classes for the select element. | +| value | `Model>` | `vec![]` | Checked values. | +| options | `MaybeSignal>>` | `vec![]` | Options that can be selected. | +| clearable | `MaybeSignal` | `false` | Allow the options to be cleared. | + +### Select Slots + +| Name | Default | Description | +| ----------- | ------- | ------------- | +| SelectLabel | `None` | Select label. | diff --git a/demo_markdown/src/lib.rs b/demo_markdown/src/lib.rs index e5322c7e..680e4e26 100644 --- a/demo_markdown/src/lib.rs +++ b/demo_markdown/src/lib.rs @@ -70,7 +70,8 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt "ThemeMdPage" => "../docs/theme/mod.md", "TimePickerMdPage" => "../docs/time_picker/mod.md", "TypographyMdPage" => "../docs/typography/mod.md", - "UploadMdPage" => "../docs/upload/mod.md" + "UploadMdPage" => "../docs/upload/mod.md", + "DropdownMdPage" => "../docs/dropdown/mod.md" }; let mut fn_list = vec![]; diff --git a/thaw/src/dropdown/dropdown-item.css b/thaw/src/dropdown/dropdown-item.css new file mode 100644 index 00000000..18027df3 --- /dev/null +++ b/thaw/src/dropdown/dropdown-item.css @@ -0,0 +1,16 @@ +.thaw-dropdown-item{ + padding: 6px 5px; + border-radius: 2px; + cursor: pointer; + display: flex; + align-items: center; +} + +.thaw-dropdown-item:hover:not(.thaw-dropdown-item--disabled) { + background-color: var(--thaw-background-color-hover); +} + +.thaw-dropdown-item.thaw-dropdown-item--disabled { + color: var(--thaw-font-color-disabled); + cursor: not-allowed; +} diff --git a/thaw/src/dropdown/dropdown.css b/thaw/src/dropdown/dropdown.css new file mode 100644 index 00000000..0ecf1529 --- /dev/null +++ b/thaw/src/dropdown/dropdown.css @@ -0,0 +1,71 @@ +.thaw-dropdown { + position: relative; + padding: 5px; + background-color: var(--thaw-background-color); + color: var(--thaw-font-color); + border-radius: 3px; + transform-origin: inherit; +} + +.thaw-dropdown-trigger { +} + +[data-thaw-placement="top-start"] > .thaw-dropdown, +[data-thaw-placement="top-end"] > .thaw-dropdown, +[data-thaw-placement="top"] > .thaw-dropdown { + margin-bottom: 4px; + box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); +} + +[data-thaw-placement="bottom-start"] > .thaw-dropdown, +[data-thaw-placement="bottom-end"] > .thaw-dropdown, +[data-thaw-placement="bottom"] > .thaw-dropdown { + margin-top: 4px; + box-shadow: 0 -3px 6px -4px rgba(0, 0, 0, 0.12), + 0 -6px 16px 0 rgba(0, 0, 0, 0.08), 0 -9px 28px 8px rgba(0, 0, 0, 0.05); +} + +[data-thaw-placement="left-start"] > .thaw-dropdown, +[data-thaw-placement="left-end"] > .thaw-dropdown, +[data-thaw-placement="left"] > .thaw-dropdown { + margin-right: 4px; + box-shadow: 3px 0 6px -4px rgba(0, 0, 0, 0.12), + 6px 0 16px 0 rgba(0, 0, 0, 0.08), 9px 0 28px 8px rgba(0, 0, 0, 0.05); +} + +[data-thaw-placement="right-start"] > .thaw-dropdown, +[data-thaw-placement="right-end"] > .thaw-dropdown, +[data-thaw-placement="right"] > .thaw-dropdown { + margin-left: 4px; + box-shadow: -3px 0 6px -4px rgba(0, 0, 0, 0.12), + -6px 0 16px 0 rgba(0, 0, 0, 0.08), -9px 0 28px 8px rgba(0, 0, 0, 0.05); +} + +.thaw-dropdown.dropdown-transition-enter-from, +.thaw-dropdown.dropdown-transition-leave-to { + opacity: 0; + transform: scale(0.85); +} + +.thaw-dropdown.dropdown-transition-enter-to, +.thaw-dropdown.dropdown-transition-leave-from { + transform: scale(1); + opacity: 1; +} + +.thaw-dropdown.dropdown-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-dropdown.dropdown-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/dropdown/dropdown_item.rs b/thaw/src/dropdown/dropdown_item.rs new file mode 100644 index 00000000..f23b11f0 --- /dev/null +++ b/thaw/src/dropdown/dropdown_item.rs @@ -0,0 +1,73 @@ +use leptos::*; +use thaw_components::{Fallback, If, OptionComp, Then}; +use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp}; + +use crate::{dropdown::HasIcon, use_theme, Icon, Theme}; + +#[component] +pub fn DropdownItem( + #[prop(optional, into)] icon: OptionalMaybeSignal, + #[prop(into)] label: MaybeSignal, + #[prop(optional, into)] disabled: MaybeSignal, + #[prop(optional, into)] class: OptionalProp>, + #[prop(optional, into)] on_click: Option>, +) -> impl IntoView { + mount_style("dropdown-item", include_str!("./dropdown-item.css")); + let theme = use_theme(Theme::light); + let css_vars = create_memo(move |_| { + let mut css_vars = String::new(); + theme.with(|theme| { + css_vars.push_str(&format!( + "--thaw-background-color-hover: {};", + theme.dropdown.item_color_hover + )); + css_vars.push_str(&format!( + "--thaw-font-color-disabled: {};", + theme.dropdown.font_color_disabled + )); + }); + css_vars + }); + + let has_icon = use_context::().expect("HasIcon not provided").0; + + if icon.get().is_some() { + has_icon.set(true); + } + + let on_click = move |event| { + if disabled.get() { + return; + } + let Some(callback) = on_click.as_ref() else { + return; + }; + callback.call(event); + }; + + view! { +
+ + + + + + + + + + + + + {label} +
+ } +} diff --git a/thaw/src/dropdown/mod.rs b/thaw/src/dropdown/mod.rs new file mode 100644 index 00000000..de02fb3c --- /dev/null +++ b/thaw/src/dropdown/mod.rs @@ -0,0 +1,201 @@ +mod dropdown_item; +mod theme; + +pub use dropdown_item::*; + +use std::time::Duration; + +use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement}; +pub use theme::DropdownTheme; + +use leptos::{html::Div, leptos_dom::helpers::TimeoutHandle, *}; +use thaw_utils::{add_event_listener, mount_style, OptionalProp}; + +use crate::{use_theme, Theme}; + +#[slot] +pub struct DropdownTrigger { + children: Children, +} + +#[derive(Copy, Clone)] +struct HasIcon(RwSignal); + +fn call_on_click_outside(element: NodeRef
, on_click: Callback<()>) { + #[cfg(any(feature = "csr", feature = "hydrate"))] + { + let handle = window_event_listener(ev::click, move |ev| { + use leptos::wasm_bindgen::__rt::IntoJsResult; + let el = ev.target(); + let mut el: Option = + el.into_js_result().map_or(None, |el| Some(el.into())); + let body = document().body().unwrap(); + while let Some(current_el) = el { + if current_el == *body { + break; + }; + let Some(dropdown_el) = element.get_untracked() else { + break; + }; + if current_el == ***dropdown_el { + return; + } + el = current_el.parent_element(); + } + on_click.call(()); + }); + on_cleanup(move || handle.remove()); + } +} + +#[component] +pub fn Dropdown( + #[prop(optional, into)] class: OptionalProp>, + dropdown_trigger: DropdownTrigger, + #[prop(optional)] trigger_type: DropdownTriggerType, + #[prop(optional)] placement: DropdownPlacement, + children: Children, +) -> impl IntoView { + mount_style("dropdown", include_str!("./dropdown.css")); + let theme = use_theme(Theme::light); + let css_vars = create_memo(move |_| { + let mut css_vars = String::new(); + theme.with(|theme| { + css_vars.push_str(&format!( + "--thaw-background-color: {};", + theme.dropdown.background_color + )); + css_vars.push_str(&format!("--thaw-font-color: {};", theme.common.font_color)); + }); + css_vars + }); + let dropdown_ref = create_node_ref::(); + let target_ref = create_node_ref::(); + let is_show_dropdown = create_rw_signal(false); + let show_dropdown_handle = store_value(None::); + + let on_mouse_enter = move |_| { + if trigger_type != DropdownTriggerType::Hover { + return; + } + show_dropdown_handle.update_value(|handle| { + if let Some(handle) = handle.take() { + handle.clear(); + } + }); + is_show_dropdown.set(true); + }; + let on_mouse_leave = move |_| { + if trigger_type != DropdownTriggerType::Hover { + return; + } + show_dropdown_handle.update_value(|handle| { + if let Some(handle) = handle.take() { + handle.clear(); + } + *handle = set_timeout_with_handle( + move || { + is_show_dropdown.set(false); + }, + Duration::from_millis(100), + ) + .ok(); + }); + }; + + call_on_click_outside( + dropdown_ref, + Callback::new(move |_| is_show_dropdown.set(false)), + ); + target_ref.on_load(move |target_el| { + add_event_listener(target_el.into_any(), ev::click, move |event| { + event.stop_propagation(); + is_show_dropdown.update(|show| *show = !*show); + }); + }); + let DropdownTrigger { + children: trigger_children, + } = dropdown_trigger; + + provide_context(HasIcon(create_rw_signal(false))); + + view! { + +
+ {trigger_children()} +
+ + +
+
{children()}
+
+
+
+
+ } +} + +#[derive(Default, PartialEq, Clone)] +pub enum DropdownTriggerType { + Hover, + #[default] + Click, +} + +impl Copy for DropdownTriggerType {} + +#[derive(Default)] +pub enum DropdownPlacement { + Top, + #[default] + Bottom, + Left, + Right, + TopStart, + TopEnd, + LeftStart, + LeftEnd, + RightStart, + RightEnd, + BottomStart, + BottomEnd, +} + +impl From for FollowerPlacement { + fn from(value: DropdownPlacement) -> Self { + match value { + DropdownPlacement::Top => Self::Top, + DropdownPlacement::Bottom => Self::Bottom, + DropdownPlacement::Left => Self::Left, + DropdownPlacement::Right => Self::Right, + DropdownPlacement::TopStart => Self::TopStart, + DropdownPlacement::TopEnd => Self::TopEnd, + DropdownPlacement::LeftStart => Self::LeftStart, + DropdownPlacement::LeftEnd => Self::LeftEnd, + DropdownPlacement::RightStart => Self::RightStart, + DropdownPlacement::RightEnd => Self::RightEnd, + DropdownPlacement::BottomStart => Self::BottomStart, + DropdownPlacement::BottomEnd => Self::BottomEnd, + } + } +} diff --git a/thaw/src/dropdown/theme.rs b/thaw/src/dropdown/theme.rs new file mode 100644 index 00000000..03109546 --- /dev/null +++ b/thaw/src/dropdown/theme.rs @@ -0,0 +1,26 @@ +use crate::theme::ThemeMethod; + +#[derive(Clone)] +pub struct DropdownTheme { + pub background_color: String, + pub item_color_hover: String, + pub font_color_disabled: String +} + +impl ThemeMethod for DropdownTheme { + fn light() -> Self { + Self { + background_color: "#fff".into(), + item_color_hover: "#f3f5f6".into(), + font_color_disabled: "#c2c2c2".into(), + } + } + + fn dark() -> Self { + Self { + background_color: "#48484e".into(), + item_color_hover: "#ffffff17".into(), + font_color_disabled: "#ffffff61".into(), + } + } +} diff --git a/thaw/src/lib.rs b/thaw/src/lib.rs index 396ce717..802e7bac 100644 --- a/thaw/src/lib.rs +++ b/thaw/src/lib.rs @@ -15,6 +15,7 @@ mod color_picker; mod date_picker; mod divider; mod drawer; +mod dropdown; mod global_style; mod grid; mod icon; @@ -62,6 +63,7 @@ pub use color_picker::*; pub use date_picker::*; pub use divider::*; pub use drawer::*; +pub use dropdown::*; pub use global_style::*; pub use grid::*; pub use icon::*; diff --git a/thaw/src/theme/mod.rs b/thaw/src/theme/mod.rs index 2f20a9a2..5fedad23 100644 --- a/thaw/src/theme/mod.rs +++ b/thaw/src/theme/mod.rs @@ -7,7 +7,7 @@ use crate::{ ButtonTheme, CalendarTheme, CollapseTheme, ColorPickerTheme, DatePickerTheme, InputTheme, MenuTheme, MessageTheme, PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme, SkeletionTheme, SliderTheme, SpinnerTheme, SwitchTheme, TableTheme, TagTheme, TimePickerTheme, - TypographyTheme, UploadTheme, + TypographyTheme, UploadTheme, DropdownTheme }; use leptos::*; @@ -45,6 +45,7 @@ pub struct Theme { pub time_picker: TimePickerTheme, pub date_picker: DatePickerTheme, pub popover: PopoverTheme, + pub dropdown: DropdownTheme, pub collapse: CollapseTheme, pub scrollbar: ScrollbarTheme, pub back_top: BackTopTheme, @@ -81,6 +82,7 @@ impl Theme { time_picker: TimePickerTheme::light(), date_picker: DatePickerTheme::light(), popover: PopoverTheme::light(), + dropdown: DropdownTheme::light(), collapse: CollapseTheme::light(), scrollbar: ScrollbarTheme::light(), back_top: BackTopTheme::light(), @@ -116,6 +118,7 @@ impl Theme { time_picker: TimePickerTheme::dark(), date_picker: DatePickerTheme::dark(), popover: PopoverTheme::dark(), + dropdown: DropdownTheme::dark(), collapse: CollapseTheme::dark(), scrollbar: ScrollbarTheme::dark(), back_top: BackTopTheme::dark(), From ee9c8d2b39553e3b744d24e465b21755a338ef0c Mon Sep 17 00:00:00 2001 From: Krzysztof Andrelczyk Date: Mon, 24 Jun 2024 08:17:40 +0200 Subject: [PATCH 2/4] dropdown demo page --- demo_markdown/docs/dropdown/mod.md | 45 +++++++++++++++++++----------- demo_markdown/docs/popover/mod.md | 21 ++++++++++---- thaw/src/dropdown/mod.rs | 7 +++-- thaw/src/dropdown/theme.rs | 2 +- thaw/src/theme/mod.rs | 6 ++-- 5 files changed, 52 insertions(+), 29 deletions(-) diff --git a/demo_markdown/docs/dropdown/mod.md b/demo_markdown/docs/dropdown/mod.md index 9b20affd..18d98816 100644 --- a/demo_markdown/docs/dropdown/mod.md +++ b/demo_markdown/docs/dropdown/mod.md @@ -148,25 +148,36 @@ view! { } ``` -### Select Props +### Dropdown Props -| Name | Type | Default | Description | -| ------- | ----------------------------------- | -------------------- | ----------------------------------------- | -| class | `OptionalProp>` | `Default::default()` | Addtional classes for the select element. | -| value | `Model>` | `None` | Checked value. | -| options | `MaybeSignal>>` | `vec![]` | Options that can be selected. | +| Name | Type | Default | Description | +| ------------ | ----------------------------------- | ---------------------------- | ------------------------------------------ | +| class | `OptionalProp>` | `Default::default()` | Addtional classes for the dropdown element. | +| trigger_type | `DropdownTriggerType` | `DropdownTriggerType::Click` | Action that displays the dropdown. | +| placement | `DropdownPlacement` | `DropdownPlacement::Bottom` | Dropdown placement. | +| children | `Children` | | The content inside dropdown. | -### Multiple Select Props +### DropdownItem Props -| Name | Type | Default | Description | -| --------- | ----------------------------------- | -------------------- | ----------------------------------------- | -| class | `OptionalProp>` | `Default::default()` | Addtional classes for the select element. | -| value | `Model>` | `vec![]` | Checked values. | -| options | `MaybeSignal>>` | `vec![]` | Options that can be selected. | -| clearable | `MaybeSignal` | `false` | Allow the options to be cleared. | +| Name | Type | Default | Description | +| -------- | -------------------------------------------- | -------------------- | ------------------------------------------------ | +| class | `OptionalProp>` | `Default::default()` | Addtional classes for the dropdown item element. | +| label | `MaybeSignal` | `Default::default()` | The label of the dropdown item. | +| icon | `OptionalMaybeSignal` | `None` | The icon of the dropdown item. | +| disabled | `MaybeSignal` | `false` | Whether the dropdown item is disabled. | +| on_click | `Option>` | `None` | Listen for dropdown item click events. | -### Select Slots -| Name | Default | Description | -| ----------- | ------- | ------------- | -| SelectLabel | `None` | Select label. | +### Dropdown Slots + +| Name | Default | Description | +| --------------- | ------- | ------------------------------------------------ | +| DropdownTrigger | `None` | The element or component that triggers dropdown. | + +### DropdownTriger Props + +| Name | Type | Default | Description | +| ------------ | ----------------------------------- | ---------------------------- | -------------------------------------------------- | +| class | `OptionalProp>` | `Default::default()` | Addtional classes for the dropdown trigger element. | +| children | `Children` | | The content inside dropdown trigger. | + diff --git a/demo_markdown/docs/popover/mod.md b/demo_markdown/docs/popover/mod.md index 28b2be50..daddd358 100644 --- a/demo_markdown/docs/popover/mod.md +++ b/demo_markdown/docs/popover/mod.md @@ -146,15 +146,24 @@ view! { ### Popover Props -| Name | Type | Default | Description | -| --------- | ----------------------------------- | ----------------------- | ----------------------------- | -| class | `OptionalProp>` | `Default::default()` | Content class of the popover. | -| placement | `PopoverPlacement` | `PopoverPlacement::Top` | Popover placement. | -| tooltip | `bool` | `false` | Tooltip. | -| children | `Children` | | The content inside popover. | +| Name | Type | Default | Description | +| -------------| ----------------------------------- | -------------------------- | --------------------------------- | +| class | `OptionalProp>` | `Default::default()` | Content class of the popover. | +| placement | `PopoverPlacement` | `PopoverPlacement::Top` | Popover placement. | +| trigger_type | `PopoverTriggerType` | `PopoverTriggerType::Hover`| Action that displays the dropdown | +| tooltip | `bool` | `false` | Tooltip. | +| children | `Children` | | The content inside popover. | ### Popover Slots | Name | Default | Description | | -------------- | ------- | ----------------------------------------------- | | PopoverTrigger | | The element or component that triggers popover. | + +### PopoverTriger Props + +| Name | Type | Default | Description | +| ------------ | ----------------------------------- | ---------------------------- | -------------------------------------------------- | +| class | `OptionalProp>` | `Default::default()` | Addtional classes for the popover trigger element. | +| children | `Children` | | The content inside popover trigger. | + diff --git a/thaw/src/dropdown/mod.rs b/thaw/src/dropdown/mod.rs index de02fb3c..386db158 100644 --- a/thaw/src/dropdown/mod.rs +++ b/thaw/src/dropdown/mod.rs @@ -9,12 +9,14 @@ use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement}; pub use theme::DropdownTheme; use leptos::{html::Div, leptos_dom::helpers::TimeoutHandle, *}; -use thaw_utils::{add_event_listener, mount_style, OptionalProp}; +use thaw_utils::{add_event_listener, class_list, mount_style, OptionalProp}; use crate::{use_theme, Theme}; #[slot] pub struct DropdownTrigger { + #[prop(optional, into)] + class: OptionalProp>, children: Children, } @@ -114,6 +116,7 @@ pub fn Dropdown( }); }); let DropdownTrigger { + class: trigger_class, children: trigger_children, } = dropdown_trigger; @@ -122,7 +125,7 @@ pub fn Dropdown( view! {
Date: Mon, 24 Jun 2024 23:29:36 +0200 Subject: [PATCH 3/4] on_select instaed of on_click --- demo_markdown/docs/dropdown/mod.md | 60 +++++++++++++++--------------- thaw/src/dropdown/dropdown_item.rs | 16 ++++---- thaw/src/dropdown/mod.rs | 37 +++++------------- thaw/src/popover/mod.rs | 36 ++++-------------- thaw_utils/src/lib.rs | 2 + thaw_utils/src/on_click_outside.rs | 28 ++++++++++++++ 6 files changed, 86 insertions(+), 93 deletions(-) create mode 100644 thaw_utils/src/on_click_outside.rs diff --git a/demo_markdown/docs/dropdown/mod.md b/demo_markdown/docs/dropdown/mod.md index 18d98816..13c51d1e 100644 --- a/demo_markdown/docs/dropdown/mod.md +++ b/demo_markdown/docs/dropdown/mod.md @@ -3,36 +3,32 @@ ```rust demo let value = create_rw_signal(None::); let message = use_message(); -let facebook = move |_| { - message.create( - "Facebook".into(), - MessageVariant::Success, - Default::default(), - ); -}; -let twitter = move |_| { - message.create( - "Twitter".into(), - MessageVariant::Warning, - Default::default(), - ); + +let on_select = move |key: String| { + match key.as_str() { + "facebook" => message.create( "Facebook".into(), MessageVariant::Success, Default::default(),), + "twitter" => message.create( "Twitter".into(), MessageVariant::Warning, Default::default(),), + _ => () + } }; + + view! { - + - - + + - + - - + + } @@ -43,13 +39,15 @@ view! { ```rust demo use leptos_meta::Style; +let on_select = move |key| println!("{}", key); + view! { - + @@ -57,7 +55,7 @@ view! { - + @@ -65,7 +63,7 @@ view! { - + @@ -73,7 +71,7 @@ view! { - + @@ -81,7 +79,7 @@ view! { - + @@ -89,7 +87,7 @@ view! { - + @@ -97,7 +95,7 @@ view! { - + @@ -105,7 +103,7 @@ view! { - + @@ -113,7 +111,7 @@ view! { - + @@ -121,7 +119,7 @@ view! { - + @@ -129,7 +127,7 @@ view! { - + @@ -137,7 +135,7 @@ view! { - + diff --git a/thaw/src/dropdown/dropdown_item.rs b/thaw/src/dropdown/dropdown_item.rs index f23b11f0..3ffff0b7 100644 --- a/thaw/src/dropdown/dropdown_item.rs +++ b/thaw/src/dropdown/dropdown_item.rs @@ -2,15 +2,18 @@ use leptos::*; use thaw_components::{Fallback, If, OptionComp, Then}; use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp}; -use crate::{dropdown::HasIcon, use_theme, Icon, Theme}; +use crate::{ + dropdown::{HasIcon, OnSelect}, + use_theme, Icon, Theme, +}; #[component] pub fn DropdownItem( #[prop(optional, into)] icon: OptionalMaybeSignal, #[prop(into)] label: MaybeSignal, + #[prop(into)] key: MaybeSignal, #[prop(optional, into)] disabled: MaybeSignal, #[prop(optional, into)] class: OptionalProp>, - #[prop(optional, into)] on_click: Option>, ) -> impl IntoView { mount_style("dropdown-item", include_str!("./dropdown-item.css")); let theme = use_theme(Theme::light); @@ -35,14 +38,13 @@ pub fn DropdownItem( has_icon.set(true); } - let on_click = move |event| { + let on_select = use_context::().expect("OnSelect not provided").0; + + let on_click = move |_| { if disabled.get() { return; } - let Some(callback) = on_click.as_ref() else { - return; - }; - callback.call(event); + on_select.call(key.get()); }; view! { diff --git a/thaw/src/dropdown/mod.rs b/thaw/src/dropdown/mod.rs index 386db158..5c780d86 100644 --- a/thaw/src/dropdown/mod.rs +++ b/thaw/src/dropdown/mod.rs @@ -9,7 +9,9 @@ use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement}; pub use theme::DropdownTheme; use leptos::{html::Div, leptos_dom::helpers::TimeoutHandle, *}; -use thaw_utils::{add_event_listener, class_list, mount_style, OptionalProp}; +use thaw_utils::{ + add_event_listener, call_on_click_outside, class_list, mount_style, OptionalProp, +}; use crate::{use_theme, Theme}; @@ -23,32 +25,8 @@ pub struct DropdownTrigger { #[derive(Copy, Clone)] struct HasIcon(RwSignal); -fn call_on_click_outside(element: NodeRef
, on_click: Callback<()>) { - #[cfg(any(feature = "csr", feature = "hydrate"))] - { - let handle = window_event_listener(ev::click, move |ev| { - use leptos::wasm_bindgen::__rt::IntoJsResult; - let el = ev.target(); - let mut el: Option = - el.into_js_result().map_or(None, |el| Some(el.into())); - let body = document().body().unwrap(); - while let Some(current_el) = el { - if current_el == *body { - break; - }; - let Some(dropdown_el) = element.get_untracked() else { - break; - }; - if current_el == ***dropdown_el { - return; - } - el = current_el.parent_element(); - } - on_click.call(()); - }); - on_cleanup(move || handle.remove()); - } -} +#[derive(Copy, Clone)] +struct OnSelect(Callback); #[component] pub fn Dropdown( @@ -56,6 +34,7 @@ pub fn Dropdown( dropdown_trigger: DropdownTrigger, #[prop(optional)] trigger_type: DropdownTriggerType, #[prop(optional)] placement: DropdownPlacement, + #[prop(into)] on_select: Callback, children: Children, ) -> impl IntoView { mount_style("dropdown", include_str!("./dropdown.css")); @@ -121,6 +100,10 @@ pub fn Dropdown( } = dropdown_trigger; provide_context(HasIcon(create_rw_signal(false))); + provide_context(OnSelect(Callback::::new(move |key| { + is_show_dropdown.set(false); + on_select.call(key); + }))); view! { diff --git a/thaw/src/popover/mod.rs b/thaw/src/popover/mod.rs index 992b8ffb..6a8cca9f 100644 --- a/thaw/src/popover/mod.rs +++ b/thaw/src/popover/mod.rs @@ -6,7 +6,9 @@ use crate::{use_theme, Theme}; use leptos::{leptos_dom::helpers::TimeoutHandle, *}; use std::time::Duration; use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement}; -use thaw_utils::{add_event_listener, class_list, mount_style, OptionalProp}; +use thaw_utils::{ + add_event_listener, call_on_click_outside, class_list, mount_style, OptionalProp, +}; #[slot] pub struct PopoverTrigger { @@ -77,33 +79,11 @@ pub fn Popover( .ok(); }); }; - #[cfg(any(feature = "csr", feature = "hydrate"))] - { - let handle = window_event_listener(ev::click, move |ev| { - use leptos::wasm_bindgen::__rt::IntoJsResult; - if trigger_type != PopoverTriggerType::Click { - return; - } - let el = ev.target(); - let mut el: Option = - el.into_js_result().map_or(None, |el| Some(el.into())); - let body = document().body().unwrap(); - while let Some(current_el) = el { - if current_el == *body { - break; - }; - let Some(popover_el) = popover_ref.get_untracked() else { - break; - }; - if current_el == ***popover_el { - return; - } - el = current_el.parent_element(); - } - is_show_popover.set(false); - }); - on_cleanup(move || handle.remove()); - } + + call_on_click_outside( + popover_ref, + Callback::new(move |_| is_show_popover.set(false)), + ); target_ref.on_load(move |target_el| { add_event_listener(target_el.into_any(), ev::click, move |event| { diff --git a/thaw_utils/src/lib.rs b/thaw_utils/src/lib.rs index 2b6d0710..d53465d3 100644 --- a/thaw_utils/src/lib.rs +++ b/thaw_utils/src/lib.rs @@ -2,6 +2,7 @@ pub mod class_list; mod dom; mod event_listener; mod hooks; +mod on_click_outside; mod optional_prop; mod signals; mod throttle; @@ -12,6 +13,7 @@ pub use event_listener::{ add_event_listener, add_event_listener_with_bool, EventListenerHandle, IntoEventTarget, }; pub use hooks::{use_click_position, use_lock_html_scroll, use_next_frame, NextFrame}; +pub use on_click_outside::call_on_click_outside; pub use optional_prop::OptionalProp; pub use signals::{ create_component_ref, ComponentRef, Model, OptionalMaybeSignal, SignalWatch, StoredMaybeSignal, diff --git a/thaw_utils/src/on_click_outside.rs b/thaw_utils/src/on_click_outside.rs new file mode 100644 index 00000000..a90de276 --- /dev/null +++ b/thaw_utils/src/on_click_outside.rs @@ -0,0 +1,28 @@ +use leptos::{html::Div, *}; + +pub fn call_on_click_outside(element: NodeRef
, on_click: Callback<()>) { + #[cfg(any(feature = "csr", feature = "hydrate"))] + { + let handle = window_event_listener(ev::click, move |ev| { + use leptos::wasm_bindgen::__rt::IntoJsResult; + let el = ev.target(); + let mut el: Option = + el.into_js_result().map_or(None, |el| Some(el.into())); + let body = document().body().unwrap(); + while let Some(current_el) = el { + if current_el == *body { + break; + }; + let Some(dropdown_el) = element.get_untracked() else { + break; + }; + if current_el == ***dropdown_el { + return; + } + el = current_el.parent_element(); + } + on_click.call(()); + }); + on_cleanup(move || handle.remove()); + } +} From f97996e06a280250c688c58abccd2679d4d14288 Mon Sep 17 00:00:00 2001 From: Krzysztof Andrelczyk Date: Tue, 25 Jun 2024 17:58:19 +0200 Subject: [PATCH 4/4] code review fixes --- demo_markdown/docs/dropdown/mod.md | 17 +++++++++-------- thaw/src/dropdown/dropdown.css | 3 --- thaw/src/dropdown/mod.rs | 16 +++++++++++----- thaw/src/popover/mod.rs | 11 ++++++----- thaw_utils/src/on_click_outside.rs | 4 ++-- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/demo_markdown/docs/dropdown/mod.md b/demo_markdown/docs/dropdown/mod.md index 13c51d1e..c2b85ded 100644 --- a/demo_markdown/docs/dropdown/mod.md +++ b/demo_markdown/docs/dropdown/mod.md @@ -1,7 +1,6 @@ # Dropdown ```rust demo -let value = create_rw_signal(None::); let message = use_message(); let on_select = move |key: String| { @@ -15,20 +14,21 @@ let on_select = move |key: String| { view! { - + - + - + - + - + + } @@ -149,8 +149,9 @@ view! { ### Dropdown Props | Name | Type | Default | Description | -| ------------ | ----------------------------------- | ---------------------------- | ------------------------------------------ | +| ------------ | ----------------------------------- | ---------------------------- | ------------------------------------------- | | class | `OptionalProp>` | `Default::default()` | Addtional classes for the dropdown element. | +| on_select | `Callback` | | Called when item is selected. | | trigger_type | `DropdownTriggerType` | `DropdownTriggerType::Click` | Action that displays the dropdown. | | placement | `DropdownPlacement` | `DropdownPlacement::Bottom` | Dropdown placement. | | children | `Children` | | The content inside dropdown. | @@ -160,10 +161,10 @@ view! { | Name | Type | Default | Description | | -------- | -------------------------------------------- | -------------------- | ------------------------------------------------ | | class | `OptionalProp>` | `Default::default()` | Addtional classes for the dropdown item element. | +| key | `MaybeSignal` | `Default::default()` | The key of the dropdown item. | | label | `MaybeSignal` | `Default::default()` | The label of the dropdown item. | | icon | `OptionalMaybeSignal` | `None` | The icon of the dropdown item. | | disabled | `MaybeSignal` | `false` | Whether the dropdown item is disabled. | -| on_click | `Option>` | `None` | Listen for dropdown item click events. | ### Dropdown Slots diff --git a/thaw/src/dropdown/dropdown.css b/thaw/src/dropdown/dropdown.css index 0ecf1529..f0a8c5f5 100644 --- a/thaw/src/dropdown/dropdown.css +++ b/thaw/src/dropdown/dropdown.css @@ -7,9 +7,6 @@ transform-origin: inherit; } -.thaw-dropdown-trigger { -} - [data-thaw-placement="top-start"] > .thaw-dropdown, [data-thaw-placement="top-end"] > .thaw-dropdown, [data-thaw-placement="top"] > .thaw-dropdown { diff --git a/thaw/src/dropdown/mod.rs b/thaw/src/dropdown/mod.rs index 5c780d86..14af3bcd 100644 --- a/thaw/src/dropdown/mod.rs +++ b/thaw/src/dropdown/mod.rs @@ -8,7 +8,7 @@ use std::time::Duration; use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement}; pub use theme::DropdownTheme; -use leptos::{html::Div, leptos_dom::helpers::TimeoutHandle, *}; +use leptos::{leptos_dom::helpers::TimeoutHandle, *}; use thaw_utils::{ add_event_listener, call_on_click_outside, class_list, mount_style, OptionalProp, }; @@ -84,12 +84,18 @@ pub fn Dropdown( }); }; - call_on_click_outside( - dropdown_ref, - Callback::new(move |_| is_show_dropdown.set(false)), - ); + if trigger_type != DropdownTriggerType::Hover { + call_on_click_outside( + dropdown_ref, + Callback::new(move |_| is_show_dropdown.set(false)), + ); + } + target_ref.on_load(move |target_el| { add_event_listener(target_el.into_any(), ev::click, move |event| { + if trigger_type != DropdownTriggerType::Click { + return; + } event.stop_propagation(); is_show_dropdown.update(|show| *show = !*show); }); diff --git a/thaw/src/popover/mod.rs b/thaw/src/popover/mod.rs index 6a8cca9f..1778501a 100644 --- a/thaw/src/popover/mod.rs +++ b/thaw/src/popover/mod.rs @@ -80,11 +80,12 @@ pub fn Popover( }); }; - call_on_click_outside( - popover_ref, - Callback::new(move |_| is_show_popover.set(false)), - ); - + if trigger_type != PopoverTriggerType::Hover { + call_on_click_outside( + popover_ref, + Callback::new(move |_| is_show_popover.set(false)), + ); + } target_ref.on_load(move |target_el| { add_event_listener(target_el.into_any(), ev::click, move |event| { if trigger_type != PopoverTriggerType::Click { diff --git a/thaw_utils/src/on_click_outside.rs b/thaw_utils/src/on_click_outside.rs index a90de276..b8879850 100644 --- a/thaw_utils/src/on_click_outside.rs +++ b/thaw_utils/src/on_click_outside.rs @@ -13,10 +13,10 @@ pub fn call_on_click_outside(element: NodeRef
, on_click: Callback<()>) { if current_el == *body { break; }; - let Some(dropdown_el) = element.get_untracked() else { + let Some(displayed_el) = element.get_untracked() else { break; }; - if current_el == ***dropdown_el { + if current_el == ***displayed_el { return; } el = current_el.parent_element();