use crate::button_group::InGroupContext;
use crate::class_list;
use crate::class_list::reactive_class::MaybeReactiveClass;
use crate::icon::Icon;
use crate::icon::icon_data::IconRef;
use crate::input_group::GroupItemClassContext;
use crate::util::signals::ComponentRef;
use crate::{spinner::Spinner, util::callback::BoxOneCallback};
use leptodon_proc_macros::generate_docs;
use leptos::logging::debug_log;
use leptos::{IntoView, component, view};
use leptos::{
either::{Either, EitherOf3},
ev, html,
prelude::*,
};
mod variations;
pub use crate::button::variations::*;
const BUTTON_SHADOW_CLASSES: &str = "shadow-sm";
const BUTTON_SPACING_CLASSES: &str = " px-5 py-2.5 mr-2";
const SHARED_BUTTON_CLASSES: &str = "hover:z-20 focus:z-10 dark:focus:ring-gray-800 outline-offset-[-1px] outline-[5px] focus:outline font-medium inline-flex items-center text-center text-sm";
const BUTTON_GRAY_FOCUS_CLASSES: &str =
"!active:outline-oa-gray-darker focus:outline-oa-gray-darker hover:focus:outline-oa-gray ";
const BUTTON_DEFAULT_TEXT: &str = "text-gray-700 dark:text-gray-300";
const OA_PRIMARY_BUTTON_CLASSES: &str = const_str::join!(
&[
"focus:outline-oa-blue hover:bg-oa-blue-darker bg-oa-blue text-white",
SHARED_BUTTON_CLASSES,
BUTTON_SHADOW_CLASSES,
BUTTON_SPACING_CLASSES
],
" "
);
const OA_DANGER_BUTTON_CLASSES: &str = const_str::join!(
&[
"focus:outline-oa-red hover:bg-oa-red-darker bg-oa-red text-white",
SHARED_BUTTON_CLASSES,
BUTTON_SHADOW_CLASSES,
BUTTON_SPACING_CLASSES
],
" "
);
const OA_SECONDARY_BUTTON_CLASSES: &str = const_str::join!(
&[
"border-solid border border-gray-400",
"!active:bg-oa-gray-darker bg-gray-200 hover:bg-oa-gray-darker !dark:active:bg-gray-600 dark:bg-gray-700 hover:dark:bg-gray-600",
BUTTON_GRAY_FOCUS_CLASSES,
BUTTON_DEFAULT_TEXT,
SHARED_BUTTON_CLASSES,
BUTTON_SHADOW_CLASSES,
BUTTON_SPACING_CLASSES
],
" "
);
pub const OA_TRANSPARENT_BUTTON_CLASSES: &str = const_str::join!(
&[
"hover:bg-oa-gray active:bg-oa-gray hover:dark:bg-gray-600 active:dark:bg-gray-600",
SHARED_BUTTON_CLASSES,
BUTTON_DEFAULT_TEXT,
BUTTON_GRAY_FOCUS_CLASSES,
BUTTON_SPACING_CLASSES
],
" "
);
pub const OA_MINIMAL_BUTTON_CLASSES: &str = const_str::join!(
&[
SHARED_BUTTON_CLASSES,
BUTTON_DEFAULT_TEXT,
BUTTON_GRAY_FOCUS_CLASSES
],
" "
);
#[generate_docs]
#[component]
pub fn Button(
#[prop(optional, into)]
id: MaybeProp<String>,
#[prop(optional, into)]
class: MaybeReactiveClass,
#[prop(optional, into)]
appearance: Signal<ButtonAppearance>,
#[prop(optional, into)]
button_type: ButtonType,
#[prop(default = ButtonShape::default(), into)]
shape: ButtonShape,
#[prop(optional, into)]
icon: MaybeProp<Signal<IconRef>>,
#[prop(optional, into)]
loading: Signal<bool>,
#[prop(optional, into)] on_click: Option<BoxOneCallback<ev::MouseEvent>>,
#[prop(optional)] children: Option<Children>,
#[prop(optional)] comp_ref: ComponentRef<ButtonRef>,
) -> impl IntoView
where
{
let in_group = use_context::<InGroupContext>().unwrap_or(InGroupContext { in_group: false });
let aria_disabled = move || {
if loading.get() { Some("true") } else { None }
};
let button_ref = NodeRef::<html::Button>::new();
comp_ref.load(ButtonRef { button_ref });
let on_click = move |e| {
if loading.get_untracked() {
return;
}
let Some(on_click) = on_click.as_ref() else {
return;
};
on_click(e);
};
let group_context = use_context::<GroupItemClassContext>();
let group_classes = group_context.map(|item| item.class);
view! {
<button
id=move || id.get()
class=class_list![
class,
group_classes.unwrap_or_default(),
if in_group.in_group { "rounded-none border-r-0 !mr-0" } else { "" },
match appearance.get() {
ButtonAppearance::Secondary => OA_SECONDARY_BUTTON_CLASSES,
ButtonAppearance::Primary => OA_PRIMARY_BUTTON_CLASSES,
ButtonAppearance::Danger => OA_DANGER_BUTTON_CLASSES,
ButtonAppearance::Subtle => todo!(),
ButtonAppearance::Transparent => OA_TRANSPARENT_BUTTON_CLASSES,
ButtonAppearance::Minimal => OA_MINIMAL_BUTTON_CLASSES,
},
match shape {
ButtonShape::Square => "rounded-none",
ButtonShape::Rounded => "rounded-lg",
ButtonShape::Circular => "rounded-full",
}
]
node_ref=button_ref
type=button_type.as_str()
aria-disabled=aria_disabled
on:click=on_click
>
{move || {
if loading.get() {
EitherOf3::A(
view! {
<span class="thaw-button__icon">
<Spinner />
</span>
},
)
} else if let Some(icon) = icon.get() {
EitherOf3::B(view!{
<Icon icon=icon.get() class="w-5 h-5"/>
})
} else {
EitherOf3::C(())
}
}}
{if let Some(children) = children {
Either::Left(children())
} else {
Either::Right(())
}}
</button>
}
}
#[derive(Default, PartialEq, Clone, Copy)]
pub enum ButtonAppearance {
#[default]
Secondary,
Primary,
Danger,
Subtle,
Transparent,
Minimal,
}
impl ButtonAppearance {
pub fn as_str(&self) -> &'static str {
match self {
ButtonAppearance::Secondary => "secondary",
ButtonAppearance::Primary => "primary",
ButtonAppearance::Subtle => "subtle",
ButtonAppearance::Transparent => "transparent",
ButtonAppearance::Danger => "danger",
ButtonAppearance::Minimal => "minimal",
}
}
}
#[derive(Default, PartialEq, Clone, Copy)]
pub enum ButtonShape {
#[default]
Rounded,
Circular,
Square,
}
impl ButtonShape {
pub fn as_str(&self) -> &'static str {
match self {
ButtonShape::Rounded => "rounded",
ButtonShape::Circular => "circular",
ButtonShape::Square => "square",
}
}
}
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum ButtonSize {
Small,
#[default]
Medium,
Large,
}
impl ButtonSize {
pub fn as_str(&self) -> &'static str {
match self {
ButtonSize::Small => "small",
ButtonSize::Medium => "medium",
ButtonSize::Large => "large",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ButtonSizeInjection(pub ButtonSize);
impl ButtonSizeInjection {
pub fn use_context() -> Option<Self> {
use_context()
}
}
#[derive(Debug, Clone, Default)]
pub enum ButtonType {
Submit,
Reset,
#[default]
Button,
}
impl ButtonType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Submit => "submit",
Self::Reset => "reset",
Self::Button => "button",
}
}
}
#[derive(Clone)]
pub struct ButtonRef {
pub(super) button_ref: NodeRef<html::Button>,
}
impl ButtonRef {
pub fn click(&self) {
if let Some(button_el) = self.button_ref.get_untracked() {
button_el.click();
} else {
debug_log!("Button is missing! can't click");
}
}
pub fn focus(&self) {
if let Some(button_el) = self.button_ref.get_untracked() {
if let Err(err) = button_el.focus() {
debug_log!("{err:?}");
}
debug_log!("Focused button");
} else {
debug_log!("Button is missing! can't focus");
}
}
pub fn blur(&self) {
if let Some(button_el) = self.button_ref.get_untracked() {
if let Err(err) = button_el.blur() {
debug_log!("{err:?}");
}
debug_log!("Blurred button");
} else {
debug_log!("Button is missing! can't blur");
}
}
}