Feat/popover (#57)

* feat: add popover component

* feat: popover component placement

* feat: popover add style

* feat: popover placement

* feat: popover add click trigger
This commit is contained in:
luoxiaozero 2023-12-21 13:31:17 +08:00 committed by GitHub
parent 46af07746c
commit 62ce434774
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1047 additions and 42 deletions

View file

@ -81,6 +81,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
<Route path="/calendar" view=CalendarPage/> <Route path="/calendar" view=CalendarPage/>
<Route path="/time-picker" view=TimePickerPage/> <Route path="/time-picker" view=TimePickerPage/>
<Route path="/date-picker" view=DatePickerPage/> <Route path="/date-picker" view=DatePickerPage/>
<Route path="/popover" view=PopoverPage/>
</Route> </Route>
<Route path="/mobile/tabbar" view=TabbarDemoPage/> <Route path="/mobile/tabbar" view=TabbarDemoPage/>
<Route path="/mobile/nav-bar" view=NavBarDemoPage/> <Route path="/mobile/nav-bar" view=NavBarDemoPage/>

View file

@ -221,6 +221,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
value: "modal".into(), value: "modal".into(),
label: "Modal".into(), label: "Modal".into(),
}, },
MenuItemOption {
value: "popover".into(),
label: "Popover".into(),
},
MenuItemOption { MenuItemOption {
value: "progress".into(), value: "progress".into(),
label: "Progress".into(), label: "Progress".into(),

View file

@ -25,6 +25,7 @@ mod message;
mod mobile; mod mobile;
mod modal; mod modal;
mod nav_bar; mod nav_bar;
mod popover;
mod progress; mod progress;
mod radio; mod radio;
mod select; mod select;
@ -70,6 +71,7 @@ pub use message::*;
pub use mobile::*; pub use mobile::*;
pub use modal::*; pub use modal::*;
pub use nav_bar::*; pub use nav_bar::*;
pub use popover::*;
pub use progress::*; pub use progress::*;
pub use radio::*; pub use radio::*;
pub use select::*; pub use select::*;

View file

@ -0,0 +1,326 @@
use crate::components::{Demo, DemoCode};
use leptos::*;
use leptos_meta::Style;
use prisms::highlight_str;
use thaw::*;
#[component]
pub fn PopoverPage() -> impl IntoView {
view! {
<div style="width: 896px; margin: 0 auto;">
<h1>"Popover"</h1>
<Demo>
<Space>
<Popover>
<PopoverTrigger slot>
<Button>"Hover"</Button>
</PopoverTrigger>
"Content"
</Popover>
<Popover trigger_type=PopoverTriggerType::Click>
<PopoverTrigger slot>
<Button>"Click"</Button>
</PopoverTrigger>
"Content"
</Popover>
</Space>
<DemoCode slot>
{highlight_str!(
r#"
<Space>
<Popover>
<PopoverTrigger slot>
<Button>
"Hover"
</Button>
</PopoverTrigger>
"Content"
</Popover>
</Space>
"#,
"rust"
)}
</DemoCode>
</Demo>
<h3>"Placement"</h3>
<Style>".demo-popover .thaw-button { width: 100% } .demo-popover .thaw-popover-trigger { display: block }"</Style>
<Demo>
<Grid x_gap=8 y_gap=8 cols=3 class="demo-popover">
<GridItem>
<Popover placement=PopoverPlacement::TopStart>
<PopoverTrigger slot>
<Button>"Top Start"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::Top>
<PopoverTrigger slot>
<Button>"Top"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::TopEnd>
<PopoverTrigger slot>
<Button>"Top End"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::LeftStart>
<PopoverTrigger slot>
<Button>"Left Start"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem offset=1>
<Popover placement=PopoverPlacement::RightStart>
<PopoverTrigger slot>
<Button>"Right Start"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::Left>
<PopoverTrigger slot>
<Button>"Left"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem offset=1>
<Popover placement=PopoverPlacement::Right>
<PopoverTrigger slot>
<Button>"Right"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::LeftEnd>
<PopoverTrigger slot>
<Button>"Left End"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem offset=1>
<Popover placement=PopoverPlacement::RightEnd>
<PopoverTrigger slot>
<Button>"Right End"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::BottomStart>
<PopoverTrigger slot>
<Button>"Bottom Start"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::Bottom>
<PopoverTrigger slot>
<Button>"Bottom"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::BottomEnd>
<PopoverTrigger slot>
<Button>"Bottom End"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
</Grid>
<DemoCode slot>
{highlight_str!(
r#"
<Grid x_gap=8 y_gap=8 cols=3 class="demo-popover">
<GridItem>
<Popover placement=PopoverPlacement::TopStart>
<PopoverTrigger slot>
<Button>"Top Start"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::Top>
<PopoverTrigger slot>
<Button>"Top"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::TopEnd>
<PopoverTrigger slot>
<Button>"Top End"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::LeftStart>
<PopoverTrigger slot>
<Button>"Left Start"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem offset=1>
<Popover placement=PopoverPlacement::RightStart>
<PopoverTrigger slot>
<Button>"Right Start"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::Left>
<PopoverTrigger slot>
<Button>"Left"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem offset=1>
<Popover placement=PopoverPlacement::Right>
<PopoverTrigger slot>
<Button>"Right"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::LeftEnd>
<PopoverTrigger slot>
<Button>"Left End"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem offset=1>
<Popover placement=PopoverPlacement::RightEnd>
<PopoverTrigger slot>
<Button>"Right End"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::BottomStart>
<PopoverTrigger slot>
<Button>"Bottom Start"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::Bottom>
<PopoverTrigger slot>
<Button>"Bottom"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::BottomEnd>
<PopoverTrigger slot>
<Button>"Bottom End"</Button>
</PopoverTrigger>
"Content"
</Popover>
</GridItem>
</Grid>
"#,
"rust"
)}
</DemoCode>
</Demo>
<h3>"Popover Props"</h3>
<Table single_column=true>
<thead>
<tr>
<th>"Name"</th>
<th>"Type"</th>
<th>"Default"</th>
<th>"Description"</th>
</tr>
</thead>
<tbody>
<tr>
<td>"class"</td>
<td>
<Text code=true>"MaybeSignal<String>"</Text>
</td>
<td>
<Text code=true>"Default::default()"</Text>
</td>
<td>"Addtional classes for the trigger element."</td>
</tr>
<tr>
<td>"content_class"</td>
<td>
<Text code=true>"MaybeSignal<String>"</Text>
</td>
<td>
<Text code=true>"Default::default()"</Text>
</td>
<td>"Content class of the popover."</td>
</tr>
<tr>
<td>"placement"</td>
<td>
<Text code=true>"PopoverPlacement"</Text>
</td>
<td>
<Text code=true>"PopoverPlacement::Top"</Text>
</td>
<td>"Popover placement."</td>
</tr>
<tr>
<td>"children"</td>
<td>
<Text code=true>"Children"</Text>
</td>
<td></td>
<td>"The content inside popover."</td>
</tr>
</tbody>
</Table>
<h3>"Popover Slots"</h3>
<Table single_column=true>
<thead>
<tr>
<th>"Name"</th>
<th>"Default"</th>
<th>"Description"</th>
</tr>
</thead>
<tbody>
<tr>
<td>"PopoverTrigger"</td>
<td></td>
<td>"The element or component that triggers popover."</td>
</tr>
</tbody>
</Table>
</div>
}
}

View file

@ -3,53 +3,365 @@ use web_sys::DomRect;
#[derive(Clone)] #[derive(Clone)]
pub enum FollowerPlacement { pub enum FollowerPlacement {
// Top, Top,
// Bottom, Bottom,
// Left, Left,
// Right, Right,
// TopStart, TopStart,
// TopEnd, TopEnd,
// LeftStart, LeftStart,
// LeftEnd, LeftEnd,
// RightStart, RightStart,
// RightEnd, RightEnd,
BottomStart, BottomStart,
// BottomEnd, BottomEnd,
} }
impl Copy for FollowerPlacement {} impl Copy for FollowerPlacement {}
pub fn get_follower_placement_style( impl FollowerPlacement {
pub fn as_str(&self) -> &'static str {
match self {
Self::Top => "top",
Self::Bottom => "bottom",
Self::Left => "left",
Self::Right => "right",
Self::TopStart => "top-start",
Self::TopEnd => "top-end",
Self::LeftStart => "left-start",
Self::LeftEnd => "left-end",
Self::RightStart => "right-start",
Self::RightEnd => "right-end",
Self::BottomStart => "bottom-start",
Self::BottomEnd => "bottom-end",
}
}
}
pub struct FollowerPlacementOffset {
pub top: f64,
pub left: f64,
pub transform: String,
pub placement: FollowerPlacement,
}
pub fn get_follower_placement_offset(
placement: FollowerPlacement, placement: FollowerPlacement,
target_rect: DomRect, target_rect: DomRect,
follower_rect: DomRect, follower_rect: DomRect,
) -> Option<String> { ) -> Option<FollowerPlacementOffset> {
// TODO: Implements FollowerPlacement more properties match placement {
_ = placement; FollowerPlacement::Top => {
let mut style = String::new(); let left = target_rect.x() + target_rect.width() / 2.0;
let left = target_rect.x(); let (top, placement) = {
let top = { let follower_height = follower_rect.height();
let follower_height = follower_rect.height(); let target_y = target_rect.y();
let target_y = target_rect.y(); let target_height = target_rect.height();
let target_height = target_rect.height(); let top = target_y - follower_height;
let top = target_y + target_height;
let Some(inner_height) = window_inner_height() else { let Some(inner_height) = window_inner_height() else {
return None; return None;
}; };
if top + follower_height > inner_height && target_y - follower_height >= 0.0 { if top < 0.0 && target_y + target_height + follower_height <= inner_height {
target_y - follower_height (target_y + target_height, FollowerPlacement::Bottom)
} else { } else {
top (top, FollowerPlacement::Top)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::from("translateX(-50%)"),
placement,
})
} }
FollowerPlacement::TopStart => {
let left = target_rect.x();
let (top, placement) = {
let follower_height = follower_rect.height();
let target_y = target_rect.y();
let target_height = target_rect.height();
let top = target_y - follower_height;
let Some(inner_height) = window_inner_height() else {
return None;
};
if top < 0.0 && target_y + target_height + follower_height <= inner_height {
(target_y + target_height, FollowerPlacement::BottomStart)
} else {
(top, FollowerPlacement::TopStart)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::new(),
placement,
})
}
FollowerPlacement::TopEnd => {
let left = target_rect.x() + target_rect.width();
let (top, placement) = {
let follower_height = follower_rect.height();
let target_y = target_rect.y();
let target_height = target_rect.height();
let top = target_y - follower_height;
let Some(inner_height) = window_inner_height() else {
return None;
};
if top < 0.0 && target_y + target_height + follower_height <= inner_height {
(target_y + target_height, FollowerPlacement::BottomEnd)
} else {
(top, FollowerPlacement::TopEnd)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::from("translateX(-100%)"),
placement,
})
}
FollowerPlacement::Left => {
let top = target_rect.y() + target_rect.height() / 2.0;
let (left, placement) = {
let follower_width = follower_rect.width();
let target_x = target_rect.x();
let target_width = target_rect.width();
let left = target_x - follower_width;
let Some(inner_width) = window_inner_width() else {
return None;
};
if left < 0.0 && target_x + target_width + follower_width > inner_width {
(target_x + follower_width, FollowerPlacement::Right)
} else {
(left, FollowerPlacement::Left)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::from("translateY(-50%)"),
placement,
})
}
FollowerPlacement::LeftStart => {
let top = target_rect.y();
let (left, placement) = {
let follower_width = follower_rect.width();
let target_x = target_rect.x();
let target_width = target_rect.width();
let left = target_x - follower_width;
let Some(inner_width) = window_inner_width() else {
return None;
};
if left < 0.0 && target_x + target_width + follower_width > inner_width {
(target_x + follower_width, FollowerPlacement::RightStart)
} else {
(left, FollowerPlacement::LeftStart)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::new(),
placement,
})
}
FollowerPlacement::LeftEnd => {
let top = target_rect.y() + target_rect.height();
let (left, placement) = {
let follower_width = follower_rect.width();
let target_x = target_rect.x();
let target_width = target_rect.width();
let left = target_x - follower_width;
let Some(inner_width) = window_inner_width() else {
return None;
};
if left < 0.0 && target_x + target_width + follower_width > inner_width {
(target_x + follower_width, FollowerPlacement::RightEnd)
} else {
(left, FollowerPlacement::LeftEnd)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::from("translateY(-100%)"),
placement,
})
}
FollowerPlacement::Right => {
let top = target_rect.y() + target_rect.height() / 2.0;
let (left, placement) = {
let follower_width = follower_rect.width();
let target_x = target_rect.x();
let target_width = target_rect.width();
let left = target_x + target_width;
let Some(inner_width) = window_inner_width() else {
return None;
};
if left + follower_width > inner_width && target_x - follower_width >= 0.0 {
(target_x - follower_width, FollowerPlacement::Left)
} else {
(left, FollowerPlacement::Right)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::from("translateY(-50%)"),
placement,
})
}
FollowerPlacement::RightStart => {
let top = target_rect.y();
let (left, placement) = {
let follower_width = follower_rect.width();
let target_x = target_rect.x();
let target_width = target_rect.width();
let left = target_x + target_width;
let Some(inner_width) = window_inner_width() else {
return None;
};
if left + follower_width > inner_width && target_x - follower_width >= 0.0 {
(target_x - follower_width, FollowerPlacement::LeftStart)
} else {
(left, FollowerPlacement::RightStart)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::new(),
placement,
})
}
FollowerPlacement::RightEnd => {
let top = target_rect.y() + target_rect.height();
let (left, placement) = {
let follower_width = follower_rect.width();
let target_x = target_rect.x();
let target_width = target_rect.width();
let left = target_x + target_width;
let Some(inner_width) = window_inner_width() else {
return None;
};
if left + follower_width > inner_width && target_x - follower_width >= 0.0 {
(target_x - follower_width, FollowerPlacement::LeftEnd)
} else {
(left, FollowerPlacement::RightEnd)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::from("translateY(-100%)"),
placement,
})
}
FollowerPlacement::Bottom => {
let left = target_rect.x() + target_rect.width() / 2.0;
let (top, placement) = {
let follower_height = follower_rect.height();
let target_y = target_rect.y();
let target_height = target_rect.height();
let top = target_y + target_height;
let Some(inner_height) = window_inner_height() else {
return None;
};
if top + follower_height > inner_height && target_y - follower_height >= 0.0 {
(target_y - follower_height, FollowerPlacement::Top)
} else {
(top, FollowerPlacement::Bottom)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::from("translateX(-50%)"),
placement,
})
}
FollowerPlacement::BottomStart => {
let left = target_rect.x();
let (top, placement) = {
let follower_height = follower_rect.height();
let target_y = target_rect.y();
let target_height = target_rect.height();
let top = target_y + target_height;
let Some(inner_height) = window_inner_height() else {
return None;
};
if top + follower_height > inner_height && target_y - follower_height >= 0.0 {
(target_y - follower_height, FollowerPlacement::TopStart)
} else {
(top, FollowerPlacement::BottomStart)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::new(),
placement,
})
}
FollowerPlacement::BottomEnd => {
let left = target_rect.x() + target_rect.width();
let (top, placement) = {
let follower_height = follower_rect.height();
let target_y = target_rect.y();
let target_height = target_rect.height();
let top = target_y + target_height;
let Some(inner_height) = window_inner_height() else {
return None;
};
if top + follower_height > inner_height && target_y - follower_height >= 0.0 {
(target_y - follower_height, FollowerPlacement::TopEnd)
} else {
(top, FollowerPlacement::BottomEnd)
}
};
Some(FollowerPlacementOffset {
top,
left,
transform: String::from("translateX(-100%)"),
placement,
})
}
}
}
fn window_inner_width() -> Option<f64> {
let Ok(inner_width) = window().inner_width() else {
return None;
}; };
let Some(inner_width) = inner_width.as_f64() else {
style.push_str(&format!( return None;
"transform: translateX({left}px) translateY({top}px);" };
)); Some(inner_width)
Some(style)
} }
fn window_inner_height() -> Option<f64> { fn window_inner_height() -> Option<f64> {

View file

@ -5,8 +5,8 @@ use crate::{
utils::{add_event_listener, EventListenerHandle}, utils::{add_event_listener, EventListenerHandle},
utils::{mount_style, with_hydration_off}, utils::{mount_style, with_hydration_off},
}; };
use get_placement_style::get_follower_placement_style;
pub use get_placement_style::FollowerPlacement; pub use get_placement_style::FollowerPlacement;
use get_placement_style::{get_follower_placement_offset, FollowerPlacementOffset};
use leptos::{ use leptos::{
html::{AnyElement, ElementDescriptor, ToHtmlElement}, html::{AnyElement, ElementDescriptor, ToHtmlElement},
leptos_dom::helpers::WindowListenerHandle, leptos_dom::helpers::WindowListenerHandle,
@ -19,6 +19,7 @@ pub struct Follower {
show: MaybeSignal<bool>, show: MaybeSignal<bool>,
#[prop(optional)] #[prop(optional)]
width: Option<FollowerWidth>, width: Option<FollowerWidth>,
#[prop(into)]
placement: FollowerPlacement, placement: FollowerPlacement,
children: Children, children: Children,
} }
@ -145,6 +146,7 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
) -> impl IntoView { ) -> impl IntoView {
let content_ref = create_node_ref::<html::Div>(); let content_ref = create_node_ref::<html::Div>();
let content_style = create_rw_signal(String::new()); let content_style = create_rw_signal(String::new());
let placement_str = create_rw_signal(placement.as_str());
let sync_position: Callback<()> = Callback::new(move |_| { let sync_position: Callback<()> = Callback::new(move |_| {
let Some(content_ref) = content_ref.get_untracked() else { let Some(content_ref) = content_ref.get_untracked() else {
return; return;
@ -162,10 +164,17 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
}; };
style.push_str(&width); style.push_str(&width);
} }
if let Some(placement_style) = if let Some(FollowerPlacementOffset {
get_follower_placement_style(placement, target_rect, content_rect) top,
left,
transform,
placement,
}) = get_follower_placement_offset(placement, target_rect, content_rect)
{ {
style.push_str(&placement_style); placement_str.set(placement.as_str());
style.push_str(&format!(
"transform: translateX({left}px) translateY({top}px) {transform};"
));
} else { } else {
logging::error!("Thaw-Binder: get_follower_placement_style return None"); logging::error!("Thaw-Binder: get_follower_placement_style return None");
} }
@ -201,6 +210,7 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
.child( .child(
html::div() html::div()
.classes("thaw-binder-follower-content") .classes("thaw-binder-follower-content")
.attr("data-thaw-placement", move || placement_str.get())
.node_ref(content_ref) .node_ref(content_ref)
.attr("style", move || content_style.get()) .attr("style", move || content_style.get())
.child(children()), .child(children()),

View file

@ -24,6 +24,7 @@ mod menu;
mod message; mod message;
pub mod mobile; pub mod mobile;
mod modal; mod modal;
mod popover;
mod progress; mod progress;
mod radio; mod radio;
mod select; mod select;
@ -66,6 +67,7 @@ pub use loading_bar::*;
pub use menu::*; pub use menu::*;
pub use message::*; pub use message::*;
pub use modal::*; pub use modal::*;
pub use popover::*;
pub use progress::*; pub use progress::*;
pub use radio::*; pub use radio::*;
pub use select::*; pub use select::*;

185
src/popover/mod.rs Normal file
View file

@ -0,0 +1,185 @@
mod theme;
use std::time::Duration;
use crate::{
components::{Binder, Follower, FollowerPlacement},
use_theme,
utils::{add_event_listener, dyn_classes, mount_style, ssr_class},
Theme,
};
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
pub use theme::PopoverTheme;
#[slot]
pub struct PopoverTrigger {
children: Children,
}
#[component]
pub fn Popover(
#[prop(optional, into)] class: MaybeSignal<String>,
#[prop(optional, into)] content_class: MaybeSignal<String>,
#[prop(optional)] trigger_type: PopoverTriggerType,
popover_trigger: PopoverTrigger,
#[prop(optional)] placement: PopoverPlacement,
children: Children,
) -> impl IntoView {
mount_style("popover", include_str!("./popover.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.time_picker.panel_background_color
));
});
css_vars
});
let popover_ref = create_node_ref::<html::Div>();
let target_ref = create_node_ref::<html::Div>();
let is_show_popover = create_rw_signal(false);
let show_popover_handle = store_value(None::<TimeoutHandle>);
let on_mouse_enter = move |_| {
if trigger_type != PopoverTriggerType::Hover {
return;
}
show_popover_handle.update_value(|handle| {
if let Some(handle) = handle.take() {
handle.clear();
}
});
is_show_popover.set(true);
};
let on_mouse_leave = move |_| {
if trigger_type != PopoverTriggerType::Hover {
return;
}
show_popover_handle.update_value(|handle| {
if let Some(handle) = handle.take() {
handle.clear();
}
*handle = set_timeout_with_handle(
move || {
is_show_popover.set(false);
},
Duration::from_millis(100),
)
.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());
}
target_ref.on_load(move |target_el| {
add_event_listener(target_el.into_any(), ev::click, move |event| {
if trigger_type != PopoverTriggerType::Click {
return;
}
event.stop_propagation();
is_show_popover.update(|show| *show = !*show);
});
});
let ssr_class = ssr_class(&class);
view! {
<Binder target_ref>
<div
class=ssr_class
use:dyn_classes=class
class="thaw-popover-trigger"
ref=target_ref
on:mouseenter=on_mouse_enter
on:mouseleave=on_mouse_leave
>
{(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
>
<div class=move || content_class.get()>{children()}</div>
<div class="thaw-popover__angle-container">
<div class="thaw-popover__angle"></div>
</div>
</div>
</Follower>
</Binder>
}
}
#[derive(Default, PartialEq, Clone)]
pub enum PopoverTriggerType {
#[default]
Hover,
Click,
}
impl Copy for PopoverTriggerType {}
#[derive(Default)]
pub enum PopoverPlacement {
#[default]
Top,
Bottom,
Left,
Right,
TopStart,
TopEnd,
LeftStart,
LeftEnd,
RightStart,
RightEnd,
BottomStart,
BottomEnd,
}
impl From<PopoverPlacement> for FollowerPlacement {
fn from(value: PopoverPlacement) -> Self {
match value {
PopoverPlacement::Top => Self::Top,
PopoverPlacement::Bottom => Self::Bottom,
PopoverPlacement::Left => Self::Left,
PopoverPlacement::Right => Self::Right,
PopoverPlacement::TopStart => Self::TopStart,
PopoverPlacement::TopEnd => Self::TopEnd,
PopoverPlacement::LeftStart => Self::LeftStart,
PopoverPlacement::LeftEnd => Self::LeftEnd,
PopoverPlacement::RightStart => Self::RightStart,
PopoverPlacement::RightEnd => Self::RightEnd,
PopoverPlacement::BottomStart => Self::BottomStart,
PopoverPlacement::BottomEnd => Self::BottomEnd,
}
}
}

140
src/popover/popover.css Normal file
View file

@ -0,0 +1,140 @@
.thaw-popover {
position: relative;
padding: 8px 14px;
background-color: var(--thaw-background-color);
border-radius: 3px;
}
.thaw-popover-trigger {
display: inline-block;
}
.thaw-popover__angle-container {
position: absolute;
}
.thaw-popover__angle {
position: absolute;
background: var(--thaw-background-color);
width: 10px;
height: 10px;
}
[data-thaw-placement="top-start"] > .thaw-popover,
[data-thaw-placement="top-end"] > .thaw-popover,
[data-thaw-placement="top"] > .thaw-popover {
margin-bottom: 10px;
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="top-start"] .thaw-popover__angle-container,
[data-thaw-placement="top-end"] .thaw-popover__angle-container,
[data-thaw-placement="top"] .thaw-popover__angle-container {
width: 100%;
height: 10px;
bottom: -10px;
left: 0;
right: 0;
}
[data-thaw-placement="top-start"] .thaw-popover__angle,
[data-thaw-placement="top-end"] .thaw-popover__angle,
[data-thaw-placement="top"] .thaw-popover__angle {
left: 50%;
transform: rotate(45deg) translateX(-7px);
}
[data-thaw-placement="bottom-start"] > .thaw-popover,
[data-thaw-placement="bottom-end"] > .thaw-popover,
[data-thaw-placement="bottom"] > .thaw-popover {
margin-top: 10px;
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-popover__angle-container,
[data-thaw-placement="bottom-end"] .thaw-popover__angle-container,
[data-thaw-placement="bottom"] .thaw-popover__angle-container {
width: 100%;
height: 10px;
top: -10px;
left: 0;
right: 0;
}
[data-thaw-placement="bottom-start"] .thaw-popover__angle,
[data-thaw-placement="bottom-end"] .thaw-popover__angle,
[data-thaw-placement="bottom"] .thaw-popover__angle {
left: 50%;
transform: rotate(45deg) translateY(7px);
}
[data-thaw-placement="left-start"] > .thaw-popover,
[data-thaw-placement="left-end"] > .thaw-popover,
[data-thaw-placement="left"] > .thaw-popover {
margin-right: 10px;
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="left-start"] .thaw-popover__angle-container,
[data-thaw-placement="left-end"] .thaw-popover__angle-container,
[data-thaw-placement="left"] .thaw-popover__angle-container {
width: 10px;
height: 100%;
right: -10px;
top: 0;
bottom: 0;
}
[data-thaw-placement="left-start"] .thaw-popover__angle,
[data-thaw-placement="left-end"] .thaw-popover__angle,
[data-thaw-placement="left"] .thaw-popover__angle {
top: 50%;
transform: rotate(45deg) translateX(-7px);
}
[data-thaw-placement="right-start"] > .thaw-popover,
[data-thaw-placement="right-end"] > .thaw-popover,
[data-thaw-placement="right"] > .thaw-popover {
margin-left: 10px;
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-popover__angle-container,
[data-thaw-placement="right-end"] .thaw-popover__angle-container,
[data-thaw-placement="right"] .thaw-popover__angle-container {
width: 10px;
height: 100%;
left: -10px;
top: 0;
bottom: 0;
}
[data-thaw-placement="right-start"] .thaw-popover__angle,
[data-thaw-placement="right-end"] .thaw-popover__angle,
[data-thaw-placement="right"] .thaw-popover__angle {
top: 50%;
transform: rotate(45deg) translateY(-7px);
}
[data-thaw-placement="bottom-start"] .thaw-popover__angle,
[data-thaw-placement="top-start"] .thaw-popover__angle {
left: 16px;
}
[data-thaw-placement="bottom-end"] .thaw-popover__angle,
[data-thaw-placement="top-end"] .thaw-popover__angle {
left: initial;
right: 7px;
}
[data-thaw-placement="right-start"] .thaw-popover__angle,
[data-thaw-placement="left-start"] .thaw-popover__angle {
top: 16px;
}
[data-thaw-placement="right-end"] .thaw-popover__angle,
[data-thaw-placement="left-end"] .thaw-popover__angle {
top: initial;
bottom: 7px;
}

20
src/popover/theme.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::theme::ThemeMethod;
#[derive(Clone)]
pub struct PopoverTheme {
pub background_color: String,
}
impl ThemeMethod for PopoverTheme {
fn light() -> Self {
Self {
background_color: "#fff".into(),
}
}
fn dark() -> Self {
Self {
background_color: "#48484e".into(),
}
}
}

View file

@ -4,9 +4,9 @@ use self::common::CommonTheme;
use crate::{ use crate::{
mobile::{NavBarTheme, TabbarTheme}, mobile::{NavBarTheme, TabbarTheme},
AlertTheme, AutoCompleteTheme, AvatarTheme, BreadcrumbTheme, ButtonTheme, CalendarTheme, AlertTheme, AutoCompleteTheme, AvatarTheme, BreadcrumbTheme, ButtonTheme, CalendarTheme,
ColorPickerTheme, DatePickerTheme, InputTheme, MenuTheme, MessageTheme, ProgressTheme, ColorPickerTheme, DatePickerTheme, InputTheme, MenuTheme, MessageTheme, PopoverTheme,
SelectTheme, SkeletionTheme, SliderTheme, SpinnerTheme, SwitchTheme, TableTheme, TagTheme, ProgressTheme, SelectTheme, SkeletionTheme, SliderTheme, SpinnerTheme, SwitchTheme, TableTheme,
TimePickerTheme, TypographyTheme, UploadTheme, TagTheme, TimePickerTheme, TypographyTheme, UploadTheme,
}; };
use leptos::*; use leptos::*;
@ -43,6 +43,7 @@ pub struct Theme {
pub calendar: CalendarTheme, pub calendar: CalendarTheme,
pub time_picker: TimePickerTheme, pub time_picker: TimePickerTheme,
pub date_picker: DatePickerTheme, pub date_picker: DatePickerTheme,
pub popover: PopoverTheme,
} }
impl Theme { impl Theme {
@ -74,6 +75,7 @@ impl Theme {
calendar: CalendarTheme::light(), calendar: CalendarTheme::light(),
time_picker: TimePickerTheme::light(), time_picker: TimePickerTheme::light(),
date_picker: DatePickerTheme::light(), date_picker: DatePickerTheme::light(),
popover: PopoverTheme::light(),
} }
} }
pub fn dark() -> Self { pub fn dark() -> Self {
@ -104,6 +106,7 @@ impl Theme {
calendar: CalendarTheme::dark(), calendar: CalendarTheme::dark(),
time_picker: TimePickerTheme::dark(), time_picker: TimePickerTheme::dark(),
date_picker: DatePickerTheme::dark(), date_picker: DatePickerTheme::dark(),
popover: PopoverTheme::dark(),
} }
} }
} }