Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ fn TheRouter() -> impl IntoView {
<Route path=path!("/nav") view=NavMdPage />
<Route path=path!("/pagination") view=PaginationMdPage />
<Route path=path!("/persona") view=PersonaMdPage />
<Route path=path!("/popconfirm") view=PopconfirmMdPage />
<Route path=path!("/popover") view=PopoverMdPage />
<Route path=path!("/progress-bar") view=ProgressBarMdPage />
<Route path=path!("/radio") view=RadioMdPage />
Expand Down
5 changes: 5 additions & 0 deletions demo/src/pages/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,11 @@ pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
value: "/components/persona",
label: "Persona",
},
NavItemOption {
group: None,
value: "/components/popconfirm",
label: "Popconfirm",
},
NavItemOption {
group: None,
value: "/components/popover",
Expand Down
1 change: 1 addition & 0 deletions demo_markdown/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"NavMdPage" => "../../thaw/src/nav/docs/mod.md",
"PaginationMdPage" => "../../thaw/src/pagination/docs/mod.md",
"PersonaMdPage" => "../../thaw/src/persona/docs/mod.md",
"PopconfirmMdPage" => "../../thaw/src/popconfirm/docs/mod.md",
"PopoverMdPage" => "../../thaw/src/popover/docs/mod.md",
"ProgressBarMdPage" => "../../thaw/src/progress_bar/docs/mod.md",
"RadioMdPage" => "../../thaw/src/radio/docs/mod.md",
Expand Down
2 changes: 2 additions & 0 deletions thaw/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod message_bar;
mod nav;
mod pagination;
mod persona;
mod popconfirm;
mod popover;
mod progress_bar;
mod radio;
Expand Down Expand Up @@ -100,6 +101,7 @@ pub use message_bar::*;
pub use nav::*;
pub use pagination::*;
pub use persona::*;
pub use popconfirm::*;
pub use popover::*;
pub use progress_bar::*;
pub use radio::*;
Expand Down
101 changes: 101 additions & 0 deletions thaw/src/popconfirm/docs/mod.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Popconfirm

A popover that asks for confirmation before executing an action.

### Basic

```rust demo
let toaster = ToasterInjection::expect_context();

let on_confirm = Callback::new(move |_| {
toaster.dispatch_toast(move || view! {
<Toast>
<ToastTitle>"Confirmed"</ToastTitle>
</Toast>
}, ToastOptions::default().with_intent(ToastIntent::Success).with_position(ToastPosition::Top));
});

let on_cancel = Callback::new(move |_| {
toaster.dispatch_toast(move || view! {
<Toast>
<ToastTitle>"Cancelled"</ToastTitle>
</Toast>
}, ToastOptions::default().with_intent(ToastIntent::Warning).with_position(ToastPosition::Top));
});

view! {
<Popconfirm
title="Delete the task"
description="Are you sure to delete this task?"
on_confirm
on_cancel
>
<Button>"Delete"</Button>
</Popconfirm>
}
```

### Custom Button Text

```rust demo
let toaster = ToasterInjection::expect_context();

let on_confirm = Callback::new(move |_| {
toaster.dispatch_toast(move || view! {
<Toast>
<ToastTitle>"Item removed"</ToastTitle>
</Toast>
}, ToastOptions::default().with_intent(ToastIntent::Success).with_position(ToastPosition::Top));
});

view! {
<Popconfirm
title="Remove item"
description="This action cannot be undone."
on_confirm
ok_text="Confirm"
cancel_text="Cancel"
>
<Button>"Remove"</Button>
</Popconfirm>
}
```

### Position

```rust demo
let on_confirm = Callback::new(move |_| {});

view! {
<Space>
<Popconfirm title="Delete this?" on_confirm position=PopoverPosition::Top>
<Button>"Top"</Button>
</Popconfirm>
<Popconfirm title="Delete this?" on_confirm position=PopoverPosition::Bottom>
<Button>"Bottom"</Button>
</Popconfirm>
<Popconfirm title="Delete this?" on_confirm position=PopoverPosition::Right>
<Button>"Right"</Button>
</Popconfirm>
</Space>
}
```

### Popconfirm Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `MaybeProp<String>` | `Default::default()` | |
| title | `String` | `""` | Title shown in the popover. |
| description | `String` | `""` | Description shown below the title. |
| position | `PopoverPosition` | `PopoverPosition::Top` | Configures the position of the popover. |
| appearance | `MaybeProp<PopoverAppearance>` | `Default::default()` | A popover can appear styled with brand or inverted. |
| trigger_type | `PopoverTriggerType` | `PopoverTriggerType::Click` | Action that displays the popconfirm. |
| size | `Signal<PopoverSize>` | `PopoverSize::Medium` | Size of the popover. |
| on_confirm | `Callback<()>` | | Called when the user clicks the confirm button. |
| on_cancel | `Option<Callback<()>>` | `None` | Called when the user clicks the cancel button. |
| on_open | `Option<BoxCallback>` | `None` | Listen for popover open events. |
| on_close | `Option<BoxCallback>` | `None` | Listen for popover close events. |
| ok_text | `&'static str` | `"Yes"` | Text for the confirm button. |
| cancel_text | `&'static str` | `"No"` | Text for the cancel button. |
| children | `T: AddAnyAttr + IntoView + Send + 'static` | | The trigger element. |
102 changes: 102 additions & 0 deletions thaw/src/popconfirm/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use crate::{
Button, ButtonAppearance, ButtonSize, Popover, PopoverAppearance, PopoverPosition, PopoverSize,
PopoverTrigger, PopoverTriggerType,
};
use leptos::prelude::*;
use thaw_utils::{mount_style, BoxCallback, Model};

/// A popover that asks for confirmation before executing an action.
///
/// ```rust
/// <Popconfirm
/// title="Delete the task"
/// description="Are you sure to delete this task?"
/// on_confirm=Callback::new(move |_| delete())
/// >
/// <Button>"Delete"</Button>
/// </Popconfirm>
/// ```
#[component]
pub fn Popconfirm<T: AddAnyAttr + IntoView + Send + 'static>(
#[prop(optional, into)] class: MaybeProp<String>,
/// Title shown in the popover.
#[prop(optional, into)]
title: String,
/// Description shown below the title.
#[prop(optional, into)]
description: String,
/// Called when the user clicks the confirm button.
#[prop(into)]
on_confirm: Callback<()>,
/// Called when the user clicks the cancel button.
#[prop(optional, into)]
on_cancel: Option<Callback<()>>,
/// Configures the position of the popover.
#[prop(optional)]
position: PopoverPosition,
/// A popover can appear styled with brand or inverted.
#[prop(optional, into)]
appearance: MaybeProp<PopoverAppearance>,
/// Action that displays the popconfirm. Defaults to Click.
#[prop(default = PopoverTriggerType::Click)]
trigger_type: PopoverTriggerType,
#[prop(optional, into)] size: Signal<PopoverSize>,
#[prop(optional, into)] on_open: Option<BoxCallback>,
#[prop(optional, into)] on_close: Option<BoxCallback>,
/// Text for the confirm button.
#[prop(default = "Yes")]
ok_text: &'static str,
/// Text for the cancel button.
#[prop(default = "No")]
cancel_text: &'static str,
/// The trigger element (e.g. a Button).
children: TypedChildren<T>,
) -> impl IntoView {
mount_style("popconfirm", include_str!("./popconfirm.css"));

let is_open: Model<bool> = RwSignal::new(false).into();

let on_yes = move |_| {
is_open.set(false);
on_confirm.run(());
};

let on_no = move |_| {
is_open.set(false);
if let Some(cb) = &on_cancel {
cb.run(());
}
};

let on_open = on_open.unwrap_or_else(|| BoxCallback::new(|| {}));
let on_close = on_close.unwrap_or_else(|| BoxCallback::new(|| {}));

view! {
<Popover
class=class
appearance=appearance
trigger_type=trigger_type
position=position
size=size
open=is_open
on_open=on_open
on_close=on_close
popover_trigger=PopoverTrigger { children }
>
{(!title.is_empty()).then(|| view! {
<div class="thaw-popconfirm__title">{title}</div>
})}
{(!description.is_empty()).then(|| view! {
<div class="thaw-popconfirm__description">{description}</div>
})}
<div class="thaw-popconfirm__actions">
<Button size=ButtonSize::Small appearance=ButtonAppearance::Secondary on_click=on_no>
{cancel_text}
</Button>
<Button size=ButtonSize::Small appearance=ButtonAppearance::Primary on_click=on_yes>
{ok_text}
</Button>
</div>
</Popover>
}
}
20 changes: 20 additions & 0 deletions thaw/src/popconfirm/popconfirm.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.thaw-popconfirm__title {
font-size: var(--fontSizeBase300);
font-weight: var(--fontWeightSemibold);
line-height: var(--lineHeightBase300);
}

.thaw-popconfirm__description {
font-size: var(--fontSizeBase200);
font-weight: var(--fontWeightRegular);
line-height: var(--lineHeightBase200);
color: var(--colorNeutralForeground2);
margin-top: 4px;
}

.thaw-popconfirm__actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 8px;
}
1 change: 1 addition & 0 deletions thaw/src/popover/docs/mod.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ view! {
| appearance | `MaybeProp<PopoverAppearance>` | `Default::default()` | A popover can appear styled with brand or inverted. When not specified, the default style is used. |
| trigger_type | `PopoverTriggerType` | `PopoverTriggerType::Hover` | Action that displays the popover. |
| popover_trigger | slot `PopoverTrigger` | | The element or component that triggers popover. |
| open | `Option<Model<bool>>` | `None` | Controls the open state of the popover programmatically. |
| on_open | `Option<BoxCallback>` | `None` | Listen for popover open events. |
| on_close | `Option<BoxCallback>` | `None` | Listen for popover close events. |
| children | `Children` | | |
Expand Down
32 changes: 24 additions & 8 deletions thaw/src/popover/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use leptos::{
};
use std::time::Duration;
use thaw_components::{Follower, FollowerArrow};
use thaw_utils::{class_list, mount_style, on_click_outside, BoxCallback};
use thaw_utils::{class_list, mount_style, on_click_outside, BoxCallback, Model};

#[component]
pub fn Popover<T>(
Expand All @@ -30,6 +30,10 @@ pub fn Popover<T>(
#[prop(optional, into)]
appearance: MaybeProp<PopoverAppearance>,
#[prop(optional, into)] size: Signal<PopoverSize>,
/// Controls the open state of the popover. When provided, the popover
/// becomes controlled and can be opened/closed programmatically.
#[prop(optional, into)]
open: Option<Model<bool>>,
#[prop(optional, into)] on_open: Option<BoxCallback>,
#[prop(optional, into)] on_close: Option<BoxCallback>,
children: Children,
Expand All @@ -40,7 +44,21 @@ where
mount_style("popover", include_str!("./popover.css"));

let popover_ref = NodeRef::<html::Div>::new();
let is_show_popover = RwSignal::new(false);
let is_show_popover = RwSignal::new(open.map(|m| m.get_untracked()).unwrap_or(false));

// Sync: model → internal signal
if let Some(model) = open {
Effect::new(move || {
is_show_popover.set(model.get());
});
}

let set_show = move |val: bool| {
is_show_popover.set(val);
if let Some(model) = open {
model.set(val);
}
};
let show_popover_handle = StoredValue::new(None::<TimeoutHandle>);

if on_open.is_some() || on_close.is_some() {
Expand Down Expand Up @@ -72,7 +90,7 @@ where
handle.clear();
}
});
is_show_popover.set(true);
set_show(true);
};
let on_mouse_leave = move |_| {
if trigger_type != PopoverTriggerType::Hover {
Expand All @@ -84,7 +102,7 @@ where
}
*handle = set_timeout_with_handle(
move || {
is_show_popover.set(false);
set_show(false);
},
Duration::from_millis(100),
)
Expand Down Expand Up @@ -118,15 +136,13 @@ where
};
Some(vec![popover_el.into(), trigger_el])
},
move || is_show_popover.set(false),
move || set_show(false),
);
Either::Left(
trigger_children
.add_any_attr(node_ref(trigger_ref))
.add_any_attr(on(ev::click, move |_| {
is_show_popover.update(|show| {
*show = !*show;
});
set_show(!is_show_popover.get_untracked());
})),
)
}
Expand Down
Loading