patternfly_yew/components/form/
checkbox.rs

1use crate::ouia;
2use crate::prelude::OuiaComponentType;
3use crate::utils::OuiaSafe;
4use crate::{core::OptionalHtml, hooks::id::use_prop_id, utils::Ouia};
5use web_sys::HtmlInputElement;
6use yew::html::IntoPropValue;
7use yew::prelude::*;
8
9const OUIA: Ouia = ouia!("Checkbox");
10
11/// The state of a checkbox.
12///
13/// In addition to the obvious two states (checked and unchecked), a checkbox can also have an
14/// "indeterminate" state.
15///
16/// This enum helps to work with this tri-state value. A boolean can easily converted into the
17/// `CheckboxState`. When converting back to a boolean, only [`CheckboxState::Checked`] will turn
18/// into `true`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum CheckboxState {
21    Checked,
22    #[default]
23    Unchecked,
24    Indeterminate,
25}
26
27impl From<bool> for CheckboxState {
28    fn from(b: bool) -> Self {
29        match b {
30            true => Self::Checked,
31            false => Self::Unchecked,
32        }
33    }
34}
35
36impl From<CheckboxState> for bool {
37    fn from(value: CheckboxState) -> Self {
38        match value {
39            CheckboxState::Checked => true,
40            CheckboxState::Indeterminate | CheckboxState::Unchecked => false,
41        }
42    }
43}
44
45impl From<CheckboxState> for Option<bool> {
46    fn from(value: CheckboxState) -> Self {
47        match value {
48            CheckboxState::Checked => Some(true),
49            CheckboxState::Unchecked => Some(false),
50            CheckboxState::Indeterminate => None,
51        }
52    }
53}
54
55impl IntoPropValue<CheckboxState> for bool {
56    fn into_prop_value(self) -> CheckboxState {
57        self.into()
58    }
59}
60
61/// Properties for [`Checkbox`].
62#[derive(Debug, Clone, PartialEq, Properties)]
63pub struct CheckboxProperties {
64    /// Id of the checkbox
65    #[prop_or_default]
66    pub id: Option<String>,
67
68    /// The name of the input field
69    #[prop_or_default]
70    pub name: Option<AttrValue>,
71
72    /// Additional classes added to the checkbox.
73    #[prop_or_default]
74    pub class: Classes,
75
76    /// Additional classes added to the radio input.
77    #[prop_or_default]
78    pub input_class: Classes,
79
80    /// Flag to show if the checkbox selection is valid or invalid.
81    #[prop_or_default]
82    pub valid: bool,
83
84    /// Flag to show if the checkbox is disabled.
85    #[prop_or_default]
86    pub disabled: bool,
87
88    /// Flag to show if the checkbox is required.
89    #[prop_or_default]
90    pub required: bool,
91
92    /// Flag to show if the checkbox is checked.
93    #[prop_or_default]
94    pub checked: CheckboxState,
95
96    /// A callback for when the checkbox selection changes.
97    #[prop_or_default]
98    pub onchange: Callback<CheckboxState>,
99
100    /// Label text of the checkbox.
101    #[prop_or_default]
102    pub label: OptionalHtml,
103
104    /// Aria-label of the checkbox.
105    #[prop_or_default]
106    pub aria_label: AttrValue,
107
108    /// Description text of the checkbox.
109    #[prop_or_default]
110    pub description: OptionalHtml,
111
112    /// Body text of the checkbox.
113    #[prop_or_default]
114    pub body: Option<Html>,
115
116    /// Sets the input wrapper component to render.
117    #[prop_or(String::from("div"))]
118    pub component: String,
119
120    /// OUIA Component id
121    #[prop_or_default]
122    pub ouia_id: Option<String>,
123    /// OUIA Component Type
124    #[prop_or(OUIA.component_type())]
125    pub ouia_type: OuiaComponentType,
126    /// OUIA Component Safe
127    #[prop_or(OuiaSafe::TRUE)]
128    pub ouia_safe: OuiaSafe,
129}
130
131/// Checkbox component
132///
133/// > A **checkbox** is used to select a single item or multiple items, typically to choose elements to perform an action or to reflect a binary setting.
134///
135/// See: <https://www.patternfly.org/components/forms/checkbox>
136///
137/// ## Properties
138///
139/// Defined by [`FormGroupProperties`].
140#[function_component(Checkbox)]
141pub fn checkbox(props: &CheckboxProperties) -> Html {
142    let ouia_id = use_memo(props.ouia_id.clone(), |id| {
143        id.clone().unwrap_or(OUIA.generated_id())
144    });
145    let id = use_prop_id(props.id.clone());
146    let mut outer_class = classes!["pf-v5-c-check", props.class.clone()];
147
148    if props.label.is_none() {
149        outer_class.push("pf-m-standalone");
150    }
151
152    let node_ref = use_node_ref();
153
154    {
155        let node_ref = node_ref.clone();
156        let checked = props.checked;
157        use_effect(move || {
158            if let Some(elem) = node_ref.cast::<HtmlInputElement>() {
159                elem.set_indeterminate(checked == CheckboxState::Indeterminate)
160            }
161        });
162    }
163
164    let onchange = use_callback(
165        (props.onchange.clone(), node_ref.clone()),
166        |_: Event, (onchange, node_ref)| {
167            let checked = node_ref
168                .cast::<HtmlInputElement>()
169                .map(|input| input.checked().into())
170                .unwrap_or_default();
171            onchange.emit(checked);
172        },
173    );
174
175    let label = if let Some(label) = &props.label.0 {
176        let mut class = classes!["pf-v5-c-check__label"];
177        if props.disabled {
178            class.push("pf-m-disabled");
179        }
180        html! (
181            <label {class} for={(*id).clone()}>
182                {label.clone()}
183                if props.required {
184                    <span class="pf-v5-c-check__label-required" aria-hidden="true">{"*"}</span>
185                }
186            </label>
187        )
188    } else {
189        html!()
190    };
191
192    html! (
193        <@{props.component.clone()} class={outer_class}>
194            <input
195                class={classes!["pf-v5-c-check__input", props.input_class.clone()]}
196                type="checkbox"
197                {onchange}
198                aria-invalid={props.valid.to_string()}
199                aria-label={props.aria_label.clone()}
200                disabled={props.disabled}
201                required={props.required}
202                id={(*id).clone()}
203                ref={node_ref.clone()}
204                checked={props.checked != CheckboxState::Unchecked}
205                data-ouia-component-id={(*ouia_id).clone()}
206                data-ouia-component-type={props.ouia_type}
207                data-ouia-safe={props.ouia_safe}
208            />
209            {label}
210            if let Some(description) = &props.description.0 {
211                <span class="pf-v5-c-check__description">{description.clone()}</span>
212            }
213            if let Some(body) = &props.body {
214                <span class="pf-v5-c-check__body">{body.clone()}</span>
215            }
216        </@>
217    )
218}