mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
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:
parent
46af07746c
commit
62ce434774
11 changed files with 1047 additions and 42 deletions
|
@ -81,6 +81,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
|
|||
<Route path="/calendar" view=CalendarPage/>
|
||||
<Route path="/time-picker" view=TimePickerPage/>
|
||||
<Route path="/date-picker" view=DatePickerPage/>
|
||||
<Route path="/popover" view=PopoverPage/>
|
||||
</Route>
|
||||
<Route path="/mobile/tabbar" view=TabbarDemoPage/>
|
||||
<Route path="/mobile/nav-bar" view=NavBarDemoPage/>
|
||||
|
|
|
@ -221,6 +221,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
|
|||
value: "modal".into(),
|
||||
label: "Modal".into(),
|
||||
},
|
||||
MenuItemOption {
|
||||
value: "popover".into(),
|
||||
label: "Popover".into(),
|
||||
},
|
||||
MenuItemOption {
|
||||
value: "progress".into(),
|
||||
label: "Progress".into(),
|
||||
|
|
|
@ -25,6 +25,7 @@ mod message;
|
|||
mod mobile;
|
||||
mod modal;
|
||||
mod nav_bar;
|
||||
mod popover;
|
||||
mod progress;
|
||||
mod radio;
|
||||
mod select;
|
||||
|
@ -70,6 +71,7 @@ pub use message::*;
|
|||
pub use mobile::*;
|
||||
pub use modal::*;
|
||||
pub use nav_bar::*;
|
||||
pub use popover::*;
|
||||
pub use progress::*;
|
||||
pub use radio::*;
|
||||
pub use select::*;
|
||||
|
|
326
demo/src/pages/popover/mod.rs
Normal file
326
demo/src/pages/popover/mod.rs
Normal 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>
|
||||
}
|
||||
}
|
|
@ -3,32 +3,282 @@ use web_sys::DomRect;
|
|||
|
||||
#[derive(Clone)]
|
||||
pub enum FollowerPlacement {
|
||||
// Top,
|
||||
// Bottom,
|
||||
// Left,
|
||||
// Right,
|
||||
// TopStart,
|
||||
// TopEnd,
|
||||
// LeftStart,
|
||||
// LeftEnd,
|
||||
// RightStart,
|
||||
// RightEnd,
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
TopStart,
|
||||
TopEnd,
|
||||
LeftStart,
|
||||
LeftEnd,
|
||||
RightStart,
|
||||
RightEnd,
|
||||
BottomStart,
|
||||
// BottomEnd,
|
||||
BottomEnd,
|
||||
}
|
||||
|
||||
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,
|
||||
target_rect: DomRect,
|
||||
follower_rect: DomRect,
|
||||
) -> Option<String> {
|
||||
// TODO: Implements FollowerPlacement more properties
|
||||
_ = placement;
|
||||
let mut style = String::new();
|
||||
) -> Option<FollowerPlacementOffset> {
|
||||
match placement {
|
||||
FollowerPlacement::Top => {
|
||||
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 - 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::Bottom)
|
||||
} else {
|
||||
(top, FollowerPlacement::Top)
|
||||
}
|
||||
};
|
||||
Some(FollowerPlacementOffset {
|
||||
top,
|
||||
left,
|
||||
transform: String::from("translateX(-50%)"),
|
||||
placement,
|
||||
})
|
||||
}
|
||||
FollowerPlacement::TopStart => {
|
||||
let left = target_rect.x();
|
||||
let top = {
|
||||
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();
|
||||
|
@ -39,17 +289,79 @@ pub fn get_follower_placement_style(
|
|||
};
|
||||
|
||||
if top + follower_height > inner_height && target_y - follower_height >= 0.0 {
|
||||
target_y - follower_height
|
||||
(target_y - follower_height, FollowerPlacement::Top)
|
||||
} else {
|
||||
top
|
||||
(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;
|
||||
|
||||
style.push_str(&format!(
|
||||
"transform: translateX({left}px) translateY({top}px);"
|
||||
));
|
||||
let Some(inner_height) = window_inner_height() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(style)
|
||||
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 {
|
||||
return None;
|
||||
};
|
||||
Some(inner_width)
|
||||
}
|
||||
|
||||
fn window_inner_height() -> Option<f64> {
|
||||
|
|
|
@ -5,8 +5,8 @@ use crate::{
|
|||
utils::{add_event_listener, EventListenerHandle},
|
||||
utils::{mount_style, with_hydration_off},
|
||||
};
|
||||
use get_placement_style::get_follower_placement_style;
|
||||
pub use get_placement_style::FollowerPlacement;
|
||||
use get_placement_style::{get_follower_placement_offset, FollowerPlacementOffset};
|
||||
use leptos::{
|
||||
html::{AnyElement, ElementDescriptor, ToHtmlElement},
|
||||
leptos_dom::helpers::WindowListenerHandle,
|
||||
|
@ -19,6 +19,7 @@ pub struct Follower {
|
|||
show: MaybeSignal<bool>,
|
||||
#[prop(optional)]
|
||||
width: Option<FollowerWidth>,
|
||||
#[prop(into)]
|
||||
placement: FollowerPlacement,
|
||||
children: Children,
|
||||
}
|
||||
|
@ -145,6 +146,7 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
|
|||
) -> impl IntoView {
|
||||
let content_ref = create_node_ref::<html::Div>();
|
||||
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 Some(content_ref) = content_ref.get_untracked() else {
|
||||
return;
|
||||
|
@ -162,10 +164,17 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
|
|||
};
|
||||
style.push_str(&width);
|
||||
}
|
||||
if let Some(placement_style) =
|
||||
get_follower_placement_style(placement, target_rect, content_rect)
|
||||
if let Some(FollowerPlacementOffset {
|
||||
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 {
|
||||
logging::error!("Thaw-Binder: get_follower_placement_style return None");
|
||||
}
|
||||
|
@ -201,6 +210,7 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
|
|||
.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()),
|
||||
|
|
|
@ -24,6 +24,7 @@ mod menu;
|
|||
mod message;
|
||||
pub mod mobile;
|
||||
mod modal;
|
||||
mod popover;
|
||||
mod progress;
|
||||
mod radio;
|
||||
mod select;
|
||||
|
@ -66,6 +67,7 @@ pub use loading_bar::*;
|
|||
pub use menu::*;
|
||||
pub use message::*;
|
||||
pub use modal::*;
|
||||
pub use popover::*;
|
||||
pub use progress::*;
|
||||
pub use radio::*;
|
||||
pub use select::*;
|
||||
|
|
185
src/popover/mod.rs
Normal file
185
src/popover/mod.rs
Normal 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
140
src/popover/popover.css
Normal 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
20
src/popover/theme.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,9 +4,9 @@ use self::common::CommonTheme;
|
|||
use crate::{
|
||||
mobile::{NavBarTheme, TabbarTheme},
|
||||
AlertTheme, AutoCompleteTheme, AvatarTheme, BreadcrumbTheme, ButtonTheme, CalendarTheme,
|
||||
ColorPickerTheme, DatePickerTheme, InputTheme, MenuTheme, MessageTheme, ProgressTheme,
|
||||
SelectTheme, SkeletionTheme, SliderTheme, SpinnerTheme, SwitchTheme, TableTheme, TagTheme,
|
||||
TimePickerTheme, TypographyTheme, UploadTheme,
|
||||
ColorPickerTheme, DatePickerTheme, InputTheme, MenuTheme, MessageTheme, PopoverTheme,
|
||||
ProgressTheme, SelectTheme, SkeletionTheme, SliderTheme, SpinnerTheme, SwitchTheme, TableTheme,
|
||||
TagTheme, TimePickerTheme, TypographyTheme, UploadTheme,
|
||||
};
|
||||
use leptos::*;
|
||||
|
||||
|
@ -43,6 +43,7 @@ pub struct Theme {
|
|||
pub calendar: CalendarTheme,
|
||||
pub time_picker: TimePickerTheme,
|
||||
pub date_picker: DatePickerTheme,
|
||||
pub popover: PopoverTheme,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
|
@ -74,6 +75,7 @@ impl Theme {
|
|||
calendar: CalendarTheme::light(),
|
||||
time_picker: TimePickerTheme::light(),
|
||||
date_picker: DatePickerTheme::light(),
|
||||
popover: PopoverTheme::light(),
|
||||
}
|
||||
}
|
||||
pub fn dark() -> Self {
|
||||
|
@ -104,6 +106,7 @@ impl Theme {
|
|||
calendar: CalendarTheme::dark(),
|
||||
time_picker: TimePickerTheme::dark(),
|
||||
date_picker: DatePickerTheme::dark(),
|
||||
popover: PopoverTheme::dark(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue