yewprint 0.5.0

Port of blueprintjs.com to Yew
Documentation
use crate::{Button, Icon, IconSize, Overlay, H4};
use std::cell::Cell;
use yew::prelude::*;

#[derive(Debug)]
pub struct Dialog {
    title_id: AttrValue,
    cb: DialogMsgCallbacks<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 DialogMsg {
    OnClose(MouseEvent),
}

impl Component for Dialog {
    type Properties = DialogProps;
    type Message = DialogMsg;

    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 {
            DialogMsg::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>
    }
}