Compare commits

..

1 commit

Author SHA1 Message Date
Adam
aa818b2823 fix: leptos 0.7-beta5 changes method trigger to notify 2024-09-08 22:54:00 -04:00
31 changed files with 460 additions and 612 deletions

View file

@ -1,16 +1,3 @@
## [0.3.4](https://github.com/thaw-ui/thaw/compare/v0.3.3...v0.3.4) (2024-09-11)
### Features
* Adds `Flex` component.
* Update leptos to v0.6.14.
### Bug Fixs
* Allow creating custom theme [#242](https://github.com/thaw-ui/thaw/pull/242).
* `CheckboxGroup` does not display default value.
* `Binder` component display position.
## [0.4.0-beta2](https://github.com/thaw-ui/thaw/compare/v0.4.0-beta...v0.4.0-beta2) (2024-09-02)
### Features

View file

@ -16,6 +16,6 @@ thaw_components = { version = "0.2.0-beta2", path = "./thaw_components" }
thaw_macro = { version = "0.1.0-beta2", path = "./thaw_macro" }
thaw_utils = { version = "0.1.0-beta2", path = "./thaw_utils" }
leptos = { version = "0.7.0-beta5" }
leptos_meta = { version = "0.7.0-beta5" }
leptos_router = { version = "0.7.0-beta5" }
leptos = { version = "0.7.0-beta4" }
leptos_meta = { version = "0.7.0-beta4" }
leptos_router = { version = "0.7.0-beta4" }

View file

@ -59,59 +59,66 @@ fn TheRouter() -> impl IntoView {
<Route path=path!("/development/components") view=DevelopmentComponentsMdPage/>
</ParentRoute>
<ParentRoute path=path!("/components") view=ComponentsPage>
<Route path=path!("/accordion") view=AccordionMdPage/>
<Route path=path!("/anchor") view=AnchorMdPage/>
<Route path=path!("/auto-complete") view=AutoCompleteMdPage/>
<Route path=path!("/avatar") view=AvatarMdPage/>
<Route path=path!("/back-top") view=BackTopMdPage/>
<Route path=path!("/badge") view=BadgeMdPage/>
<Route path=path!("/breadcrumb") view=BreadcrumbMdPage/>
<Route path=path!("/button") view=ButtonMdPage/>
<Route path=path!("/calendar") view=CalendarMdPage/>
<Route path=path!("/card") view=CardMdPage/>
<Route path=path!("/checkbox") view=CheckboxMdPage/>
<Route path=path!("/color-picker") view=ColorPickerMdPage/>
<Route path=path!("/combobox") view=ComboboxMdPage/>
<Route path=path!("/config-provider") view=ConfigProviderMdPage/>
<Route path=path!("/date-picker") view=DatePickerMdPage/>
<Route path=path!("/dialog") view=DialogMdPage/>
<Route path=path!("/divider") view=DividerMdPage/>
<Route path=path!("/drawer") view=DrawerMdPage/>
<Route path=path!("/field") view=FieldMdPage/>
<Route path=path!("/flex") view=FlexMdPage/>
<Route path=path!("/menu") view=MenuMdPage/>
<Route path=path!("/grid") view=GridMdPage/>
<Route path=path!("/icon") view=IconMdPage/>
<Route path=path!("/image") view=ImageMdPage/>
<Route path=path!("/input") view=InputMdPage/>
<Route path=path!("/layout") view=LayoutMdPage/>
<Route path=path!("/link") view=LinkMdPage/>
<Route path=path!("/loading-bar") view=LoadingBarMdPage/>
<Route path=path!("/message-bar") view=MessageBarMdPage/>
<Route path=path!("/nav") view=NavMdPage/>
<Route path=path!("/pagination") view=PaginationMdPage/>
<Route path=path!("/popover") view=PopoverMdPage/>
<Route path=path!("/progress-bar") view=ProgressBarMdPage/>
<Route path=path!("/radio") view=RadioMdPage/>
<Route path=path!("/scrollbar") view=ScrollbarMdPage/>
<Route path=path!("/select") view=SelectMdPage/>
<Route path=path!("/skeleton") view=SkeletonMdPage/>
<Route path=path!("/slider") view=SliderMdPage/>
<Route path=path!("/space") view=SpaceMdPage/>
<Route path=path!("/spin-button") view=SpinButtonMdPage/>
<Route path=path!("/spinner") view=SpinnerMdPage/>
<Route path=path!("/switch") view=SwitchMdPage/>
<Route path=path!("/tab-list") view=TabListMdPage/>
<Route path=path!("/table") view=TableMdPage/>
<Route path=path!("/tag") view=TagMdPage/>
<Route path=path!("/tag-group") view=TagGroupMdPage/>
<Route path=path!("/tag-picker") view=TagPickerMdPage/>
<Route path=path!("/text") view=TextMdPage/>
<Route path=path!("/textarea") view=TextareaMdPage/>
<Route path=path!("/time-picker") view=TimePickerMdPage/>
<Route path=path!("/toast") view=ToastMdPage />
<Route path=path!("/tooltip") view=TooltipMdPage />
<Route path=path!("/upload") view=UploadMdPage/>
{view!{
<Route path=path!("/accordion") view=AccordionMdPage/>
<Route path=path!("/anchor") view=AnchorMdPage/>
<Route path=path!("/auto-complete") view=AutoCompleteMdPage/>
<Route path=path!("/avatar") view=AvatarMdPage/>
<Route path=path!("/back-top") view=BackTopMdPage/>
<Route path=path!("/badge") view=BadgeMdPage/>
<Route path=path!("/breadcrumb") view=BreadcrumbMdPage/>
<Route path=path!("/button") view=ButtonMdPage/>
<Route path=path!("/calendar") view=CalendarMdPage/>
<Route path=path!("/card") view=CardMdPage/>
<Route path=path!("/checkbox") view=CheckboxMdPage/>
<Route path=path!("/color-picker") view=ColorPickerMdPage/>
<Route path=path!("/combobox") view=ComboboxMdPage/>
<Route path=path!("/config-provider") view=ConfigProviderMdPage/>
}.into_inner()}
{view!{
<Route path=path!("/date-picker") view=DatePickerMdPage/>
<Route path=path!("/dialog") view=DialogMdPage/>
<Route path=path!("/divider") view=DividerMdPage/>
<Route path=path!("/drawer") view=DrawerMdPage/>
<Route path=path!("/field") view=FieldMdPage/>
<Route path=path!("/menu") view=MenuMdPage/>
<Route path=path!("/grid") view=GridMdPage/>
<Route path=path!("/icon") view=IconMdPage/>
<Route path=path!("/image") view=ImageMdPage/>
<Route path=path!("/input") view=InputMdPage/>
<Route path=path!("/layout") view=LayoutMdPage/>
<Route path=path!("/link") view=LinkMdPage/>
<Route path=path!("/loading-bar") view=LoadingBarMdPage/>
<Route path=path!("/message-bar") view=MessageBarMdPage/>
<Route path=path!("/nav") view=NavMdPage/>
}.into_inner()}
{view!{
<Route path=path!("/pagination") view=PaginationMdPage/>
<Route path=path!("/popover") view=PopoverMdPage/>
<Route path=path!("/progress-bar") view=ProgressBarMdPage/>
<Route path=path!("/radio") view=RadioMdPage/>
<Route path=path!("/scrollbar") view=ScrollbarMdPage/>
<Route path=path!("/select") view=SelectMdPage/>
<Route path=path!("/skeleton") view=SkeletonMdPage/>
<Route path=path!("/slider") view=SliderMdPage/>
<Route path=path!("/space") view=SpaceMdPage/>
<Route path=path!("/spin-button") view=SpinButtonMdPage/>
<Route path=path!("/spinner") view=SpinnerMdPage/>
<Route path=path!("/switch") view=SwitchMdPage/>
<Route path=path!("/tab-list") view=TabListMdPage/>
<Route path=path!("/table") view=TableMdPage/>
}.into_inner()}
{view!{
<Route path=path!("/tag") view=TagMdPage/>
<Route path=path!("/tag-group") view=TagGroupMdPage/>
<Route path=path!("/tag-picker") view=TagPickerMdPage/>
<Route path=path!("/text") view=TextMdPage/>
<Route path=path!("/textarea") view=TextareaMdPage/>
<Route path=path!("/time-picker") view=TimePickerMdPage/>
<Route path=path!("/toast") view=ToastMdPage />
<Route path=path!("/tooltip") view=TooltipMdPage />
<Route path=path!("/upload") view=UploadMdPage/>
}.into_inner()}
</ParentRoute>
</Routes>
</Router>

View file

@ -272,11 +272,6 @@ pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
value: "/components/field",
label: "Field",
},
NavItemOption {
group: None,
value: "/components/flex",
label: "Flex",
},
NavItemOption {
group: None,
value: "/components/grid",

View file

@ -42,7 +42,6 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"DividerMdPage" => "../../thaw/src/divider/docs/mod.md",
"DrawerMdPage" => "../../thaw/src/drawer/docs/mod.md",
"FieldMdPage" => "../../thaw/src/field/docs/mod.md",
"FlexMdPage" => "../../thaw/src/flex/docs/mod.md",
"GridMdPage" => "../../thaw/src/grid/docs/mod.md",
"IconMdPage" => "../../thaw/src/icon/docs/mod.md",
"ImageMdPage" => "../../thaw/src/image/docs/mod.md",

View file

@ -9,10 +9,10 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7", optional = true }
console_error_panic_hook = "0.1"
leptos = { version = "0.7.0-beta5", features = ["experimental-islands"] }
leptos_axum = { version = "0.7.0-beta5", optional = true }
leptos_meta = { version = "0.7.0-beta5" }
leptos_router = { version = "0.7.0-beta5" }
leptos = { version = "0.7.0-beta4", features = ["experimental-islands"] }
leptos_axum = { version = "0.7.0-beta4", optional = true }
leptos_meta = { version = "0.7.0-beta4" }
leptos_router = { version = "0.7.0-beta4" }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }

View file

@ -9,10 +9,10 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7.5", optional = true }
console_error_panic_hook = "0.1"
leptos = { version = "0.7.0-beta5" }
leptos_axum = { version = "0.7.0-beta5", optional = true }
leptos_meta = { version = "0.7.0-beta5" }
leptos_router = { version = "0.7.0-beta5" }
leptos = { version = "0.7.0-beta4" }
leptos_axum = { version = "0.7.0-beta4", optional = true }
leptos_meta = { version = "0.7.0-beta4" }
leptos_router = { version = "0.7.0-beta4" }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.5.0", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }

View file

@ -2,7 +2,7 @@ use crate::{ConfigInjection, Icon};
use leptos::{either::Either, ev, html, prelude::*};
use thaw_components::{CSSTransition, Teleport};
use thaw_utils::{
add_event_listener, class_list, get_scroll_parent_element, mount_style, BoxCallback,
add_event_listener, class_list, get_scroll_parent, mount_style, BoxCallback,
EventListenerHandle,
};
@ -43,7 +43,7 @@ pub fn BackTop(
};
request_animation_frame(move || {
let scroll_el = get_scroll_parent_element(&placeholder_el)
let scroll_el = get_scroll_parent(&placeholder_el)
.unwrap_or_else(|| document().document_element().unwrap());
{

View file

@ -55,7 +55,7 @@ pub fn Combobox(
let Some(clear_icon_el) = clear_icon_ref.get() else {
return;
};
let handler = add_event_listener(clear_icon_el, ev::click, move |e| {
let handler = add_event_listener(clear_icon_el.into(), ev::click, move |e| {
if disabled.get_untracked() {
return;
}

View file

@ -1,110 +0,0 @@
# Flex
```rust demo
view! {
<Flex>
<Button>"1"</Button>
<Button>"2"</Button>
<Button>"3"</Button>
</Flex>
<Flex>
<For
each=move || 0..4
key=move |i| i.clone()
let:i
>
<Button>{i}</Button>
</For>
</Flex>
}
```
### Vertical
```rust demo
view! {
<Flex vertical=true>
<Button>"1"</Button>
<Button>"2"</Button>
<Button>"3"</Button>
</Flex>
}
```
### Gap
```rust demo
view! {
<Flex gap=FlexGap::Large>
<Button>"1"</Button>
<Button>"2"</Button>
<Button>"3"</Button>
</Flex>
<Flex gap=FlexGap::WH(36, 36)>
<Button>"1"</Button>
<Button>"2"</Button>
<Button>"3"</Button>
</Flex>
}
```
### Align
```rust demo
view! {
<Flex vertical=true>
<Flex align=FlexAlign::Start>
<Button attr:style="height: 60px">"Start"</Button>
<Button attr:style="height: 40px">"Start"</Button>
<Button>"Start"</Button>
</Flex>
<Flex align=FlexAlign::Center>
<Button attr:style="height: 60px">"Center"</Button>
<Button attr:style="height: 40px">"Center"</Button>
<Button>"Center"</Button>
</Flex>
<Flex align=FlexAlign::End>
<Button attr:style="height: 60px">"End"</Button>
<Button attr:style="height: 40px">"End"</Button>
<Button>"End"</Button>
</Flex>
</Flex>
}
```
### Justify
```rust demo
view! {
<Flex vertical=true>
<Flex justify=FlexJustify::SpaceAround>
<Button>"SpaceAround"</Button>
<Button>"SpaceAround"</Button>
<Button>"SpaceAround"</Button>
</Flex>
<Flex justify=FlexJustify::Center>
<Button>"Center"</Button>
<Button>"Center"</Button>
<Button>"Center"</Button>
</Flex>
<Flex justify=FlexJustify::End>
<Button>"End"</Button>
<Button>"End"</Button>
<Button>"End"</Button>
</Flex>
</Flex>
}
```
### Flex Props
| Name | Type | Default | Description |
| -------- | ------------------------ | -------------------- | -------------------------------------- |
| class | `MaybeProp<String>` | `Default::default()` | |
| style | `MaybeProp<String>` | `Default::default()` | |
| inline | `MaybeSignal<bool>` | `false` | Whether it's display is `inline-flex`. |
| vertical | `bool` | `false` | Whether to lay out vertically. |
| gap | `FlexGap` | `FlexGap::Medium` | Flex's gap. |
| align | `MaybeProp<FlexAlign>` | `None` | Vertical arrangement. |
| justify | `MaybeProp<FlexJustify>` | `None` | Horizontal arrangement. |
| children | `Children` | | |

View file

@ -1,130 +0,0 @@
use leptos::prelude::*;
use thaw_utils::class_list;
#[component]
pub fn Flex(
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] style: MaybeProp<String>,
/// Flex's gap.
#[prop(optional)]
gap: FlexGap,
/// Whether to lay out vertically.
#[prop(optional)]
vertical: bool,
/// Whether it's display is `inline-flex`.
#[prop(optional, into)]
inline: MaybeSignal<bool>,
/// Vertical arrangement.
#[prop(optional, into)]
align: MaybeProp<FlexAlign>,
/// Horizontal arrangement.
#[prop(optional, into)]
justify: MaybeProp<FlexJustify>,
children: Children,
) -> impl IntoView {
let style = Memo::new(move |_| {
let mut s = String::new();
let display = if inline.get() {
"display: inline-flex;"
} else {
"display: flex;"
};
s.push_str(display);
let direction = if vertical {
"flex-direction: column;"
} else {
"flex-direction: row;"
};
let gap = match gap {
FlexGap::Small => "gap: 4px 8px;",
FlexGap::Medium => "gap: 8px 12px;",
FlexGap::Large => "gap: 12px 16px;",
FlexGap::Size(size) => &format!("gap: {size}px {size}px;"),
FlexGap::WH(width, height) => &format!("gap: {width}px {height}px;"),
};
s.push_str(direction);
s.push_str(gap);
if let Some(align) = align.get() {
s.push_str(&format!("align-items: {};", align.as_str()));
}
if let Some(justify) = justify.get() {
s.push_str(&format!("justify-content: {};", justify.as_str()));
}
style.with(|style| {
if let Some(style) = style.as_ref() {
s.push_str(style);
}
});
s
});
view! {
<div class=class_list!["thaw-flex", class] style=move || style.get()>
{children()}
</div>
}
}
#[derive(Default)]
pub enum FlexGap {
Small,
#[default]
Medium,
Large,
Size(u16),
/// width and height
WH(u16, u16),
}
#[derive(Clone)]
pub enum FlexAlign {
FlexStart,
FlexEnd,
Start,
End,
Center,
Baseline,
Stretch,
}
impl FlexAlign {
fn as_str(&self) -> &'static str {
match self {
Self::FlexStart => "flex-start",
Self::FlexEnd => "flex-end",
Self::Start => "start",
Self::End => "end",
Self::Center => "center",
Self::Baseline => "baseline",
Self::Stretch => "stretch",
}
}
}
#[derive(Clone)]
pub enum FlexJustify {
FlexStart,
FlexEnd,
Start,
End,
Center,
SpaceAround,
SpaceBetween,
SpaceEvenly,
}
impl FlexJustify {
fn as_str(&self) -> &'static str {
match self {
Self::FlexStart => "flex-start",
Self::FlexEnd => "flex-end",
Self::Start => "start",
Self::End => "end",
Self::Center => "center",
Self::SpaceAround => "space-around",
Self::SpaceBetween => "space-between",
Self::SpaceEvenly => "space-evenly",
}
}
}

View file

@ -1,3 +0,0 @@
mod flex;
pub use flex::*;

View file

@ -19,7 +19,6 @@ mod dialog;
mod divider;
mod drawer;
mod field;
mod flex;
mod grid;
mod icon;
mod image;
@ -74,7 +73,6 @@ pub use dialog::*;
pub use divider::*;
pub use drawer::*;
pub use field::*;
pub use flex::*;
pub use grid::*;
pub use icon::*;
pub use image::*;

View file

@ -77,4 +77,3 @@ view! {
| disabled | `MaybeSignal<bool>` | `false` | Whether the link is disabled. |
| disabled_focusable | `MaybeSignal<bool>` | `false` | When set, allows the link to be focusable even when it has been disabled. |
| children | `Children` | | |
| target | `MaybeSignal<String>` | | Specifies where to open the linked document. |

View file

@ -16,9 +16,6 @@ pub fn Link(
#[prop(optional, into)]
disabled_focusable: MaybeSignal<bool>,
children: Children,
/// Specifies where to open the linked document.
#[prop(optional, into)]
target: MaybeSignal<String>,
) -> impl IntoView {
mount_style("link", include_str!("./link.css"));
@ -49,7 +46,6 @@ pub fn Link(
href=href
tabindex=tabindex
aria-disabled=move || link_disabled.get().then_some("true")
target=target
>
{children()}
</a>

View file

@ -80,7 +80,7 @@ pub fn Menu(
let Some(target_el) = target_ref.get() else {
return;
};
let handler = add_event_listener(target_el, ev::click, move |event| {
let handler = add_event_listener(target_el.into(), ev::click, move |event| {
if trigger_type != MenuTriggerType::Click {
return;
}

View file

@ -99,7 +99,7 @@ pub fn Popover(
let Some(target_el) = target_ref.get() else {
return;
};
let handler = add_event_listener(target_el, ev::click, move |event| {
let handler = add_event_listener(target_el.into(), ev::click, move |event| {
if trigger_type != PopoverTriggerType::Click {
return;
}

View file

@ -23,22 +23,10 @@ view! {
}
```
### Custom label
```rust demo
view! {
<Spinner label="Label"/>
<Spinner>
<b style="color: blue">"Label"</b>
</Spinner>
}
```
### Spinner Props
| Name | Type | Default | Description |
| -------- | -------------------------- | --------------------- | ---------------------------------- |
| class | `MaybeProp<String>` | `Default::default()` | |
| label | `MaybeProp<String>` | `Default::default()` | An optional label for the Spinner. |
| size | `MaybeSignal<SpinnerSize>` | `SpinnerSize::Medium` | The size of the spinner. |
| children | `Option<Children>` | `None` | |
| Name | Type | Default | Description |
| ----- | -------------------------- | --------------------- | ---------------------------------- |
| class | `MaybeProp<String>` | `Default::default()` | |
| label | `MaybeProp<String>` | `Default::default()` | An optional label for the Spinner. |
| size | `MaybeSignal<SpinnerSize>` | `SpinnerSize::Medium` | The size of the spinner. |

View file

@ -38,16 +38,14 @@ pub fn Spinner(
/// The size of the spinner.
#[prop(optional, into)]
size: MaybeSignal<SpinnerSize>,
#[prop(optional)] children: Option<Children>,
) -> impl IntoView {
mount_style("spinner", include_str!("./spinner.css"));
let id = StoredValue::new(uuid::Uuid::new_v4().to_string());
let spinner_label = label.clone();
let children_flag = children.is_some();
let labelledby = move || {
spinner_label.with(|label| {
if label.is_some() || children_flag {
if label.is_some() {
Some(id.get_value())
} else {
None
@ -68,29 +66,17 @@ pub fn Spinner(
<span class="thaw-spinner__spinner">
<span class="thaw-spinner__spinner-tail"></span>
</span>
{if let Some(children) = children {
view! {
<label class="thaw-spinner__label" id=id.get_value()>
{children()}
</label>
}
.into_any()
} else {
{
move || {
if let Some(label) = label.get() {
view! {
<label class="thaw-spinner__label" id=id.get_value()>
{label}
</label>
}
.into()
} else {
None
}
{move || {
if let Some(label) = label.get() {
view! {
<label class="thaw-spinner__label" id=id.get_value()>
{label}
</label>
}
.into()
} else {
None
}
.into_any()
}}
</div>
}

View file

@ -34,7 +34,7 @@ pub fn Upload(
let Some(trigger_el) = trigger_ref.get() else {
return;
};
let handle = add_event_listener(trigger_el, ev::click, move |_| {
let handle = add_event_listener(trigger_el.into(), ev::click, move |_| {
if let Some(input_ref) = input_ref.get_untracked() {
input_ref.click();
}

View file

@ -1,4 +1,4 @@
.thaw-binder-follower {
.thaw-binder-follower-container {
position: absolute;
left: 0;
right: 0;
@ -9,5 +9,5 @@
.thaw-binder-follower-content {
position: absolute;
z-index: 1000000;
z-index: 2000;
}

View file

@ -1,7 +1,7 @@
use leptos::prelude::window;
use web_sys::DomRect;
#[derive(Clone, PartialEq)]
#[derive(Clone)]
pub enum FollowerPlacement {
Top,
Bottom,
@ -66,151 +66,309 @@ pub fn get_follower_placement_offset(
placement: FollowerPlacement,
target_rect: DomRect,
follower_rect: DomRect,
content_rect: DomRect,
) -> Option<FollowerPlacementOffset> {
let (left, placement, top, transform) = match placement {
FollowerPlacement::Top | FollowerPlacement::TopStart | FollowerPlacement::TopEnd => {
let Some(window_inner_height) = window_inner_height() else {
return None;
};
let content_height = content_rect.height();
let target_top = target_rect.top();
let target_bottom = target_rect.bottom();
let top = target_top - content_height;
let (top, new_placement) =
if top < 0.0 && target_bottom + content_height <= window_inner_height {
(target_bottom, FollowerPlacement::Bottom)
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, 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 placement == FollowerPlacement::Top {
let left = target_rect.left() + target_rect.width() / 2.0;
let transform = String::from("translateX(-50%)");
(left, new_placement, top, transform)
} else if placement == FollowerPlacement::TopStart {
let left = target_rect.left();
let transform = String::new();
(left, new_placement, top, transform)
} else if placement == FollowerPlacement::TopEnd {
let left = target_rect.right();
let transform = String::from("translateX(-100%)");
(left, new_placement, top, transform)
} else {
unreachable!()
}
}
FollowerPlacement::Bottom
| FollowerPlacement::BottomStart
| FollowerPlacement::BottomEnd => {
let Some(window_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)
}
};
let content_height = content_rect.height();
let target_top = target_rect.top();
let target_bottom = target_rect.bottom();
let top = target_bottom;
let (top, new_placement) = if top + content_height > window_inner_height
&& target_top - content_height >= 0.0
{
(target_top - content_height, FollowerPlacement::Top)
} else {
(top, FollowerPlacement::Bottom)
};
if placement == FollowerPlacement::Bottom {
let left = target_rect.left() + target_rect.width() / 2.0;
let transform = String::from("translateX(-50%)");
(left, new_placement, top, transform)
} else if placement == FollowerPlacement::BottomStart {
let left = target_rect.left();
let transform = String::new();
(left, new_placement, top, transform)
} else if placement == FollowerPlacement::BottomEnd {
let left = target_rect.right();
let transform = String::from("translateX(-100%)");
(left, new_placement, top, transform)
} else {
unreachable!()
}
}
FollowerPlacement::Left | FollowerPlacement::LeftStart | FollowerPlacement::LeftEnd => {
let Some(window_inner_width) = window_inner_width() else {
return None;
};
let content_width = content_rect.width();
let target_left = target_rect.left();
let target_right = target_rect.right();
let left = target_left - content_width;
leptos::logging::log!(
"{}-{} {} {}",
Some(FollowerPlacementOffset {
top,
left,
target_right,
content_width,
window_inner_width
);
let (left, new_placement) =
if left < 0.0 && target_right + content_width <= window_inner_width {
(target_right, FollowerPlacement::Right)
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 placement == FollowerPlacement::Left {
let top = target_rect.top() + target_rect.height() / 2.0;
let transform = String::from("translateY(-50%)");
(left, new_placement, top, transform)
} else if placement == FollowerPlacement::LeftStart {
let top = target_rect.top();
let transform = String::new();
(left, new_placement, top, transform)
} else if placement == FollowerPlacement::LeftEnd {
let top = target_rect.bottom();
let transform = String::from("translateY(-100%)");
(left, new_placement, top, transform)
} else {
unreachable!()
}
}
FollowerPlacement::Right | FollowerPlacement::RightStart | FollowerPlacement::RightEnd => {
let Some(window_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)
}
};
let content_width = content_rect.width();
let target_left = target_rect.left();
let target_right = target_rect.right();
let left = target_right;
let (left, new_placement) = if left + content_width > window_inner_width
&& target_left - content_width >= 0.0
{
(target_left - content_width, FollowerPlacement::Left)
} else {
(left, FollowerPlacement::Right)
};
if placement == FollowerPlacement::Right {
let top = target_rect.top() + target_rect.height() / 2.0;
let transform = String::from("translateY(-50%)");
(left, new_placement, top, transform)
} else if placement == FollowerPlacement::RightStart {
let top = target_rect.top();
let transform = String::new();
(left, new_placement, top, transform)
} else if placement == FollowerPlacement::RightEnd {
let top = target_rect.bottom();
let transform = String::from("translateY(-100%)");
(left, new_placement, top, transform)
} else {
unreachable!()
}
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;
Some(FollowerPlacementOffset {
top: top - follower_rect.top(),
left: left - follower_rect.left(),
placement,
transform,
})
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> {

View file

@ -14,7 +14,7 @@ use leptos::{
leptos_dom::helpers::WindowListenerHandle,
prelude::*,
};
use thaw_utils::{add_event_listener, get_scroll_parent_node, mount_style, EventListenerHandle};
use thaw_utils::{add_event_listener, get_scroll_parent, mount_style, EventListenerHandle};
#[slot]
pub struct Follower {
@ -82,21 +82,16 @@ where
let scrollable_element_handle_vec = StoredValue::<Vec<EventListenerHandle>>::new(vec![]);
let resize_handle = StoredValue::new(None::<WindowListenerHandle>);
let follower_ref = NodeRef::<html::Div>::new();
let content_ref = NodeRef::<html::Div>::new();
let content_style = RwSignal::new(String::new());
let placement_str = RwSignal::new(follower_placement.as_str());
let sync_position = move || {
let Some(follower_el) = follower_ref.get_untracked() else {
return;
};
let Some(content_ref) = content_ref.get_untracked() else {
return;
};
let Some(target_ref) = target_ref.get_untracked() else {
return;
};
let follower_rect = follower_el.get_bounding_client_rect();
let target_rect = target_ref.get_bounding_client_rect();
let content_rect = content_ref.get_bounding_client_rect();
let mut style = String::new();
@ -113,12 +108,8 @@ where
left,
transform,
placement,
}) = get_follower_placement_offset(
follower_placement,
target_rect,
follower_rect,
content_rect,
) {
}) = get_follower_placement_offset(follower_placement, target_rect, content_rect)
{
placement_str.set(placement.as_str());
style.push_str(&format!(
"transform-origin: {};",
@ -141,12 +132,12 @@ where
};
let mut handle_vec = vec![];
let mut cursor = get_scroll_parent_node(&el);
let mut cursor = get_scroll_parent(&el);
loop {
if let Some(node) = cursor.take() {
cursor = get_scroll_parent_node(&node);
if let Some(el) = cursor.take() {
cursor = get_scroll_parent(&el);
let handle = add_event_listener(node, ev::scroll, move |_| {
let handle = add_event_listener(el, ev::scroll, move |_| {
sync_position();
});
handle_vec.push(handle);
@ -206,16 +197,18 @@ where
view! {
{children()}
<Teleport immediate=follower_show>
<div class="thaw-binder-follower" node_ref=follower_ref>
<div
class="thaw-binder-follower-content"
data-thaw-placement=move || placement_str.get()
node_ref=content_ref
style=move || content_style.get()
>
<Provider value=follower_injection>{follower_children()}</Provider>
<Provider value=follower_injection>
<div class="thaw-binder-follower-container">
<div
class="thaw-binder-follower-content"
data-thaw-placement=move || placement_str.get()
node_ref=content_ref
style=move || content_style.get()
>
{follower_children()}
</div>
</div>
</div>
</Provider>
</Teleport>
}
}

View file

@ -92,13 +92,13 @@ where
}
};
let handle = match types {
AnimationTypes::Transition => {
add_event_listener(el.deref().clone(), ev::transitionend, move |_| {
event_listener()
})
}
AnimationTypes::Transition => add_event_listener(
el.deref().clone().into(),
ev::transitionend,
move |_| event_listener(),
),
AnimationTypes::Animation => {
add_event_listener(el.deref().clone(), ev::animationend, move |_| {
add_event_listener(el.deref().clone().into(), ev::animationend, move |_| {
event_listener()
})
}

View file

@ -1,49 +1,39 @@
use leptos::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, Node};
use web_sys::Element;
pub fn get_scroll_parent_node(node: &Node) -> Option<Node> {
let parent_node = node.parent_node()?;
pub fn get_scroll_parent(element: &Element) -> Option<Element> {
let Some(parent_element) = get_parent_element(element) else {
return None;
};
let node_type = parent_node.node_type();
if node_type == Node::ELEMENT_NODE {
let el = parent_node.clone().dyn_into::<Element>().unwrap();
if let Some((overflow, overflow_x, overflow_y)) = get_overflow(&el) {
if parent_element.node_type() == 9 {
return Some(parent_element);
}
if parent_element.node_type() == 1 {
if let Some((overflow, overflow_x, overflow_y)) = get_overflow(&parent_element) {
let overflow = format!("{overflow}{overflow_x}{overflow_y}");
if overflow.contains("auto") {
return Some(parent_node);
return Some(parent_element);
}
if overflow.contains("scroll") {
return Some(parent_node);
return Some(parent_element);
}
if overflow.contains("overlay") {
return Some(parent_node);
return Some(parent_element);
}
}
} else if node_type == Node::DOCUMENT_NODE {
return Some(document().into());
}
get_scroll_parent_node(&parent_node)
get_scroll_parent(&parent_element)
}
pub fn get_scroll_parent_element(element: &Element) -> Option<Element> {
let parent_element = element.parent_element()?;
if let Some((overflow, overflow_x, overflow_y)) = get_overflow(&parent_element) {
let overflow = format!("{overflow}{overflow_x}{overflow_y}");
if overflow.contains("auto") {
return Some(parent_element);
}
if overflow.contains("scroll") {
return Some(parent_element);
}
if overflow.contains("overlay") {
return Some(parent_element);
}
fn get_parent_element(element: &Element) -> Option<Element> {
if element.node_type() == 9 {
None
} else {
element.parent_element()
}
get_scroll_parent_element(&parent_element)
}
fn get_overflow(parent_element: &Element) -> Option<(String, String, String)> {

View file

@ -2,6 +2,6 @@ mod get_scroll_parent;
mod mount_style;
mod scroll_into_view;
pub use get_scroll_parent::{get_scroll_parent_element, get_scroll_parent_node};
pub use get_scroll_parent::get_scroll_parent;
pub use mount_style::{mount_dynamic_style, mount_style};
pub use scroll_into_view::scroll_into_view;

View file

@ -3,8 +3,8 @@ use web_sys::HtmlElement;
pub fn scroll_into_view(el: &HtmlElement) {
cfg_if! { if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
use super::get_scroll_parent_element;
if let Some(parent) = get_scroll_parent_element(el) {
use super::get_scroll_parent;
if let Some(parent) = get_scroll_parent(el) {
let parent_rect = parent.get_bounding_client_rect();
let el_rect = el.get_bounding_client_rect();
if el_rect.y() < parent_rect.y() {

View file

@ -1,9 +1,9 @@
use ::wasm_bindgen::{prelude::Closure, JsCast};
use leptos::ev;
use web_sys::EventTarget;
use web_sys::{Element, EventTarget};
pub fn add_event_listener<E>(
target: impl Into<EventTarget>,
target: Element,
event: E,
cb: impl Fn(E::EventType) + 'static,
) -> EventListenerHandle
@ -31,12 +31,12 @@ impl EventListenerHandle {
}
fn add_event_listener_untyped(
target: impl Into<EventTarget>,
target: Element,
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
) -> EventListenerHandle {
fn wel(
target: EventTarget,
target: Element,
cb: Box<dyn FnMut(web_sys::Event)>,
event_name: &str,
) -> EventListenerHandle {
@ -54,11 +54,11 @@ fn add_event_listener_untyped(
})
}
wel(target.into(), Box::new(cb), event_name)
wel(target, Box::new(cb), event_name)
}
pub fn add_event_listener_with_bool<E: ev::EventDescriptor + 'static>(
target: impl Into<EventTarget>,
target: impl IntoEventTarget,
event: E,
cb: impl Fn(E::EventType) + 'static,
use_capture: bool,
@ -67,7 +67,7 @@ where
E::EventType: JsCast,
{
add_event_listener_untyped_with_bool(
target,
target.into_event_target(),
&event.name(),
move |e| cb(e.unchecked_into::<E::EventType>()),
use_capture,
@ -75,7 +75,7 @@ where
}
fn add_event_listener_untyped_with_bool(
target: impl Into<EventTarget>,
target: EventTarget,
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
use_capture: bool,
@ -107,30 +107,24 @@ fn add_event_listener_untyped_with_bool(
})
}
wel(target.into(), Box::new(cb), event_name, use_capture)
wel(target, Box::new(cb), event_name, use_capture)
}
// pub trait IntoEventTarget {
// fn into_event_target(self) -> EventTarget;
// }
pub trait IntoEventTarget {
fn into_event_target(self) -> EventTarget;
}
// impl IntoEventTarget for EventTarget {
// fn into_event_target(self) -> EventTarget {
// self
// }
// }
impl IntoEventTarget for EventTarget {
fn into_event_target(self) -> EventTarget {
self
}
}
// impl IntoEventTarget for web_sys::Document {
// fn into_event_target(self) -> EventTarget {
// self.into()
// }
// }
// impl IntoEventTarget for Element {
// fn into_event_target(self) -> EventTarget {
// self.into()
// }
// }
impl IntoEventTarget for web_sys::Document {
fn into_event_target(self) -> EventTarget {
self.into()
}
}
// impl IntoEventTarget for HtmlElement<AnyElement> {
// fn into_event_target(self) -> EventTarget {

View file

@ -4,6 +4,6 @@ mod signal_watch;
mod stored_maybe_signal;
pub use component_ref::ComponentRef;
pub use model::{Model, OptionModel, OptionModelWithValue, VecModel, VecModelWithValue};
pub use model::{Model, OptionModel, VecModel, VecModelWithValue, OptionModelWithValue};
pub use signal_watch::SignalWatch;
pub use stored_maybe_signal::StoredMaybeSignal;

View file

@ -58,9 +58,7 @@ impl<T: Send + Sync> OptionModel<T> {
pub fn with_untracked<O>(&self, fun: impl FnOnce(OptionModelWithValue<T>) -> O) -> O {
match self {
Self::T(read, _, _) => read.with_untracked(|value| fun(OptionModelWithValue::T(value))),
Self::Option(read, _, _) => {
read.with_untracked(|value| fun(OptionModelWithValue::Option(value)))
}
Self::Option(read, _, _) => read.with_untracked(|value| fun(OptionModelWithValue::Option(value))),
}
}
}

View file

@ -67,7 +67,10 @@ impl<T: Send + Sync> VecModel<T> {
}
}
pub fn with<O>(&self, fun: impl FnOnce(VecModelWithValue<T>) -> O) -> O {
pub fn with<O>(
&self,
fun: impl FnOnce(VecModelWithValue<T>) -> O,
) -> O {
match self {
Self::T(read, _, _) => read.with(|value| fun(VecModelWithValue::T(value))),
Self::Option(read, _, _) => read.with(|value| fun(VecModelWithValue::Option(value))),