leptos_twelements/components/
button.rs

1use leptos::{html::Button, *};
2use wasm_bindgen::prelude::wasm_bindgen;
3use web_sys::HtmlButtonElement;
4
5use crate::methods::Ripple;
6
7/// [ButtonStyle] influences the look&feel of a button.
8///
9/// See [Tailwind Elements: Buttons#Hierarchy](https://tailwind-elements.com/docs/standard/components/buttons/#hierarchy)
10#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum ButtonStyle {
12    /// The primary button style.
13    ///
14    /// See [Tailwind Elements: Buttons#Hierarchy](https://tailwind-elements.com/docs/standard/components/buttons/#hierarchy)
15    #[default]
16    Primary,
17
18    /// The secondary button style.
19    ///
20    /// See [Tailwind Elements: Buttons#Hierarchy](https://tailwind-elements.com/docs/standard/components/buttons/#hierarchy)
21    Secondary,
22
23    /// The tertiary button style.
24    ///
25    /// See [Tailwind Elements: Buttons#Hierarchy](https://tailwind-elements.com/docs/standard/components/buttons/#hierarchy)
26    Tertiary,
27
28    /// The success button style.
29    ///
30    /// See [Tailwind Elements: Buttons#Contextual](https://tailwind-elements.com/docs/standard/components/buttons/#contextual)
31    Success,
32
33    /// The danger button style.
34    ///
35    /// See [Tailwind Elements: Buttons#Contextual](https://tailwind-elements.com/docs/standard/components/buttons/#contextual)
36    Danger,
37
38    /// The warning button style.
39    ///
40    /// See [Tailwind Elements: Buttons#Contextual](https://tailwind-elements.com/docs/standard/components/buttons/#contextual)
41    Warning,
42
43    /// The info button style.
44    ///
45    /// See [Tailwind Elements: Buttons#Contextual](https://tailwind-elements.com/docs/standard/components/buttons/#contextual)
46    Info,
47
48    /// The neutral light button style.
49    ///
50    /// See [Tailwind Elements: Buttons#Neutral](https://tailwind-elements.com/docs/standard/components/buttons/#neutral)
51    NeutralLight,
52
53    /// The neutral dark button style.
54    ///
55    /// See [Tailwind Elements: Buttons#Neutral](https://tailwind-elements.com/docs/standard/components/buttons/#neutral)
56    NeutralDark,
57
58    /// The neutral dark button style.
59    ///
60    /// See [Tailwind Elements: Buttons#Link](https://tailwind-elements.com/docs/standard/components/buttons/#link)
61    Link,
62
63    /// The floating button style
64    ///
65    /// See [Tailwind Elements: Buttons#Floating](https://tailwind-elements.com/docs/standard/components/buttons/#floating)
66    Floating,
67    // TODO Floating Buttons are probably better handled as a property that's orthogonal to color (e.g. primary, secondary, ...)?
68
69    // TODO Outline variants (rounded and non-rounded)
70    // TODO Fixed buttons
71    // TODO Sizes
72    // TODO With icon
73    // TODO Block buttons
74}
75
76impl ButtonStyle {
77    const fn classes(self) -> &'static str {
78        match self {
79            Self::Primary => "inline-block bg-primary px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#3b71ca] transition duration-150 ease-in-out hover:bg-primary-600 hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:bg-primary-600 focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:outline-none focus:ring-0 active:bg-primary-700 active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(59,113,202,0.5)] dark:hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)]",
80            Self::Secondary => "inline-block bg-primary-100 px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-primary-700 transition duration-150 ease-in-out hover:bg-primary-accent-100 focus:bg-primary-accent-100 focus:outline-none focus:ring-0 active:bg-primary-accent-200",
81            Self::Tertiary => "inline-block px-2 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-primary hover:text-primary-600 focus:text-primary-600 focus:outline-none focus:ring-0 active:text-primary-700",
82            Self::Success => "inline-block bg-success px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#14a44d] transition duration-150 ease-in-out hover:bg-success-600 hover:shadow-[0_8px_9px_-4px_rgba(20,164,77,0.3),0_4px_18px_0_rgba(20,164,77,0.2)] focus:bg-success-600 focus:shadow-[0_8px_9px_-4px_rgba(20,164,77,0.3),0_4px_18px_0_rgba(20,164,77,0.2)] focus:outline-none focus:ring-0 active:bg-success-700 active:shadow-[0_8px_9px_-4px_rgba(20,164,77,0.3),0_4px_18px_0_rgba(20,164,77,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(20,164,77,0.5)] dark:hover:shadow-[0_8px_9px_-4px_rgba(20,164,77,0.2),0_4px_18px_0_rgba(20,164,77,0.1)] dark:focus:shadow-[0_8px_9px_-4px_rgba(20,164,77,0.2),0_4px_18px_0_rgba(20,164,77,0.1)] dark:active:shadow-[0_8px_9px_-4px_rgba(20,164,77,0.2),0_4px_18px_0_rgba(20,164,77,0.1)]",
83            Self::Danger => "inline-block bg-danger px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#dc4c64] transition duration-150 ease-in-out hover:bg-danger-600 hover:shadow-[0_8px_9px_-4px_rgba(220,76,100,0.3),0_4px_18px_0_rgba(220,76,100,0.2)] focus:bg-danger-600 focus:shadow-[0_8px_9px_-4px_rgba(220,76,100,0.3),0_4px_18px_0_rgba(220,76,100,0.2)] focus:outline-none focus:ring-0 active:bg-danger-700 active:shadow-[0_8px_9px_-4px_rgba(220,76,100,0.3),0_4px_18px_0_rgba(220,76,100,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(220,76,100,0.5)] dark:hover:shadow-[0_8px_9px_-4px_rgba(220,76,100,0.2),0_4px_18px_0_rgba(220,76,100,0.1)] dark:focus:shadow-[0_8px_9px_-4px_rgba(220,76,100,0.2),0_4px_18px_0_rgba(220,76,100,0.1)] dark:active:shadow-[0_8px_9px_-4px_rgba(220,76,100,0.2),0_4px_18px_0_rgba(220,76,100,0.1)]",
84            Self::Warning => "inline-block bg-warning px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#e4a11b] transition duration-150 ease-in-out hover:bg-warning-600 hover:shadow-[0_8px_9px_-4px_rgba(228,161,27,0.3),0_4px_18px_0_rgba(228,161,27,0.2)] focus:bg-warning-600 focus:shadow-[0_8px_9px_-4px_rgba(228,161,27,0.3),0_4px_18px_0_rgba(228,161,27,0.2)] focus:outline-none focus:ring-0 active:bg-warning-700 active:shadow-[0_8px_9px_-4px_rgba(228,161,27,0.3),0_4px_18px_0_rgba(228,161,27,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(228,161,27,0.5)] dark:hover:shadow-[0_8px_9px_-4px_rgba(228,161,27,0.2),0_4px_18px_0_rgba(228,161,27,0.1)] dark:focus:shadow-[0_8px_9px_-4px_rgba(228,161,27,0.2),0_4px_18px_0_rgba(228,161,27,0.1)] dark:active:shadow-[0_8px_9px_-4px_rgba(228,161,27,0.2),0_4px_18px_0_rgba(228,161,27,0.1)]",
85            Self::Info => "inline-block bg-info px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#54b4d3] transition duration-150 ease-in-out hover:bg-info-600 hover:shadow-[0_8px_9px_-4px_rgba(84,180,211,0.3),0_4px_18px_0_rgba(84,180,211,0.2)] focus:bg-info-600 focus:shadow-[0_8px_9px_-4px_rgba(84,180,211,0.3),0_4px_18px_0_rgba(84,180,211,0.2)] focus:outline-none focus:ring-0 active:bg-info-700 active:shadow-[0_8px_9px_-4px_rgba(84,180,211,0.3),0_4px_18px_0_rgba(84,180,211,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(84,180,211,0.5)] dark:hover:shadow-[0_8px_9px_-4px_rgba(84,180,211,0.2),0_4px_18px_0_rgba(84,180,211,0.1)] dark:focus:shadow-[0_8px_9px_-4px_rgba(84,180,211,0.2),0_4px_18px_0_rgba(84,180,211,0.1)] dark:active:shadow-[0_8px_9px_-4px_rgba(84,180,211,0.2),0_4px_18px_0_rgba(84,180,211,0.1)]",
86            Self::NeutralLight => "inline-block bg-neutral-50 px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-neutral-800 shadow-[0_4px_9px_-4px_#cbcbcb] transition duration-150 ease-in-out hover:bg-neutral-100 hover:shadow-[0_8px_9px_-4px_rgba(203,203,203,0.3),0_4px_18px_0_rgba(203,203,203,0.2)] focus:bg-neutral-100 focus:shadow-[0_8px_9px_-4px_rgba(203,203,203,0.3),0_4px_18px_0_rgba(203,203,203,0.2)] focus:outline-none focus:ring-0 active:bg-neutral-200 active:shadow-[0_8px_9px_-4px_rgba(203,203,203,0.3),0_4px_18px_0_rgba(203,203,203,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(251,251,251,0.3)] dark:hover:shadow-[0_8px_9px_-4px_rgba(251,251,251,0.1),0_4px_18px_0_rgba(251,251,251,0.05)] dark:focus:shadow-[0_8px_9px_-4px_rgba(251,251,251,0.1),0_4px_18px_0_rgba(251,251,251,0.05)] dark:active:shadow-[0_8px_9px_-4px_rgba(251,251,251,0.1),0_4px_18px_0_rgba(251,251,251,0.05)]",
87            Self::NeutralDark => "inline-block bg-neutral-800 px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-neutral-50 shadow-[0_4px_9px_-4px_rgba(51,45,45,0.7)] transition duration-150 ease-in-out hover:bg-neutral-800 hover:shadow-[0_8px_9px_-4px_rgba(51,45,45,0.2),0_4px_18px_0_rgba(51,45,45,0.1)] focus:bg-neutral-800 focus:shadow-[0_8px_9px_-4px_rgba(51,45,45,0.2),0_4px_18px_0_rgba(51,45,45,0.1)] focus:outline-none focus:ring-0 active:bg-neutral-900 active:shadow-[0_8px_9px_-4px_rgba(51,45,45,0.2),0_4px_18px_0_rgba(51,45,45,0.1)] dark:bg-neutral-900 dark:shadow-[0_4px_9px_-4px_#030202] dark:hover:bg-neutral-900 dark:hover:shadow-[0_8px_9px_-4px_rgba(3,2,2,0.3),0_4px_18px_0_rgba(3,2,2,0.2)] dark:focus:bg-neutral-900 dark:focus:shadow-[0_8px_9px_-4px_rgba(3,2,2,0.3),0_4px_18px_0_rgba(3,2,2,0.2)] dark:active:bg-neutral-900 dark:active:shadow-[0_8px_9px_-4px_rgba(3,2,2,0.3),0_4px_18px_0_rgba(3,2,2,0.2)]",
88            Self::Link => "inline-block px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-primary transition duration-150 ease-in-out hover:bg-neutral-100 hover:text-primary-600 focus:text-primary-600 focus:outline-none focus:ring-0 active:text-primary-700",
89            Self::Floating => "inline-block bg-primary p-2 uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#3b71ca] transition duration-150 ease-in-out hover:bg-primary-600 hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:bg-primary-600 focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:outline-none focus:ring-0 active:bg-primary-700 active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(59,113,202,0.5)] dark:hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)]"
90        }
91    }
92}
93
94// TODO Allow buttons with `href` instead of `on:click`
95
96/// A Button component.
97///
98/// To capture click events, use the `on:click` event property which will be passed through to the underlying button.
99///
100/// See [Tailwind Elements: Buttons](https://tailwind-elements.com/docs/standard/components/buttons)
101#[component]
102pub fn Button(
103    /// The style of the button. See [ButtonStyle].
104    #[prop(into)]
105    style: MaybeSignal<ButtonStyle>,
106    /// Whether the button is rounded.
107    #[prop(into, default = false.into())]
108    rounded: MaybeSignal<bool>,
109    /// Whether the button is disabled.
110    #[prop(into, default = false.into())]
111    disabled: MaybeSignal<bool>,
112    /// Whether to add a ripple effect to the button.
113    #[prop(into, default = None.into())]
114    ripple: MaybeSignal<Option<Ripple>>,
115    /// Elements displayed in the button
116    children: Children,
117) -> impl IntoView {
118    let classes = move || {
119        let mut classes = style().classes().to_string();
120        if rounded() {
121            classes.push_str(" rounded-full");
122        } else {
123            classes.push_str("rounded");
124        }
125        if disabled() {
126            classes.push_str(" pointer-events-none disabled:opacity-70")
127        }
128        classes
129    };
130
131    let button_ref: NodeRef<Button> = create_node_ref();
132    // TODO This explicit initialization is a workaround for https://github.com/mdbootstrap/Tailwind-Elements/issues/1743
133    create_effect(move |_| {
134        if let Some(element) = button_ref() {
135            let jsbutton = JsButton::new(&element);
136            on_cleanup(move || jsbutton.dispose());
137        }
138    });
139
140    Ripple::apply(button_ref, ripple);
141
142    view! {
143        <button
144            ref=button_ref
145            type="button"
146            class=classes
147            disabled=disabled
148        >
149            {children()}
150        </button>
151    }
152}
153
154#[wasm_bindgen]
155extern "C" {
156    #[wasm_bindgen(js_namespace = te, js_name = Button)]
157    type JsButton;
158
159    #[wasm_bindgen(constructor, js_namespace = te, js_class = Button, final)]
160    fn new(e: &HtmlButtonElement) -> JsButton;
161
162    #[wasm_bindgen(method, js_namespace = te, js_class = Button, final)]
163    fn dispose(this: &JsButton);
164}