yew-duskmoon 0.8.2

DuskMoon, a Yew component libaray.
Documentation
use yew::prelude::*;
use yew::virtual_dom::AttrValue;

use super::variants;

#[derive(Clone, Copy, Default, PartialEq, Eq)]
pub enum PopoverTrigger {
    #[default]
    Click,
    Hover,
    Focus,
}

#[derive(Properties, Clone, PartialEq)]
pub struct PopoverProps {
    #[prop_or_default]
    pub class: Classes,
    #[prop_or_default]
    pub children: Children,
    #[prop_or_default]
    pub variant: Option<String>,
    #[prop_or_default]
    pub trigger: PopoverTrigger,
    #[prop_or(AttrValue::from("Show popover"))]
    pub trigger_label: AttrValue,
    #[prop_or_else(default_trigger_class)]
    pub trigger_class: Classes,
}

#[function_component(Popover)]
pub fn popover(props: &PopoverProps) -> Html {
    let is_open = use_state_eq(|| false);
    let trigger = props.trigger;
    let placement = PopoverPlacement::from_classes(&props.class);
    let mut classes = classes!("popover");
    if let Some(variant) = &props.variant {
        classes.push(format!("popover-{}", variant));
    }
    if trigger == PopoverTrigger::Hover {
        classes.push("popover-hover");
    }
    if *is_open {
        classes.push("show");
        classes.push("popover-show");
    }
    classes.push(props.class.clone());

    let onclick = {
        let is_open = is_open.clone();
        Callback::from(move |_| {
            if trigger == PopoverTrigger::Click {
                is_open.set(!*is_open);
            }
        })
    };
    let onmouseenter = {
        let is_open = is_open.clone();
        Callback::from(move |_| {
            if trigger == PopoverTrigger::Hover {
                is_open.set(true);
            }
        })
    };
    let onmouseleave = {
        let is_open = is_open.clone();
        Callback::from(move |_| {
            if trigger == PopoverTrigger::Hover {
                is_open.set(false);
            }
        })
    };
    let onfocus = {
        let is_open = is_open.clone();
        Callback::from(move |_| {
            if trigger == PopoverTrigger::Focus {
                is_open.set(true);
            }
        })
    };
    let onblur = {
        let is_open = is_open.clone();
        Callback::from(move |_| {
            if trigger == PopoverTrigger::Focus {
                is_open.set(false);
            }
        })
    };

    let content_style = popover_content_style(props.variant.as_deref(), placement);
    let arrow_style = popover_arrow_style(props.variant.as_deref(), placement);
    let root_style = AttrValue::from(
        "position: relative; display: inline-block; width: max-content; justify-self: start; opacity: 1; visibility: visible; z-index: auto; min-width: 0; max-width: none; padding: 0; background: transparent; border: 0; box-shadow: none;",
    );
    let expanded = if *is_open { "true" } else { "false" };

    html! {
        <div class={classes} style={root_style} onmouseenter={onmouseenter} onmouseleave={onmouseleave}>
            <button
                type="button"
                class={props.trigger_class.clone()}
                aria-haspopup="dialog"
                aria-expanded={expanded}
                onclick={onclick}
                onfocus={onfocus}
                onblur={onblur}
            >
                { props.trigger_label.clone() }
            </button>
            <div class="popover-content" role="dialog" style={content_style}>
                {
                    if props.children.is_empty() {
                        html! { <div class="popover-body">{ "Popover content" }</div> }
                    } else {
                        html! { <>{ for props.children.iter() }</> }
                    }
                }
                <span class="popover-arrow" aria-hidden="true" style={arrow_style}></span>
            </div>
        </div>
    }
}

fn default_trigger_class() -> Classes {
    classes!("btn", "btn-primary")
}

#[derive(Clone, Copy)]
enum PopoverPlacement {
    Top,
    Bottom,
    Left,
    Right,
}

impl PopoverPlacement {
    fn from_classes(classes: &Classes) -> Self {
        if classes.contains("popover-bottom") {
            Self::Bottom
        } else if classes.contains("popover-left") {
            Self::Left
        } else if classes.contains("popover-right") {
            Self::Right
        } else {
            Self::Top
        }
    }

    fn content_style(self) -> &'static str {
        match self {
            Self::Top => "top: auto; right: auto; bottom: 100%; left: 50%; height: auto; margin-top: 0; margin-right: 0; margin-bottom: 0.75rem; margin-left: 0;",
            Self::Bottom => "top: 100%; right: auto; bottom: auto; left: 50%; height: auto; margin-top: 0.75rem; margin-right: 0; margin-bottom: 0; margin-left: 0;",
            Self::Left => "top: 50%; right: 100%; bottom: auto; left: auto; height: auto; margin-top: 0; margin-right: 0.75rem; margin-bottom: 0; margin-left: 0;",
            Self::Right => "top: 50%; right: auto; bottom: auto; left: 100%; height: auto; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0.75rem;",
        }
    }

    fn arrow_style(self) -> &'static str {
        match self {
            Self::Top => "top: auto; right: auto; bottom: -0.375rem; left: 50%; transform: translateX(-50%) rotate(45deg); border-style: solid; border-width: 1px; border-top-width: 0; border-left-width: 0;",
            Self::Bottom => "top: -0.375rem; right: auto; bottom: auto; left: 50%; transform: translateX(-50%) rotate(45deg); border-style: solid; border-width: 1px; border-bottom-width: 0; border-right-width: 0;",
            Self::Left => "top: 50%; right: -0.375rem; bottom: auto; left: auto; transform: translateY(-50%) rotate(45deg); border-style: solid; border-width: 1px; border-left-width: 0; border-bottom-width: 0;",
            Self::Right => "top: 50%; right: auto; bottom: auto; left: -0.375rem; transform: translateY(-50%) rotate(45deg); border-style: solid; border-width: 1px; border-right-width: 0; border-top-width: 0;",
        }
    }
}

fn popover_content_style(variant: Option<&str>, placement: PopoverPlacement) -> AttrValue {
    let declaration = format!(
        "{} {}",
        placement.content_style(),
        "background-color: color-mix(in oklch, var(--component-solid, var(--component-color, var(--color-surface))) var(--popover-color-intensity, 30%), var(--color-surface)); border-color: var(--component-color, var(--color-outline-variant)); color: var(--component-on-container, var(--color-on-surface)); --color-on-surface: var(--component-on-container, var(--color-on-surface)); --color-on-surface-variant: var(--component-on-container, var(--color-on-surface-variant));"
    );

    variants::style(variant, &declaration)
}

fn popover_arrow_style(variant: Option<&str>, placement: PopoverPlacement) -> AttrValue {
    let declaration = format!(
        "{} {}",
        placement.arrow_style(),
        "background-color: color-mix(in oklch, var(--component-solid, var(--component-color, var(--color-surface))) var(--popover-color-intensity, 30%), var(--color-surface)); border-color: var(--component-color, var(--color-outline-variant));"
    );

    variants::style(variant, &declaration)
}