patternfly_yew/components/
number_input.rs1use std::{fmt::Display, str::FromStr};
2
3use crate::prelude::*;
4use num_traits::PrimInt;
5use yew::prelude::*;
6
7#[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 #[prop_or(T::zero())]
18 pub value: T,
19 #[prop_or_default]
21 pub class: Classes,
22 #[prop_or_default]
24 pub width_chars: Option<u8>,
25 #[prop_or_default]
27 pub disabled: bool,
28 #[prop_or_default]
30 pub onminus: Option<Callback<()>>,
31 #[prop_or_default]
33 pub onchange: Option<Callback<T>>,
34 #[prop_or_default]
36 pub onplus: Option<Callback<()>>,
37 #[prop_or_default]
39 pub unit: Option<NumberInputUnit>,
40 #[prop_or(T::min_value())]
42 pub min: T,
43 #[prop_or(T::max_value())]
45 pub max: T,
46 #[prop_or_default]
48 pub state: InputState,
49 #[prop_or_default]
51 pub input_name: Option<String>,
52 #[prop_or(AttrValue::from("Minus"))]
54 pub minus_button_aria_label: AttrValue,
55 #[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}