Implement Dialog and Alert (#172)

This commit is contained in:
Cecile Tonglet 2023-05-17 14:00:02 +02:00 committed by GitHub
parent 993b8cc8bd
commit ad55431263
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1051 additions and 114 deletions

View file

@ -55,7 +55,7 @@ Roadmap
- [ ] [Breadcrumbs](https://blueprintjs.com/docs/#core/components/breadcrumbs) - [ ] [Breadcrumbs](https://blueprintjs.com/docs/#core/components/breadcrumbs)
- [x] [Button](https://blueprintjs.com/docs/#core/components/button) - [x] [Button](https://blueprintjs.com/docs/#core/components/button)
- [ ] Complete Button API - [ ] Complete Button API
- [ ] AnchorButton - [x] AnchorButton
- [x] [ButtonGroup](https://blueprintjs.com/docs/#core/components/button-group) - [x] [ButtonGroup](https://blueprintjs.com/docs/#core/components/button-group)
- depends on: Button - depends on: Button
- [x] [Callout](https://blueprintjs.com/docs/#core/components/callout) - [x] [Callout](https://blueprintjs.com/docs/#core/components/callout)
@ -103,11 +103,11 @@ Roadmap
- [x] [Overlay](https://blueprintjs.com/docs/#core/components/overlay) - [x] [Overlay](https://blueprintjs.com/docs/#core/components/overlay)
- depends on: Portal - depends on: Portal
- [x] [Portal](https://blueprintjs.com/docs/#core/components/portal) - [x] [Portal](https://blueprintjs.com/docs/#core/components/portal)
- [ ] [Alert](https://blueprintjs.com/docs/#core/components/alert) - [x] [Alert](https://blueprintjs.com/docs/#core/components/alert)
- depends on: Button, Dialog - depends on: Button, Dialog
- [ ] [Context menu](https://blueprintjs.com/docs/#core/components/context-menu) - [ ] [Context menu](https://blueprintjs.com/docs/#core/components/context-menu)
- depends on: Popover - depends on: Popover
- [ ] [Dialog](https://blueprintjs.com/docs/#core/components/dialog) - [x] [Dialog](https://blueprintjs.com/docs/#core/components/dialog)
- depends on: Icon, Overlay, Button - depends on: Icon, Overlay, Button
- [ ] [Drawer](https://blueprintjs.com/docs/#core/components/drawer) - [ ] [Drawer](https://blueprintjs.com/docs/#core/components/drawer)
- depends on: Icon, Overlay, Button - depends on: Icon, Overlay, Button

131
src/alert.rs Normal file
View file

@ -0,0 +1,131 @@
use crate::{Button, Dialog, Icon, Intent};
use yew::prelude::*;
#[derive(Debug)]
pub struct Alert {
cb: MsgCallbacks<Self>,
}
#[derive(Debug, PartialEq, Properties)]
pub struct AlertProps {
#[prop_or_default]
pub dark: Option<bool>,
#[prop_or_default]
pub class: Classes,
#[prop_or_default]
pub style: Option<AttrValue>,
#[prop_or_default]
pub open: bool,
#[prop_or_default]
pub icon: Option<Icon>,
#[prop_or_default]
pub intent: Option<Intent>,
#[prop_or_default]
pub loading: bool,
#[prop_or(html!("OK"))]
pub confirm_button: Html,
#[prop_or_default]
pub cancel_button: Option<Html>,
#[prop_or_default]
pub onclose: Callback<bool>,
#[prop_or_default]
pub children: Children,
}
#[derive(yew_callbacks::Callbacks)]
pub enum Msg {
OnCancel,
OnConfirmClick(MouseEvent),
OnCancelClick(MouseEvent),
}
impl Component for Alert {
type Properties = AlertProps;
type Message = Msg;
fn create(ctx: &Context<Self>) -> Self {
Self {
cb: ctx.link().into(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let Self::Properties { onclose, .. } = ctx.props();
let Self::Properties { loading, .. } = ctx.props();
match msg {
Msg::OnCancel => {
if !loading {
onclose.emit(false);
}
false
}
Msg::OnConfirmClick(_event) => {
if !loading {
onclose.emit(true);
}
false
}
Msg::OnCancelClick(_event) => {
if !loading {
onclose.emit(false);
}
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let Self::Properties {
dark,
class,
style,
open,
icon,
intent,
loading,
confirm_button,
cancel_button,
onclose: _,
children,
} = ctx.props();
let cancel_button_html = cancel_button.clone().map(|x| {
html! {
<Button
disabled={loading}
onclick={self.cb.on_cancel_click()}
>
{x}
</Button>
}
});
html! {
<Dialog
{dark}
class={classes!("bp3-alert", class.clone())}
{style}
{open}
onclose={self.cb.on_cancel()}
>
<div class={classes!("bp3-alert-body")}>
<Icon {icon} size={40} {intent} />
<div class={classes!("bp3-alert-contents")}>
{for children.iter()}
</div>
</div>
<div class={classes!("bp3-alert-footer")}>
<Button
{loading}
{intent}
onclick={self.cb.on_confirm_click()}
>
{confirm_button.clone()}
</Button>
{cancel_button_html}
</div>
</Dialog>
}
}
}

View file

@ -2,105 +2,187 @@ use crate::{Icon, IconSize, Intent, Spinner};
use yew::prelude::*; use yew::prelude::*;
use yew::virtual_dom::AttrValue; use yew::virtual_dom::AttrValue;
#[derive(Clone, PartialEq, Properties)] macro_rules! generate_with_common_props {
pub struct ButtonProps { (
#[prop_or_default] $(#[$attr:meta])*
pub fill: bool, $vis:vis struct $name:ident {
#[prop_or_default] $(
pub minimal: bool, $(#[$field_attr:meta])*
#[prop_or_default] $field_vis:vis $field_name:ident : $field_ty:ty,
pub small: bool, )*
#[prop_or_default] }
pub outlined: bool, ) => {
#[prop_or_default] $(#[$attr])*
pub loading: bool, $vis struct $name {
#[prop_or_default] #[prop_or_default]
pub large: bool, pub fill: bool,
#[prop_or_default] #[prop_or_default]
pub active: bool, pub minimal: bool,
#[prop_or_default] #[prop_or_default]
pub disabled: bool, pub small: bool,
#[prop_or_default] #[prop_or_default]
pub icon: Option<Icon>, pub outlined: bool,
#[prop_or_default] #[prop_or_default]
pub right_icon: Option<Icon>, pub loading: bool,
#[prop_or_default] #[prop_or_default]
pub intent: Option<Intent>, pub large: bool,
#[prop_or_default] #[prop_or_default]
pub title: Option<AttrValue>, pub active: bool,
#[prop_or_default] #[prop_or_default]
pub onclick: Callback<MouseEvent>, pub disabled: bool,
#[prop_or_default] #[prop_or_default]
pub class: Classes, pub icon: Option<Icon>,
#[prop_or_default] #[prop_or_default]
pub style: Option<AttrValue>, pub right_icon: Option<Icon>,
#[prop_or_default] #[prop_or_default]
pub button_ref: NodeRef, pub intent: Option<Intent>,
#[prop_or_default] #[prop_or_default]
pub children: Children, pub title: Option<AttrValue>,
#[prop_or_default]
pub class: Classes,
#[prop_or_default]
pub style: Option<AttrValue>,
#[prop_or_default]
pub button_ref: NodeRef,
#[prop_or_default]
pub left_element: Option<Html>,
#[prop_or_default]
pub right_element: Option<Html>,
#[prop_or_default]
pub aria_label: Option<AttrValue>,
#[prop_or_default]
pub children: Children,
$(
$(#[$field_attr])*
$field_vis $field_name: $field_ty,
)*
}
impl $name {
fn common_classes(&self) -> Classes {
let disabled = self.disabled || self.loading;
classes!(
"bp3-button",
self.fill.then_some("bp3-fill"),
self.minimal.then_some("bp3-minimal"),
self.small.then_some("bp3-small"),
self.outlined.then_some("bp3-outlined"),
self.loading.then_some("bp3-loading"),
self.large.then_some("bp3-large"),
(self.active && !disabled).then_some("bp3-active"),
disabled.then_some("bp3-disabled"),
self.intent,
self.class.clone(),
)
}
fn render_children(&self) -> Html {
html! {
<>
{
self.loading
.then(|| html! {
<Spinner
class={classes!("bp3-button-spinner")}
size={IconSize::LARGE}
/>
})
}
<Icon icon={self.icon.clone()} />
{self.left_element.clone()}
{
(!self.children.is_empty())
.then(|| html! {
<span class="bp3-button-text">
{for self.children.iter()}
</span>
})
}
<Icon icon={self.right_icon.clone()} />
{self.right_element.clone()}
</>
}
}
}
};
}
generate_with_common_props! {
#[derive(Clone, PartialEq, Properties)]
pub struct ButtonProps {
#[prop_or_default]
pub onclick: Callback<MouseEvent>,
}
} }
#[function_component(Button)] #[function_component(Button)]
pub fn button(props: &ButtonProps) -> Html { pub fn button(props: &ButtonProps) -> Html {
let ButtonProps { let ButtonProps {
fill,
minimal,
small,
outlined,
loading, loading,
large,
active,
disabled, disabled,
icon,
right_icon,
intent,
title, title,
onclick,
class,
style, style,
button_ref, button_ref,
children, aria_label,
onclick,
..
} = props; } = props;
let disabled = *disabled || *loading;
html! { html! {
<button <button
class={classes!( class={props.common_classes()}
"bp3-button",
fill.then_some("bp3-fill"),
minimal.then_some("bp3-minimal"),
small.then_some("bp3-small"),
outlined.then_some("bp3-outlined"),
loading.then_some("bp3-loading"),
large.then_some("bp3-large"),
(*active && !disabled).then_some("bp3-active"),
disabled.then_some("bp3-disabled"),
intent,
class.clone(),
)}
{style} {style}
{title} {title}
aria-label={aria_label}
onclick={(!disabled).then_some(onclick.clone())} onclick={(!disabled).then_some(onclick.clone())}
ref={button_ref.clone()} ref={button_ref.clone()}
> >
{ {props.render_children()}
loading
.then(|| html! {
<Spinner
class={classes!("bp3-button-spinner")}
size={IconSize::LARGE}
/>
})
}
<Icon {icon} />
{
(!children.is_empty())
.then(|| html! {
<span class="bp3-button-text">
{for children.iter()}
</span>
})
}
<Icon icon={right_icon} />
</button> </button>
} }
} }
generate_with_common_props! {
#[derive(Clone, PartialEq, Properties)]
pub struct AnchorButtonProps {
#[prop_or_default]
pub href: AttrValue,
#[prop_or_default]
pub target: Option<AttrValue>,
}
}
#[function_component(AnchorButton)]
pub fn anchor_button(props: &AnchorButtonProps) -> Html {
let AnchorButtonProps {
loading,
disabled,
title,
style,
button_ref,
aria_label,
href,
target,
..
} = props;
let disabled = *disabled || *loading;
html! {
<a
class={props.common_classes()}
{style}
{title}
aria-label={aria_label}
href={(!disabled).then_some(href.clone())}
{target}
ref={button_ref.clone()}
>
{props.render_children()}
</a>
}
}

221
src/dialog.rs Normal file
View file

@ -0,0 +1,221 @@
use crate::{Button, Icon, IconSize, Overlay, H4};
use std::cell::Cell;
use yew::prelude::*;
#[derive(Debug)]
pub struct Dialog {
title_id: AttrValue,
cb: MsgCallbacks<Self>,
}
#[derive(Debug, PartialEq, Properties)]
pub struct DialogProps {
#[prop_or_default]
pub dark: Option<bool>,
#[prop_or_default]
pub class: Classes,
#[prop_or_default]
pub style: Option<AttrValue>,
#[prop_or_default]
pub open: bool,
#[prop_or_default]
pub onclose: Callback<()>,
#[prop_or(true)]
pub show_close_button: bool,
#[prop_or_default]
pub icon: Option<Icon>,
#[prop_or_default]
pub title: Option<Html>,
#[prop_or_default]
pub container_ref: NodeRef,
#[prop_or_default]
pub aria_labelledby: Option<AttrValue>,
#[prop_or_default]
pub aria_describedby: Option<AttrValue>,
#[prop_or_default]
pub children: Children,
}
#[derive(yew_callbacks::Callbacks)]
pub enum Msg {
OnClose(MouseEvent),
}
impl Component for Dialog {
type Properties = DialogProps;
type Message = Msg;
fn create(ctx: &Context<Self>) -> Self {
thread_local! {
static ID: Cell<usize> = Default::default();
}
let id = ID.with(|x| {
let next = x.get().wrapping_add(1);
x.replace(next)
});
let title_id = AttrValue::from(format!("title-bp-dialog-{id}"));
Self {
title_id,
cb: ctx.link().into(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::OnClose(_event) => {
ctx.props().onclose.emit(());
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let Self::Properties {
dark,
class,
style,
open,
onclose,
show_close_button,
icon,
title,
container_ref,
aria_labelledby,
aria_describedby,
children,
} = ctx.props();
let aria_labelledby = aria_labelledby
.clone()
.or(title.is_some().then(|| self.title_id.clone()));
let close_button = show_close_button.then(|| {
html! {
<Button
aria_label="Close"
class={classes!("bp3-dialog-close-button")}
left_element={html!(<Icon icon={Icon::SmallCross} size={IconSize::LARGE} />)}
minimal=true
onclick={self.cb.on_close()}
/>
}
});
let header = title.clone().map(|title| {
html! {
<div class={classes!("bp3-dialog-header")}>
<Icon {icon} size={IconSize::LARGE} aria_hidden=true tab_index={-1} />
<H4 id={&self.title_id}>{title}</H4>
{close_button}
</div>
}
});
html! {
<Overlay
{container_ref}
{dark}
class={classes!("bp3-dialog-container")}
{open}
{onclose}
scrollable=true
backdrop=true
>
<div
class={classes!("bp3-dialog", class.clone())}
role="dialog"
aria-labelledby={aria_labelledby}
aria-describedby={aria_describedby}
{style}
>
{header}
{for children.iter()}
</div>
</Overlay>
}
}
}
#[derive(Clone, PartialEq, Properties)]
pub struct DialogFooterProps {
#[prop_or_default]
pub class: Classes,
#[prop_or_default]
pub style: Option<AttrValue>,
#[prop_or(false)]
pub minimal: bool,
#[prop_or_default]
pub actions: Option<Html>,
#[prop_or_default]
pub children: Children,
}
#[function_component(DialogFooter)]
pub fn dialog_footer(props: &DialogFooterProps) -> Html {
let DialogFooterProps {
class,
style,
minimal,
actions,
children,
} = props;
let actions_html = actions.clone().map(|html| {
html! {
<div class="bp3-dialog-footer-actions">{html}</div>
}
});
html! {
<div
class={classes!(
"bp3-dialog-footer",
(!minimal).then_some("bp3-dialog-footer-fixed"),
class.clone(),
)}
{style}
>
<div class="bp3-dialog-footer-main-section">
{for children.iter()}
</div>
{actions_html}
</div>
}
}
#[derive(Clone, PartialEq, Properties)]
pub struct DialogBodyProps {
#[prop_or_default]
pub class: Classes,
#[prop_or_default]
pub style: Option<AttrValue>,
#[prop_or(true)]
pub use_overflow_scroll_container: bool,
#[prop_or_default]
pub children: Children,
}
#[function_component(DialogBody)]
pub fn dialog_body(props: &DialogBodyProps) -> Html {
let DialogBodyProps {
class,
style,
use_overflow_scroll_container,
children,
} = props;
html! {
<div
role="dialogbody"
class={classes!(
"bp3-dialog-body",
use_overflow_scroll_container.then_some("bp3-dialog-body-scroll-container"),
class.clone(),
)}
{style}
>
{for children.iter()}
</div>
}
}

View file

@ -2,6 +2,8 @@ use yew::prelude::*;
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct ChildrenOnlyProps { pub struct ChildrenOnlyProps {
#[prop_or_default]
pub id: Option<AttrValue>,
#[prop_or_default] #[prop_or_default]
pub class: Classes, pub class: Classes,
#[prop_or_default] #[prop_or_default]
@ -11,9 +13,15 @@ pub struct ChildrenOnlyProps {
macro_rules! build_component { macro_rules! build_component {
($name:ident, $tag:tt, $class:literal) => { ($name:ident, $tag:tt, $class:literal) => {
#[function_component($name)] #[function_component($name)]
pub fn $tag(ChildrenOnlyProps { class, children }: &ChildrenOnlyProps) -> Html { pub fn $tag(
ChildrenOnlyProps {
id,
class,
children,
}: &ChildrenOnlyProps,
) -> Html {
html! { html! {
<$tag class={classes!($class, class.clone())}> <$tag {id} class={classes!($class, class.clone())}>
{children.clone()} {children.clone()}
</$tag> </$tag>
} }

View file

@ -73,6 +73,12 @@ impl IntoPropValue<f64> for IconSize {
} }
} }
impl IntoPropValue<IconSize> for Option<IconSize> {
fn into_prop_value(self) -> IconSize {
self.unwrap_or_default()
}
}
impl Default for Icon { impl Default for Icon {
#[inline] #[inline]
fn default() -> Self { fn default() -> Self {
@ -97,6 +103,8 @@ impl Icon {
intent, intent,
size, size,
onclick, onclick,
aria_hidden,
tab_index,
}: &IconProps, }: &IconProps,
) -> Html { ) -> Html {
if let Icon::Custom(html) = icon { if let Icon::Custom(html) = icon {
@ -117,10 +125,14 @@ impl Icon {
let width = AttrValue::from(format!("{size}")); let width = AttrValue::from(format!("{size}"));
let height = width.clone(); let height = width.clone();
let aria_hidden = aria_hidden.or(title.is_some().then_some(true));
html! { html! {
<span <span
class={classes!("bp3-icon", class.clone(), intent)} class={classes!("bp3-icon", class.clone(), intent)}
{onclick} {onclick}
aria-hidden={aria_hidden.and_then(|x| x.then_some(AttrValue::Static("true")))}
tabIndex={tab_index.map(|x| x.to_string())}
> >
<svg <svg
{fill} {fill}
@ -159,6 +171,10 @@ pub struct IconProps {
pub size: IconSize, pub size: IconSize,
#[prop_or_default] #[prop_or_default]
pub onclick: Callback<MouseEvent>, pub onclick: Callback<MouseEvent>,
#[prop_or_default]
pub aria_hidden: Option<bool>,
#[prop_or_default]
pub tab_index: Option<isize>,
} }
impl Component for Icon { impl Component for Icon {

View file

@ -5,9 +5,11 @@
clippy::type_complexity, clippy::type_complexity,
clippy::derive_partial_eq_without_eq, clippy::derive_partial_eq_without_eq,
clippy::uninlined_format_args, clippy::uninlined_format_args,
clippy::derivable_impls clippy::derivable_impls,
clippy::enum_variant_names
)] )]
mod alert;
mod button_group; mod button_group;
mod buttons; mod buttons;
mod callout; mod callout;
@ -15,6 +17,7 @@ mod card;
mod checkbox; mod checkbox;
mod collapse; mod collapse;
mod control_group; mod control_group;
mod dialog;
mod divider; mod divider;
mod html_elements; mod html_elements;
mod html_select; mod html_select;
@ -38,6 +41,7 @@ mod text_area;
#[cfg(feature = "tree")] #[cfg(feature = "tree")]
mod tree; mod tree;
pub use alert::*;
pub use button_group::*; pub use button_group::*;
pub use buttons::*; pub use buttons::*;
pub use callout::*; pub use callout::*;
@ -45,6 +49,7 @@ pub use card::*;
pub use checkbox::*; pub use checkbox::*;
pub use collapse::*; pub use collapse::*;
pub use control_group::*; pub use control_group::*;
pub use dialog::*;
pub use divider::*; pub use divider::*;
pub use html_elements::*; pub use html_elements::*;
pub use html_select::*; pub use html_select::*;
@ -71,6 +76,8 @@ pub use text_area::*;
pub use tree::*; pub use tree::*;
use implicit_clone::ImplicitClone; use implicit_clone::ImplicitClone;
use std::cell::Cell;
use yew::classes;
use yew::Classes; use yew::Classes;
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
@ -180,3 +187,57 @@ impl From<&Elevation> for Classes {
Self::from(*elevation) Self::from(*elevation)
} }
} }
pub struct Dark;
impl Dark {
pub fn with<T>(&self, f: impl FnOnce(&Cell<bool>) -> T) -> T {
thread_local! {
static DARK: Cell<bool> = {
Cell::new(web_sys::window()
.and_then(|x| x.match_media("(prefers-color-scheme: dark)").ok().flatten())
.map(|x| x.matches())
.unwrap_or(true))
}
}
DARK.with(f)
}
pub fn get(&self) -> bool {
self.with(|x| x.get())
}
pub fn set(&self, value: bool) {
self.with(|x| x.set(value))
}
pub fn replace(&self, value: bool) -> bool {
self.with(|x| x.replace(value))
}
pub fn toggle(&self) -> bool {
self.with(|x| {
let value = x.get();
x.set(!value);
value
})
}
pub fn classes(&self) -> Classes {
self.classes_with_override(None)
}
pub fn classes_with_override(&self, force: impl Into<Option<bool>>) -> Classes {
if force.into().unwrap_or(self.get()) {
thread_local! {
static CLASSES: Classes = classes!("bp3-dark");
}
CLASSES.with(|x| x.clone())
} else {
thread_local! {
static CLASSES: Classes = classes!();
}
CLASSES.with(|x| x.clone())
}
}
}

View file

@ -1,4 +1,4 @@
use crate::Portal; use crate::{Dark, Portal};
use gloo::timers::callback::Timeout; use gloo::timers::callback::Timeout;
use wasm_bindgen::closure::Closure; use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
@ -25,10 +25,15 @@ pub struct Overlay {
#[derive(Debug, PartialEq, Properties)] #[derive(Debug, PartialEq, Properties)]
pub struct OverlayProps { pub struct OverlayProps {
#[prop_or_default]
pub dark: Option<bool>,
#[prop_or_default] #[prop_or_default]
pub class: Classes, pub class: Classes,
#[prop_or_default] #[prop_or_default]
pub style: Option<AttrValue>, pub style: Option<AttrValue>,
// NOTE: this should have been false by default
#[prop_or(true)]
pub scrollable: bool,
#[prop_or_default] #[prop_or_default]
pub open: bool, pub open: bool,
#[prop_or(true)] #[prop_or(true)]
@ -36,6 +41,8 @@ pub struct OverlayProps {
#[prop_or_default] #[prop_or_default]
pub onclose: Callback<()>, pub onclose: Callback<()>,
#[prop_or_default] #[prop_or_default]
pub container_ref: NodeRef,
#[prop_or_default]
pub children: Children, pub children: Children,
} }
@ -183,11 +190,14 @@ impl Component for Overlay {
fn view(&self, ctx: &Context<Self>) -> Html { fn view(&self, ctx: &Context<Self>) -> Html {
let Self::Properties { let Self::Properties {
dark,
class, class,
style, style,
scrollable,
open, open,
backdrop, backdrop,
onclose: _, onclose: _,
container_ref,
children, children,
} = ctx.props(); } = ctx.props();
@ -230,10 +240,12 @@ impl Component for Overlay {
html! { html! {
<Portal> <Portal>
<div <div
ref={container_ref}
class={classes!( class={classes!(
"bp3-overlay", "bp3-overlay",
"bp3-overlay-scroll-container", scrollable.then_some("bp3-overlay-scroll-container"),
open.then_some("bp3-overlay-open"), open.then_some("bp3-overlay-open"),
Dark.classes_with_override(*dark),
)} )}
aria-live="polite" aria-live="polite"
onkeydown={ctx.link().callback(Msg::OnKeyDown)} onkeydown={ctx.link().callback(Msg::OnKeyDown)}

View file

@ -0,0 +1,106 @@
use gloo::timers::callback::Timeout;
use yew::prelude::*;
use yewprint::{Alert, Button, Icon, Intent};
pub struct Example {
open_error: bool,
open_deletion: bool,
loading: bool,
}
#[derive(Clone, PartialEq, Properties)]
pub struct ExampleProps {
pub will_load: bool,
}
pub enum Msg {
OpenFileError,
OpenFileDeletion,
Close(bool),
FinishClosing,
}
impl Component for Example {
type Message = Msg;
type Properties = ExampleProps;
fn create(_ctx: &Context<Self>) -> Self {
Example {
open_error: false,
open_deletion: false,
loading: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let Self::Properties { will_load, .. } = *ctx.props();
match msg {
Msg::OpenFileError => {
self.loading = false;
self.open_error = true;
}
Msg::OpenFileDeletion => {
self.loading = false;
self.open_deletion = true;
}
Msg::Close(res) => {
if res {
if will_load {
self.loading = true;
let callback = ctx.link().callback(|()| Msg::FinishClosing);
Timeout::new(2000, move || callback.emit(())).forget();
} else {
self.open_error = false;
self.open_deletion = false;
}
} else {
self.open_error = false;
self.open_deletion = false;
}
}
Msg::FinishClosing => {
self.open_error = false;
self.open_deletion = false;
}
}
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
<Button
onclick={ctx.link().callback(|_| Msg::OpenFileError)}
>
{"Open file error alert"}
</Button>
<Alert
open={self.open_error}
onclose={ctx.link().callback(|res| Msg::Close(res))}
loading={self.loading}
>
<p>{"Couldn't create the file because the containing folder doesn't \
exist anymore. You will be assimilated."}</p>
</Alert>
<Button
onclick={ctx.link().callback(|_| Msg::OpenFileDeletion)}
>
{"Open file deletion alert"}
</Button>
<Alert
open={self.open_deletion}
onclose={ctx.link().callback(|res| Msg::Close(res))}
loading={self.loading}
icon={Icon::Trash}
intent={Intent::Danger}
confirm_button={html!("Move to Trash")}
cancel_button={html!("Cancel")}
>
<p>{"Are you sure you want to move "}<b>{"filename"}</b>{" to Trash? \
You will be able to restore it later, but it will become Borg."}</p>
</Alert>
</>
}
}
}

View file

@ -0,0 +1,79 @@
mod example;
use crate::ExampleContainer;
use example::*;
use yew::prelude::*;
use yewprint::{Switch, H1, H5};
pub struct AlertDoc {
callback: Callback<ExampleProps>,
state: ExampleProps,
}
impl Component for AlertDoc {
type Message = ExampleProps;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
AlertDoc {
callback: ctx.link().callback(|x| x),
state: ExampleProps { will_load: true },
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
self.state = msg;
true
}
fn view(&self, _ctx: &Context<Self>) -> Html {
let example_props = self.state.clone();
let source = crate::include_raw_html!(
concat!(env!("OUT_DIR"), "/", file!(), ".html"),
"bp3-code-block"
);
let props_component = html! {
<AlertProps
callback={self.callback.clone()}
example_props={example_props.clone()}
/>
};
html! {
<div>
<H1 class={classes!("docs-title")}>{"Alert"}</H1>
<SourceCodeUrl />
<div>
<ExampleContainer
source={source}
props={Some(props_component)}
>
<Example ..example_props />
</ExampleContainer>
</div>
</div>
}
}
}
crate::build_example_prop_component! {
AlertProps for ExampleProps =>
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
<H5>{"Props"}</H5>
<Switch
onclick={self.update_props(ctx, |props, _| ExampleProps {
will_load: !props.will_load,
..props
})}
checked={ctx.props().example_props.will_load}
label={html!("Does alert use loading state")}
/>
</div>
}
}
}
crate::build_source_code_component!();

View file

@ -1,3 +1,4 @@
use crate::alert::*;
use crate::button_group::*; use crate::button_group::*;
use crate::buttons::*; use crate::buttons::*;
use crate::callout::*; use crate::callout::*;
@ -5,6 +6,7 @@ use crate::card::*;
use crate::checkbox::*; use crate::checkbox::*;
use crate::collapse::*; use crate::collapse::*;
use crate::control_group::*; use crate::control_group::*;
use crate::dialog::*;
use crate::divider::*; use crate::divider::*;
use crate::html_select::*; use crate::html_select::*;
use crate::icon::*; use crate::icon::*;
@ -23,10 +25,9 @@ use crate::tag::*;
use crate::text::*; use crate::text::*;
use crate::text_area::*; use crate::text_area::*;
use crate::tree::*; use crate::tree::*;
use crate::DARK;
use yew::prelude::*; use yew::prelude::*;
use yew_router::prelude::*; use yew_router::prelude::*;
use yewprint::{Icon, Menu, MenuItem}; use yewprint::{Dark, Icon, Menu, MenuItem};
#[function_component(AppRoot)] #[function_component(AppRoot)]
pub fn app_root() -> Html { pub fn app_root() -> Html {
@ -55,7 +56,7 @@ impl Component for App {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg { match msg {
Msg::ToggleLight => { Msg::ToggleLight => {
DARK.with(|x| x.replace(!x.get())); Dark.toggle();
} }
Msg::GoToMenu(event, doc_menu) => { Msg::GoToMenu(event, doc_menu) => {
event.prevent_default(); event.prevent_default();
@ -70,7 +71,7 @@ impl Component for App {
} }
fn view(&self, ctx: &Context<Self>) -> Html { fn view(&self, ctx: &Context<Self>) -> Html {
let dark = DARK.with(|x| x.get()); let dark = Dark.get();
let netlify_badge = if dark { let netlify_badge = if dark {
"https://www.netlify.com/img/global/badges/netlify-color-accent.svg" "https://www.netlify.com/img/global/badges/netlify-color-accent.svg"
@ -88,6 +89,12 @@ impl Component for App {
.callback(|_| Msg::ToggleLight)} .callback(|_| Msg::ToggleLight)}
icon={go_to_theme_icon} icon={go_to_theme_icon}
/> />
<MenuItem
text={html!("Alert")}
href="/alert"
onclick={ctx.link()
.callback(|e| Msg::GoToMenu(e, DocMenu::Alert))}
/>
<MenuItem <MenuItem
text={html!("Button")} text={html!("Button")}
href="/button" href="/button"
@ -130,6 +137,12 @@ impl Component for App {
onclick={ctx.link() onclick={ctx.link()
.callback(|e| Msg::GoToMenu(e, DocMenu::ControlGroup))} .callback(|e| Msg::GoToMenu(e, DocMenu::ControlGroup))}
/> />
<MenuItem
text={html!("Dialog")}
href="/dialog"
onclick={ctx.link()
.callback(|e| Msg::GoToMenu(e, DocMenu::Dialog))}
/>
<MenuItem <MenuItem
text={html!("Divider")} text={html!("Divider")}
href="/divider" href="/divider"
@ -276,7 +289,7 @@ impl Component for App {
}; };
html! { html! {
<div class={classes!("docs-root", dark.then_some("bp3-dark"))}> <div class={classes!("docs-root", Dark.classes())}>
<div class={classes!("docs-app")}> <div class={classes!("docs-app")}>
{{ navigation }} {{ navigation }}
<main class={classes!("docs-content-wrapper")} role="main"> <main class={classes!("docs-content-wrapper")} role="main">
@ -292,6 +305,7 @@ impl Component for App {
fn switch(route: DocMenu) -> Html { fn switch(route: DocMenu) -> Html {
match route { match route {
DocMenu::Alert => html! (<AlertDoc />),
DocMenu::Button | DocMenu::Home => html! (<ButtonDoc />), DocMenu::Button | DocMenu::Home => html! (<ButtonDoc />),
DocMenu::ButtonGroup => html! (<ButtonGroupDoc />), DocMenu::ButtonGroup => html! (<ButtonGroupDoc />),
DocMenu::Callout => html!(<CalloutDoc />), DocMenu::Callout => html!(<CalloutDoc />),
@ -299,6 +313,7 @@ fn switch(route: DocMenu) -> Html {
DocMenu::Checkbox => html!(<CheckboxDoc />), DocMenu::Checkbox => html!(<CheckboxDoc />),
DocMenu::Collapse => html!(<CollapseDoc />), DocMenu::Collapse => html!(<CollapseDoc />),
DocMenu::ControlGroup => html!(<ControlGroupDoc />), DocMenu::ControlGroup => html!(<ControlGroupDoc />),
DocMenu::Dialog => html! (<DialogDoc />),
DocMenu::Divider => html!(<DividerDoc />), DocMenu::Divider => html!(<DividerDoc />),
DocMenu::HtmlSelect => html!(<HtmlSelectDoc />), DocMenu::HtmlSelect => html!(<HtmlSelectDoc />),
DocMenu::Icon => html!(<IconDoc />), DocMenu::Icon => html!(<IconDoc />),
@ -322,6 +337,8 @@ fn switch(route: DocMenu) -> Html {
#[derive(PartialEq, Eq, Hash, Clone, Copy, Routable)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Routable)]
pub enum DocMenu { pub enum DocMenu {
#[at("/alert")]
Alert,
#[at("/button-group")] #[at("/button-group")]
ButtonGroup, ButtonGroup,
#[at("/button")] #[at("/button")]
@ -336,10 +353,12 @@ pub enum DocMenu {
Collapse, Collapse,
#[at("/control-group")] #[at("/control-group")]
ControlGroup, ControlGroup,
#[at("/html-select")] #[at("/dialog")]
HtmlSelect, Dialog,
#[at("/divider")] #[at("/divider")]
Divider, Divider,
#[at("/html-select")]
HtmlSelect,
#[at("/icon")] #[at("/icon")]
Icon, Icon,
#[at("/input-group")] #[at("/input-group")]

View file

@ -0,0 +1,177 @@
use yew::prelude::*;
use yewprint::{AnchorButton, Button, Dialog, DialogBody, DialogFooter, Icon, Intent};
pub struct Example {
show_dialog: bool,
show_dialog_with_title: bool,
show_dialog_with_title_and_footer: bool,
}
#[derive(Clone, PartialEq, Properties)]
pub struct ExampleProps {}
pub enum Msg {
ShowDialog,
ShowDialogWithTitle,
ShowDialogWithTitleAndFooter,
Close,
}
impl Component for Example {
type Message = Msg;
type Properties = ExampleProps;
fn create(_ctx: &Context<Self>) -> Self {
Example {
show_dialog: false,
show_dialog_with_title: false,
show_dialog_with_title_and_footer: false,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::ShowDialog => {
self.show_dialog = true;
}
Msg::ShowDialogWithTitle => {
self.show_dialog_with_title = true;
}
Msg::ShowDialogWithTitleAndFooter => {
self.show_dialog_with_title_and_footer = true;
}
Msg::Close => {
self.show_dialog = false;
self.show_dialog_with_title = false;
self.show_dialog_with_title_and_footer = false;
}
}
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
<Button
onclick={ctx.link().callback(|_| Msg::ShowDialog)}
>
{"Show dialog"}
</Button>
<Dialog
open={self.show_dialog}
onclose={ctx.link().callback(|_| Msg::Close)}
>
<div style="margin: 0px 20px;">
<div class="bp3-popover2-target" aria-haspopup="true">
<InterestingDialogBody />
<VisitChatGptWebsiteButton />
</div>
</div>
</Dialog>
<Button
onclick={ctx.link().callback(|_| Msg::ShowDialogWithTitle)}
>
{"Show dialog with title"}
</Button>
<Dialog
open={self.show_dialog_with_title}
onclose={ctx.link().callback(|_| Msg::Close)}
title={html!("Embracing disruptive approach")}
icon={Icon::InfoSign}
>
<div style="margin: 0px 20px;">
<div class="bp3-popover2-target" aria-haspopup="true">
<InterestingDialogBody />
<VisitChatGptWebsiteButton />
</div>
</div>
</Dialog>
<Button
onclick={ctx.link().callback(|_| Msg::ShowDialogWithTitleAndFooter)}
>
{"Show dialog with title and footer"}
</Button>
<Dialog
open={self.show_dialog_with_title_and_footer}
onclose={ctx.link().callback(|_| Msg::Close)}
title={html!("Embracing disruptive approach")}
icon={Icon::InfoSign}
>
<InterestingDialogBody />
<DialogFooter
actions={html! {
<>
<Button onclick={ctx.link().callback(|_| Msg::Close)}>
{"Close"}
</Button>
<VisitChatGptWebsiteButton fill=false />
</>
}}
/>
</Dialog>
</>
}
}
}
#[function_component(InterestingDialogBody)]
pub fn interesting_content(_: &()) -> Html {
html! {
<DialogBody>
<p><strong>{"Our transformative solution optimizes paperclip management \
through advanced technology, revolutionizing office supply logistics and \
driving operational efficiency."}</strong></p>
<p>{"Leveraging synergistic ideation and cutting-edge paradigm shifts,
our groundbreaking solution addresses the pervasive issue of inefficient \
paperclip utilization. We recognize the urgent need to optimize paperclip
distribution and deployment, revolutionizing the way organizations engage
with this crucial office asset. By seamlessly integrating blockchain
technology, artificial intelligence algorithms, and quantum-inspired
computing, our transformative platform offers a paradigmatic shift in the
paperclip management landscape."}</p>
<p>{"Our innovative solution streamlines paperclip logistics through a \
gamified interface that incentivizes employees to achieve maximum paperclip
productivity. Leveraging the power of machine learning, our algorithm
predicts demand patterns, ensuring optimal inventory levels and eliminating
wasteful overstocking. Furthermore, our blockchain-based tracking system
guarantees immutable traceability, empowering organizations to perform
data-driven analysis on paperclip utilization, leading to unprecedented
insights and cost-saving opportunities."}</p>
<p>{"By embracing this disruptive approach, organizations can transcend \
traditional paperclip constraints and unlock a new era of operational
efficiency. Our solution not only resolves the current paperclip dilemma
but also paves the way for a sustainable future, reducing environmental
impact through our proprietary green paperclip technology. Join us in the
paperclip revolution and embrace the future of office supplies where
mundane tasks are transformed into innovative experiences that drive
productivity and fuel growth."}</p>
<p>{"Unlock the power of seamless paperclip management with our innovative
solution - experience unparalleled efficiency and cost-saving opportunities
for your organization. Try it today and revolutionize your office supply
logistics!"}</p>
</DialogBody>
}
}
#[derive(Clone, PartialEq, Properties)]
pub struct VisitChatGptWebsiteButtonProps {
#[prop_or(true)]
pub fill: bool,
}
#[function_component(VisitChatGptWebsiteButton)]
pub fn visit_chat_gpt_website_button(
VisitChatGptWebsiteButtonProps { fill }: &VisitChatGptWebsiteButtonProps,
) -> Html {
html! {
<AnchorButton
href="https://chat.openai.com/"
target="_blank"
{fill}
intent={Intent::Primary}
icon={Icon::Share}
>
{"Generate all sorts of bs by visiting ChatGPT"}
</AnchorButton>
}
}

View file

@ -0,0 +1,42 @@
mod example;
use crate::ExampleContainer;
use example::*;
use yew::prelude::*;
use yewprint::H1;
pub struct DialogDoc {
callback: Callback<ExampleProps>,
}
impl Component for DialogDoc {
type Message = ExampleProps;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
DialogDoc {
callback: ctx.link().callback(|x| x),
}
}
fn view(&self, _ctx: &Context<Self>) -> Html {
let source = crate::include_raw_html!(
concat!(env!("OUT_DIR"), "/", file!(), ".html"),
"bp3-code-block"
);
html! {
<div>
<H1 class={classes!("docs-title")}>{"Dialog"}</H1>
<SourceCodeUrl />
<div>
<ExampleContainer source={source}>
<Example />
</ExampleContainer>
</div>
</div>
}
}
}
crate::build_source_code_component!();

View file

@ -8,6 +8,7 @@
clippy::uninlined_format_args clippy::uninlined_format_args
)] )]
mod alert;
mod app; mod app;
mod button_group; mod button_group;
mod buttons; mod buttons;
@ -16,6 +17,7 @@ mod card;
mod checkbox; mod checkbox;
mod collapse; mod collapse;
mod control_group; mod control_group;
mod dialog;
mod divider; mod divider;
mod example; mod example;
mod html_select; mod html_select;
@ -40,16 +42,6 @@ mod tree;
pub use app::*; pub use app::*;
pub use example::*; pub use example::*;
pub use logo::*; pub use logo::*;
use std::cell::Cell;
thread_local! {
pub static DARK: Cell<bool> = {
Cell::new(web_sys::window()
.and_then(|x| x.match_media("(prefers-color-scheme: dark)").ok().flatten())
.map(|x| x.matches())
.unwrap_or(true))
}
}
#[macro_export] #[macro_export]
macro_rules! include_raw_html { macro_rules! include_raw_html {

View file

@ -15,10 +15,6 @@ impl Component for MenuDoc {
MenuDoc MenuDoc
} }
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
true
}
fn view(&self, _ctx: &Context<Self>) -> Html { fn view(&self, _ctx: &Context<Self>) -> Html {
let source = crate::include_raw_html!( let source = crate::include_raw_html!(
concat!(env!("OUT_DIR"), "/", file!(), ".html"), concat!(env!("OUT_DIR"), "/", file!(), ".html"),

View file

@ -1,11 +1,9 @@
use crate::DARK;
use yew::prelude::*; use yew::prelude::*;
use yewprint::{Button, Card, Elevation, Icon, Intent, Overlay, H3}; use yewprint::{Button, Card, Elevation, Icon, Intent, Overlay, H3};
pub struct Example { pub struct Example {
open: bool, open: bool,
tall: bool, tall: bool,
show_button_ref: NodeRef,
} }
#[derive(Clone, PartialEq, Properties)] #[derive(Clone, PartialEq, Properties)]
@ -27,7 +25,6 @@ impl Component for Example {
Example { Example {
open: false, open: false,
tall: false, tall: false,
show_button_ref: NodeRef::default(),
} }
} }
@ -54,7 +51,6 @@ impl Component for Example {
<div> <div>
<Button <Button
onclick={ctx.link().callback(|_| Msg::Open)} onclick={ctx.link().callback(|_| Msg::Open)}
button_ref={self.show_button_ref.clone()}
> >
{"Show overlay"} {"Show overlay"}
</Button> </Button>
@ -63,7 +59,6 @@ impl Component for Example {
onclose={ctx.link().callback(|_| Msg::Close)} onclose={ctx.link().callback(|_| Msg::Close)}
{backdrop} {backdrop}
class={classes!( class={classes!(
DARK.with(|x| x.get().then_some("bp3-dark")),
self.tall.then_some("docs-overlay-example-tall"), self.tall.then_some("docs-overlay-example-tall"),
)} )}
style="left: calc(50vw - 200px); margin: 10vh 0; top: 0; width: 400px;" style="left: calc(50vw - 200px); margin: 10vh 0; top: 0; width: 400px;"