Skip to main content

patternfly_yew/components/button/
mod.rs

1//! Button
2
3use crate::ouia;
4use crate::prelude::{AsClasses, Spinner, SpinnerSize};
5use crate::utils::{Ouia, OuiaComponentType, OuiaSafe};
6use web_sys::HtmlElement;
7use yew::html::IntoPropValue;
8use yew::prelude::*;
9use yew::virtual_dom::AttrValue;
10
11const OUIA: Ouia = ouia!("Button");
12
13#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
14pub enum ButtonVariant {
15    #[default]
16    None,
17    Primary,
18    Secondary,
19    Tertiary,
20    Warning,
21    Danger,
22    DangerSecondary,
23    Link,
24    InlineLink,
25    Control,
26    Plain,
27}
28
29impl ButtonVariant {
30    pub fn as_classes(&self) -> Vec<&'static str> {
31        match self {
32            Self::None => vec![],
33            Self::Primary => vec!["pf-m-primary"],
34            Self::Secondary => vec!["pf-m-secondary"],
35            Self::Tertiary => vec!["pf-m-tertiary"],
36            Self::Warning => vec!["pf-m-warning"],
37            Self::Danger => vec!["pf-m-danger"],
38            Self::DangerSecondary => vec!["pf-m-danger", "pf-m-secondary"],
39            Self::Link => vec!["pf-m-link"],
40            Self::InlineLink => vec!["pf-m-link", "pf-m-inline"],
41            Self::Control => vec!["pf-m-control"],
42            Self::Plain => vec!["pf-m-plain"],
43        }
44    }
45}
46
47#[derive(Clone, Copy, Default, Eq, PartialEq, Debug)]
48pub enum Align {
49    #[default]
50    Start,
51    End,
52}
53
54/// Button Type.
55///
56/// See: <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type>
57#[derive(Clone, Copy, Default, Eq, PartialEq, Debug)]
58pub enum ButtonType {
59    #[default]
60    Button,
61    Reset,
62    Submit,
63}
64
65impl IntoPropValue<Option<AttrValue>> for ButtonType {
66    fn into_prop_value(self) -> Option<AttrValue> {
67        Some(AttrValue::Static(match self {
68            Self::Submit => "submit",
69            Self::Reset => "reset",
70            Self::Button => "button",
71        }))
72    }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76pub enum ButtonSize {
77    Small,
78    #[default]
79    Medium,
80    Large,
81}
82
83impl AsClasses for ButtonSize {
84    fn extend_classes(&self, classes: &mut Classes) {
85        match self {
86            ButtonSize::Small => classes.push("pf-m-small"),
87            ButtonSize::Medium => (),
88            ButtonSize::Large => classes.push("pf-m-display-lg"),
89        };
90    }
91}
92
93/// Properties for [`Button`]
94#[derive(Clone, PartialEq, Properties)]
95pub struct ButtonProperties {
96    #[prop_or_default]
97    pub id: Option<AttrValue>,
98    #[prop_or_default]
99    pub class: Classes,
100    #[prop_or_default]
101    pub style: Option<AttrValue>,
102
103    #[prop_or_default]
104    pub label: String,
105    #[prop_or_default]
106    pub onclick: Callback<MouseEvent>,
107    #[prop_or_default]
108    pub variant: ButtonVariant,
109    #[prop_or_default]
110    pub icon: Option<Html>,
111    #[prop_or_default]
112    pub align: Align,
113
114    #[prop_or_default]
115    pub disabled: bool,
116
117    #[prop_or_default]
118    pub block: bool,
119
120    #[prop_or_default]
121    pub loading: bool,
122
123    #[prop_or_default]
124    pub aria_label: Option<AttrValue>,
125    #[prop_or_default]
126    pub aria_labelledby: Option<AttrValue>,
127    #[prop_or_default]
128    pub aria_haspopup: Option<AttrValue>,
129    #[prop_or_default]
130    pub aria_expanded: Option<AttrValue>,
131    #[prop_or_default]
132    pub aria_controls: Option<AttrValue>,
133
134    #[prop_or_default]
135    pub r#type: ButtonType,
136
137    #[prop_or_default]
138    pub form: Option<AttrValue>,
139    #[prop_or_default]
140    pub formaction: Option<AttrValue>,
141
142    #[prop_or_default]
143    pub role: Option<AttrValue>,
144
145    #[prop_or_default]
146    pub expanded: bool,
147
148    #[prop_or_default]
149    pub children: Html,
150
151    #[prop_or_default]
152    pub tabindex: Option<isize>,
153
154    #[prop_or_default]
155    pub r#ref: Option<NodeRef>,
156
157    #[prop_or_default]
158    pub size: ButtonSize,
159
160    /// OUIA Component id
161    #[prop_or_default]
162    pub ouia_id: Option<String>,
163    /// OUIA Component Type
164    #[prop_or(OUIA.component_type())]
165    pub ouia_type: OuiaComponentType,
166    /// OUIA Component Safe
167    #[prop_or(OuiaSafe::TRUE)]
168    pub ouia_safe: OuiaSafe,
169}
170
171/// Button component
172///
173/// > A **button** is a box area or text that communicates and triggers user actions when clicked or selected. Buttons can be used to communicate and immediately trigger actions a user can take in an application, like submitting a form, canceling a process, or creating a new object. Buttons can also be used to take a user to a new location, like another page inside of a web application, or an external site such as help or documentation.
174///
175/// See: <https://www.patternfly.org/components/button>
176///
177/// ## Properties
178///
179/// Defined by [`ButtonProperties`].
180#[function_component(Button)]
181pub fn button(props: &ButtonProperties) -> Html {
182    let ouia_id = use_memo(props.ouia_id.clone(), |id| {
183        id.clone().unwrap_or(OUIA.generated_id())
184    });
185    let node_ref = use_node_ref();
186    let node_ref = props.r#ref.as_ref().unwrap_or(&node_ref);
187
188    let mut classes: Classes = classes!(
189        "pf-v6-c-button",
190        props.class.clone(),
191        props.variant.as_classes(),
192        props.size.as_classes(),
193    );
194
195    if props.expanded {
196        classes.push("pf-m-expanded");
197    }
198    if props.block {
199        classes.push("pf-m-block");
200    }
201    if props.loading {
202        classes.push("pf-m-progress pf-m-in-progress")
203    }
204
205    let has_text = !props.label.is_empty() || props.children != Html::default();
206
207    let icon = match &props.icon {
208        Some(icon) => {
209            let mut classes = Classes::from("pf-v6-c-button__icon");
210
211            // only align the icon if there is text to align it to
212            if has_text {
213                match props.align {
214                    Align::Start => classes.push("pf-m-start"),
215                    Align::End => classes.push("pf-m-end"),
216                }
217            }
218
219            html!(<span class={classes}>{ icon.clone() }</span>)
220        }
221        None => html!(),
222    };
223
224    let text = match has_text {
225        true => html!(
226            <span class="pf-v6-c-button__text">
227                { &props.label }
228                { props.children.clone() }
229            </span>
230        ),
231        false => html!(),
232    };
233
234    let content = match props.align {
235        Align::Start => html!(<>{ icon }{ text }</>),
236        Align::End => html!(<>{ text }{ icon }</>),
237    };
238
239    let onclick = {
240        let onclick = props.onclick.clone();
241        let node_ref = node_ref.clone();
242        Callback::from(move |evt| {
243            // Blur (loose focus) on the button element, to remove the focus after clicking
244            if let Some(node) = node_ref.cast::<HtmlElement>() {
245                node.blur().ok();
246            }
247            onclick.emit(evt);
248        })
249    };
250    let tabindex: Option<AttrValue> = props.tabindex.map(|i| i.to_string().into());
251
252    html! (
253        <button
254            ref={node_ref}
255            id={props.id.clone()}
256            class={classes}
257            style={props.style.clone()}
258            disabled={props.disabled}
259            type={props.r#type}
260            {onclick}
261            role={props.role.clone()}
262            form={props.form.clone()}
263            formaction={props.formaction.clone()}
264            aria-label={props.aria_label.clone()}
265            aria-labelledby={&props.aria_labelledby}
266            aria-haspopup={&props.aria_haspopup}
267            aria-expanded={&props.aria_expanded}
268            aria-controls={&props.aria_controls}
269            {tabindex}
270            data-ouia-component-id={(*ouia_id).clone()}
271            data-ouia-component-type={props.ouia_type}
272            data-ouia-safe={props.ouia_safe}
273        >
274            if props.loading {
275                <span class="pf-v6-c-button__progress">
276                    <Spinner size={SpinnerSize::Md} />
277                </span>
278            }
279            { content }
280        </button>
281    )
282}