patternfly_yew/components/
number_input.rs

1use std::{fmt::Display, str::FromStr};
2
3use crate::prelude::*;
4use num_traits::PrimInt;
5use yew::prelude::*;
6
7/// Position of the number input unit in relation to the number input.
8#[derive(Debug, Clone, PartialEq)]
9pub enum NumberInputUnit {
10    Before(Html),
11    After(Html),
12}
13
14#[derive(Debug, Clone, PartialEq, Properties)]
15pub struct NumberInputProperties<T: PrimInt + Display + FromStr + 'static> {
16    /// Value of the number input.
17    #[prop_or(T::zero())]
18    pub value: T,
19    /// Additional classes added to the number input.
20    #[prop_or_default]
21    pub class: Classes,
22    /// Sets the width of the number input to a number of characters
23    #[prop_or_default]
24    pub width_chars: Option<u8>,
25    /// Indicates whether the whole number input should be disabled.
26    #[prop_or_default]
27    pub disabled: bool,
28    /// Callback for the minus button.
29    #[prop_or_default]
30    pub onminus: Option<Callback<()>>,
31    /// Callback the text input changing.
32    #[prop_or_default]
33    pub onchange: Option<Callback<T>>,
34    /// Callback for the plus button.
35    #[prop_or_default]
36    pub onplus: Option<Callback<()>>,
37    /// Adds the given unit to the number input.
38    #[prop_or_default]
39    pub unit: Option<NumberInputUnit>,
40    /// Minimum value of the number input, disabling the minus button when reached.
41    #[prop_or(T::min_value())]
42    pub min: T,
43    /// Maximum value of the number input, disabling the plus button when reached.
44    #[prop_or(T::max_value())]
45    pub max: T,
46    /// Value to indicate if the input is modified to show the validiation state.
47    #[prop_or_default]
48    pub state: InputState,
49    /// Name of the input.
50    #[prop_or_default]
51    pub input_name: Option<String>,
52    /// Aria label of the minus button.
53    #[prop_or(AttrValue::from("Minus"))]
54    pub minus_button_aria_label: AttrValue,
55    /// Aria label of the plus button.
56    #[prop_or(AttrValue::from("Plus"))]
57    pub plus_button_aria_label: AttrValue,
58}
59
60#[function_component(NumberInput)]
61pub fn number_input<T: PrimInt + Display + FromStr + 'static>(
62    props: &NumberInputProperties<T>,
63) -> Html {
64    let mut class = props.class.clone();
65    class.push("pf-v5-c-number-input");
66    if props.state != InputState::Default {
67        class.push("pf-m-status");
68    }
69    let width_style_name = "--pf-v5-c-number-input--c-form-control--width-chars";
70    let style = props
71        .width_chars
72        .map(|w| format!("{width_style_name}:{w};"));
73
74    let onminusclick = use_callback(props.onminus.clone(), |_, onminus| {
75        if let Some(onminus) = onminus {
76            onminus.emit(());
77        }
78    });
79    let onplusclick = use_callback(props.onplus.clone(), |_, onplus| {
80        if let Some(onplus) = onplus {
81            onplus.emit(());
82        }
83    });
84    let onchange = use_callback(props.onchange.clone(), |new_val: String, onchange| {
85        let Some(onchange) = onchange else {
86            return;
87        };
88        match new_val.parse::<T>() {
89            Ok(n) => onchange.emit(n),
90            Err(_) => log::warn!("[NumberInput] Failed to parse {new_val} into a number."),
91        };
92    });
93    html! {
94        <div {class} {style}>
95            if let Some(NumberInputUnit::Before(unit)) = &props.unit {
96                <Unit>{unit.clone()}</Unit>
97            }
98            <InputGroup>
99                <InputGroupItem>
100                    <Button
101                        variant={ButtonVariant::Control}
102                        aria_label={props.minus_button_aria_label.clone()}
103                        disabled={props.disabled || props.value <= props.min}
104                        onclick={onminusclick}
105                    >
106                        <span class="pf-v5-c-number-input__icon">
107                            {Icon::Minus}
108                        </span>
109                    </Button>
110                </InputGroupItem>
111                <InputGroupItem>
112                    <TextInput
113                        r#type={TextInputType::Number}
114                        value={props.value.to_string()}
115                        name={props.input_name.clone()}
116                        disabled={props.disabled}
117                        onchange={onchange}
118                        state={props.state}
119                    />
120                </InputGroupItem>
121                <InputGroupItem>
122                    <Button
123                        variant={ButtonVariant::Control}
124                        aria_label={props.plus_button_aria_label.clone()}
125                        disabled={props.disabled || props.value >= props.max}
126                        onclick={onplusclick}
127                    >
128                        <span class="pf-v5-c-number-input__icon">
129                            {Icon::Plus}
130                        </span>
131                    </Button>
132                </InputGroupItem>
133            </InputGroup>
134            if let Some(NumberInputUnit::After(unit)) = &props.unit {
135                <Unit>{unit.clone()}</Unit>
136            }
137        </div>
138    }
139}
140
141#[derive(Debug, Clone, PartialEq, Properties)]
142struct UnitProperties {
143    children: Html,
144}
145
146#[function_component(Unit)]
147fn unit(props: &UnitProperties) -> Html {
148    html!(<div class="pf-v5-c-number-input__unit">{props.children.clone()}</div>)
149}