1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use std::{fmt::Display, str::FromStr};

use crate::prelude::*;
use num_traits::PrimInt;
use yew::prelude::*;

/// Position of the number input unit in relation to the number input.
#[derive(Debug, Clone, PartialEq)]
pub enum NumberInputUnit {
    Before(Html),
    After(Html),
}

#[derive(Debug, Clone, PartialEq, Properties)]
pub struct NumberInputProperties<T: PrimInt + Display + FromStr + 'static> {
    /// Value of the number input.
    #[prop_or(T::zero())]
    pub value: T,
    /// Additional classes added to the number input.
    #[prop_or_default]
    pub class: Classes,
    /// Sets the width of the number input to a number of characters
    #[prop_or_default]
    pub width_chars: Option<u8>,
    /// Indicates whether the whole number input should be disabled.
    #[prop_or_default]
    pub disabled: bool,
    /// Callback for the minus button.
    #[prop_or_default]
    pub onminus: Option<Callback<()>>,
    /// Callback the text input changing.
    #[prop_or_default]
    pub onchange: Option<Callback<T>>,
    /// Callback for the plus button.
    #[prop_or_default]
    pub onplus: Option<Callback<()>>,
    /// Adds the given unit to the number input.
    #[prop_or_default]
    pub unit: Option<NumberInputUnit>,
    /// Minimum value of the number input, disabling the minus button when reached.
    #[prop_or(T::min_value())]
    pub min: T,
    /// Maximum value of the number input, disabling the plus button when reached.
    #[prop_or(T::max_value())]
    pub max: T,
    /// Value to indicate if the input is modified to show the validiation state.
    #[prop_or_default]
    pub state: InputState,
    /// Name of the input.
    #[prop_or_default]
    pub input_name: Option<String>,
    /// Aria label of the minus button.
    #[prop_or(AttrValue::from("Minus"))]
    pub minus_button_aria_label: AttrValue,
    /// Aria label of the plus button.
    #[prop_or(AttrValue::from("Plus"))]
    pub plus_button_aria_label: AttrValue,
}

#[function_component(NumberInput)]
pub fn number_input<T: PrimInt + Display + FromStr + 'static>(
    props: &NumberInputProperties<T>,
) -> Html {
    let mut class = props.class.clone();
    class.push("pf-v5-c-number-input");
    if props.state != InputState::Default {
        class.push("pf-m-status");
    }
    let width_style_name = "--pf-v5-c-number-input--c-form-control--width-chars";
    let style = props
        .width_chars
        .map(|w| format!("{width_style_name}:{w};"));

    let onminusclick = use_callback(props.onminus.clone(), |_, onminus| {
        if let Some(onminus) = onminus {
            onminus.emit(());
        }
    });
    let onplusclick = use_callback(props.onplus.clone(), |_, onplus| {
        if let Some(onplus) = onplus {
            onplus.emit(());
        }
    });
    let onchange = use_callback(props.onchange.clone(), |new_val: String, onchange| {
        let Some(onchange) = onchange else {
            return;
        };
        match new_val.parse::<T>() {
            Ok(n) => onchange.emit(n),
            Err(_) => log::warn!("[NumberInput] Failed to parse {new_val} into a number."),
        };
    });
    html! {
        <div {class} {style}>
            if let Some(NumberInputUnit::Before(unit)) = &props.unit {
                <Unit>{unit.clone()}</Unit>
            }
            <InputGroup>
                <InputGroupItem>
                    <Button
                        variant={ButtonVariant::Control}
                        aria_label={props.minus_button_aria_label.clone()}
                        disabled={props.disabled || props.value <= props.min}
                        onclick={onminusclick}
                    >
                        <span class="pf-v5-c-number-input__icon">
                            {Icon::Minus}
                        </span>
                    </Button>
                </InputGroupItem>
                <InputGroupItem>
                    <TextInput
                        r#type={TextInputType::Number}
                        value={props.value.to_string()}
                        name={props.input_name.clone()}
                        disabled={props.disabled}
                        onchange={onchange}
                        state={props.state}
                    />
                </InputGroupItem>
                <InputGroupItem>
                    <Button
                        variant={ButtonVariant::Control}
                        aria_label={props.plus_button_aria_label.clone()}
                        disabled={props.disabled || props.value >= props.max}
                        onclick={onplusclick}
                    >
                        <span class="pf-v5-c-number-input__icon">
                            {Icon::Plus}
                        </span>
                    </Button>
                </InputGroupItem>
            </InputGroup>
            if let Some(NumberInputUnit::After(unit)) = &props.unit {
                <Unit>{unit.clone()}</Unit>
            }
        </div>
    }
}

#[derive(Debug, Clone, PartialEq, Properties)]
struct UnitProperties {
    children: Html,
}

#[function_component(Unit)]
fn unit(props: &UnitProperties) -> Html {
    html!(<div class="pf-v5-c-number-input__unit">{props.children.clone()}</div>)
}