Feature/dropdown (#210)

* dropdown with icon

* dropdown demo page

* on_select instaed of on_click

* code review fixes
This commit is contained in:
kandrelczyk 2024-06-26 13:06:18 +00:00 committed by GitHub
parent 2f96fec20d
commit 7685e99c8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 628 additions and 37 deletions

View file

@ -62,6 +62,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
<Route path="/date-picker" view=DatePickerMdPage/>
<Route path="/divider" view=DividerMdPage/>
<Route path="/drawer" view=DrawerMdPage/>
<Route path="/dropdown" view=DropdownMdPage/>
<Route path="/grid" view=GridMdPage/>
<Route path="/icon" view=IconMdPage/>
<Route path="/image" view=ImageMdPage/>

View file

@ -130,6 +130,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
value: "divider".into(),
label: "Divider".into(),
},
MenuItemOption {
value: "dropdown".into(),
label: "Dropdown".into(),
},
MenuItemOption {
value: "icon".into(),
label: "Icon".into(),

View file

@ -0,0 +1,182 @@
# Dropdown
```rust demo
let message = use_message();
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! {
<Space>
<Dropdown on_select trigger_type=DropdownTriggerType::Hover>
<DropdownTrigger slot>
<Button>"Hover"</Button>
</DropdownTrigger>
<DropdownItem key="facebook" icon=icondata::AiFacebookOutlined label="Facebook"></DropdownItem>
<DropdownItem key="twitter" disabled=true icon=icondata::AiTwitterOutlined label="Twitter"></DropdownItem>
</Dropdown>
<Dropdown on_select>
<DropdownTrigger slot>
<Button>"Click"</Button>
</DropdownTrigger>
<DropdownItem key="facebook" icon=icondata::AiFacebookOutlined label="Facebook"></DropdownItem>
<DropdownItem key="twitter" icon=icondata::AiTwitterOutlined label="Twitter"></DropdownItem>
<DropdownItem key="no_icon" disabled=true label="Mastodon"></DropdownItem>
</Dropdown>
</Space>
}
```
### Placement
```rust demo
use leptos_meta::Style;
let on_select = move |key| println!("{}", key);
view! {
<Style>
".demo-dropdown .thaw-button { width: 100% } .demo-dropdown .thaw-dropdown-trigger { display: block }"
</Style>
<Grid x_gap=8 y_gap=8 cols=3 class="demo-dropdown">
<GridItem>
<Dropdown on_select placement=DropdownPlacement::TopStart>
<DropdownTrigger slot>
<Button>"Top Start"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::Top>
<DropdownTrigger slot>
<Button>"Top"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::TopEnd>
<DropdownTrigger slot>
<Button>"Top End"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::LeftStart>
<DropdownTrigger slot>
<Button>"Left Start"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem offset=1>
<Dropdown on_select placement=DropdownPlacement::RightStart>
<DropdownTrigger slot>
<Button>"Right Start"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::Left>
<DropdownTrigger slot>
<Button>"Left"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem offset=1>
<Dropdown on_select placement=DropdownPlacement::Right>
<DropdownTrigger slot>
<Button>"Right"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::LeftEnd>
<DropdownTrigger slot>
<Button>"Left End"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem offset=1>
<Dropdown on_select placement=DropdownPlacement::RightEnd>
<DropdownTrigger slot>
<Button>"Right End"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::BottomStart>
<DropdownTrigger slot>
<Button>"Bottom Start"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::Bottom>
<DropdownTrigger slot>
<Button>"Bottom"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::BottomEnd>
<DropdownTrigger slot>
<Button>"Bottom End"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
</Grid>
}
```
### Dropdown Props
| Name | Type | Default | Description |
| ------------ | ----------------------------------- | ---------------------------- | ------------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the dropdown element. |
| on_select | `Callback<String>` | | 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. |
### DropdownItem Props
| Name | Type | Default | Description |
| -------- | -------------------------------------------- | -------------------- | ------------------------------------------------ |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the dropdown item element. |
| key | `MaybeSignal<String>` | `Default::default()` | The key of the dropdown item. |
| label | `MaybeSignal<String>` | `Default::default()` | The label of the dropdown item. |
| icon | `OptionalMaybeSignal<icondata_core::Icon>` | `None` | The icon of the dropdown item. |
| disabled | `MaybeSignal<bool>` | `false` | Whether the dropdown item is disabled. |
### Dropdown Slots
| Name | Default | Description |
| --------------- | ------- | ------------------------------------------------ |
| DropdownTrigger | `None` | The element or component that triggers dropdown. |
### DropdownTriger Props
| Name | Type | Default | Description |
| ------------ | ----------------------------------- | ---------------------------- | -------------------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the dropdown trigger element. |
| children | `Children` | | The content inside dropdown trigger. |

View file

@ -147,9 +147,10 @@ view! {
### Popover Props
| Name | Type | Default | Description |
| --------- | ----------------------------------- | ----------------------- | ----------------------------- |
| -------------| ----------------------------------- | -------------------------- | --------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `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. |
@ -158,3 +159,11 @@ view! {
| Name | Default | Description |
| -------------- | ------- | ----------------------------------------------- |
| PopoverTrigger | | The element or component that triggers popover. |
### PopoverTriger Props
| Name | Type | Default | Description |
| ------------ | ----------------------------------- | ---------------------------- | -------------------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the popover trigger element. |
| children | `Children` | | The content inside popover trigger. |

View file

@ -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![];

View file

@ -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;
}

View file

@ -0,0 +1,68 @@
.thaw-dropdown {
position: relative;
padding: 5px;
background-color: var(--thaw-background-color);
color: var(--thaw-font-color);
border-radius: 3px;
transform-origin: inherit;
}
[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);
}

View file

@ -0,0 +1,75 @@
use leptos::*;
use thaw_components::{Fallback, If, OptionComp, Then};
use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp};
use crate::{
dropdown::{HasIcon, OnSelect},
use_theme, Icon, Theme,
};
#[component]
pub fn DropdownItem(
#[prop(optional, into)] icon: OptionalMaybeSignal<icondata_core::Icon>,
#[prop(into)] label: MaybeSignal<String>,
#[prop(into)] key: MaybeSignal<String>,
#[prop(optional, into)] disabled: MaybeSignal<bool>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
) -> 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::<HasIcon>().expect("HasIcon not provided").0;
if icon.get().is_some() {
has_icon.set(true);
}
let on_select = use_context::<OnSelect>().expect("OnSelect not provided").0;
let on_click = move |_| {
if disabled.get() {
return;
}
on_select.call(key.get());
};
view! {
<div
class=class_list![
"thaw-dropdown-item", ("thaw-dropdown-item--disabled", move || disabled.get()),
class.map(| c | move || c.get())
]
style=move || css_vars.get()
on:click=on_click
>
<OptionComp value=icon.get() let:icon>
<Fallback slot>
<If cond=has_icon>
<Then slot>
<span style="width: 18px; margin-right: 8px"></span>
</Then>
</If>
</Fallback>
<Icon icon=icon style="font-size: 18px; margin-right: 8px"/>
</OptionComp>
<span style="flex-grow: 1">{label}</span>
</div>
}
}

193
thaw/src/dropdown/mod.rs Normal file
View file

@ -0,0 +1,193 @@
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::{leptos_dom::helpers::TimeoutHandle, *};
use thaw_utils::{
add_event_listener, call_on_click_outside, class_list, mount_style, OptionalProp,
};
use crate::{use_theme, Theme};
#[slot]
pub struct DropdownTrigger {
#[prop(optional, into)]
class: OptionalProp<MaybeSignal<String>>,
children: Children,
}
#[derive(Copy, Clone)]
struct HasIcon(RwSignal<bool>);
#[derive(Copy, Clone)]
struct OnSelect(Callback<String>);
#[component]
pub fn Dropdown(
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
dropdown_trigger: DropdownTrigger,
#[prop(optional)] trigger_type: DropdownTriggerType,
#[prop(optional)] placement: DropdownPlacement,
#[prop(into)] on_select: Callback<String>,
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::<html::Div>();
let target_ref = create_node_ref::<html::Div>();
let is_show_dropdown = create_rw_signal(false);
let show_dropdown_handle = store_value(None::<TimeoutHandle>);
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();
});
};
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);
});
});
let DropdownTrigger {
class: trigger_class,
children: trigger_children,
} = dropdown_trigger;
provide_context(HasIcon(create_rw_signal(false)));
provide_context(OnSelect(Callback::<String>::new(move |key| {
is_show_dropdown.set(false);
on_select.call(key);
})));
view! {
<Binder target_ref>
<div
class=class_list!["thaw-dropdown-trigger", trigger_class.map(| c | move || c.get())]
ref=target_ref
on:mouseenter=on_mouse_enter
on:mouseleave=on_mouse_leave
>
{trigger_children()}
</div>
<Follower slot show=is_show_dropdown placement>
<CSSTransition
node_ref=dropdown_ref
name="dropdown-transition"
appear=is_show_dropdown.get_untracked()
show=is_show_dropdown
let:display
>
<div
class="thaw-dropdown"
style=move || {
display.get().map(|d| d.to_string()).unwrap_or_else(|| css_vars.get())
}
ref=dropdown_ref
on:mouseenter=on_mouse_enter
on:mouseleave=on_mouse_leave
>
<div class=class.map(|c| move || c.get())>{children()}</div>
</div>
</CSSTransition>
</Follower>
</Binder>
}
}
#[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<DropdownPlacement> 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,
}
}
}

View file

@ -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(),
}
}
}

View file

@ -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::*;

View file

@ -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,34 +79,13 @@ 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<web_sys::Element> =
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());
}
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 {

View file

@ -4,8 +4,8 @@ use self::common::CommonTheme;
use crate::{
mobile::{NavBarTheme, TabbarTheme},
AlertTheme, AnchorTheme, AutoCompleteTheme, AvatarTheme, BackTopTheme, BreadcrumbTheme,
ButtonTheme, CalendarTheme, CollapseTheme, ColorPickerTheme, DatePickerTheme, InputTheme,
MenuTheme, MessageTheme, PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme,
ButtonTheme, CalendarTheme, CollapseTheme, ColorPickerTheme, DatePickerTheme, DropdownTheme,
InputTheme, MenuTheme, MessageTheme, PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme,
SkeletionTheme, SliderTheme, SpinnerTheme, SwitchTheme, TableTheme, TagTheme, TimePickerTheme,
TypographyTheme, UploadTheme,
};
@ -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(),

View file

@ -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,

View file

@ -0,0 +1,28 @@
use leptos::{html::Div, *};
pub fn call_on_click_outside(element: NodeRef<Div>, 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<web_sys::Element> =
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(displayed_el) = element.get_untracked() else {
break;
};
if current_el == ***displayed_el {
return;
}
el = current_el.parent_element();
}
on_click.call(());
});
on_cleanup(move || handle.remove());
}
}