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-v6-c-number-input");
66 if props.state != InputState::Default {
67 class.push("pf-m-status");
68 }
69 let width_style_name = "--pf-v6-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-v6-c-number-input__icon">{ Icon::Minus }</span>
107 </Button>
108 </InputGroupItem>
109 <InputGroupItem>
110 <TextInput
111 r#type={TextInputType::Number}
112 value={props.value.to_string()}
113 name={props.input_name.clone()}
114 disabled={props.disabled}
115 onchange={onchange}
116 state={props.state}
117 />
118 </InputGroupItem>
119 <InputGroupItem>
120 <Button
121 variant={ButtonVariant::Control}
122 aria_label={props.plus_button_aria_label.clone()}
123 disabled={props.disabled || props.value >= props.max}
124 onclick={onplusclick}
125 >
126 <span class="pf-v6-c-number-input__icon">{ Icon::Plus }</span>
127 </Button>
128 </InputGroupItem>
129 </InputGroup>
130 if let Some(NumberInputUnit::After(unit)) = &props.unit {
131 <Unit>{ unit.clone() }</Unit>
132 }
133 </div>
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Properties)]
138struct UnitProperties {
139 children: Html,
140}
141
142#[function_component(Unit)]
143fn unit(props: &UnitProperties) -> Html {
144 html!(<div class="pf-v6-c-number-input__unit">{ props.children.clone() }</div>)
145}