1use crate::components::config_provider::{ComponentSize, use_config};
11use crate::components::control::{ControlStatus, push_status_class};
12use crate::components::form::use_form_item_control;
13use crate::components::form::{FormItemControlContext, form_value_to_string};
14use crate::components::icon::{Icon, IconKind};
15use crate::foundation::{
16 ClassListExt, InputClassNames, InputSemantic, InputStyles, StyleStringExt, Variant,
17 variant_from_bordered,
18};
19use dioxus::events::KeyboardEvent;
20use dioxus::prelude::Key;
21use dioxus::prelude::*;
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
25pub enum InputSize {
26 Small,
27 #[default]
28 Middle,
29 Large,
30}
31
32impl InputSize {
33 fn from_global(size: ComponentSize) -> Self {
34 match size {
35 ComponentSize::Small => InputSize::Small,
36 ComponentSize::Large => InputSize::Large,
37 ComponentSize::Middle => InputSize::Middle,
38 }
39 }
40
41 fn as_class(&self) -> &'static str {
42 match self {
43 InputSize::Small => "adui-input-sm",
44 InputSize::Middle => "",
45 InputSize::Large => "adui-input-lg",
46 }
47 }
48}
49
50#[derive(Props, Clone, PartialEq)]
52pub struct InputProps {
53 #[props(optional)]
55 pub value: Option<String>,
56 #[props(optional)]
58 pub default_value: Option<String>,
59 #[props(optional)]
60 pub placeholder: Option<String>,
61 #[props(default)]
62 pub disabled: bool,
63 #[props(optional)]
65 pub size: Option<InputSize>,
66 #[props(optional)]
68 pub variant: Option<Variant>,
69 #[props(optional)]
71 pub bordered: Option<bool>,
72 #[props(optional)]
74 pub status: Option<ControlStatus>,
75 #[props(optional)]
77 pub prefix: Option<Element>,
78 #[props(optional)]
80 pub suffix: Option<Element>,
81 #[props(optional)]
83 pub addon_before: Option<Element>,
84 #[props(optional)]
86 pub addon_after: Option<Element>,
87 #[props(default)]
89 pub allow_clear: bool,
90 #[props(optional)]
92 pub max_length: Option<usize>,
93 #[props(default)]
95 pub show_count: bool,
96 #[props(optional)]
97 pub class: Option<String>,
98 #[props(optional)]
100 pub root_class_name: Option<String>,
101 #[props(optional)]
102 pub style: Option<String>,
103 #[props(optional)]
105 pub class_names: Option<InputClassNames>,
106 #[props(optional)]
108 pub styles: Option<InputStyles>,
109 #[props(optional)]
111 pub on_change: Option<EventHandler<String>>,
112 #[props(optional)]
114 pub on_press_enter: Option<EventHandler<()>>,
115 #[props(optional)]
118 pub data_attributes: Option<Vec<(String, String)>>,
119}
120
121#[component]
123pub fn Input(props: InputProps) -> Element {
124 let InputProps {
125 value,
126 default_value,
127 placeholder,
128 disabled,
129 size,
130 variant,
131 bordered,
132 status,
133 prefix,
134 suffix,
135 addon_before,
136 addon_after,
137 allow_clear,
138 max_length,
139 show_count,
140 class,
141 root_class_name,
142 style,
143 class_names,
144 styles,
145 on_change,
146 on_press_enter,
147 data_attributes,
148 } = props;
149
150 let input_id = use_signal(|| format!("adui-input-{}", rand_id()));
152
153 #[cfg(target_arch = "wasm32")]
155 {
156 if let Some(data_attrs) = data_attributes.as_ref() {
157 let id = input_id.read().clone();
158 let attrs = data_attrs.clone();
159 {
160 use_effect(move || {
161 use wasm_bindgen::JsCast;
162 if let Some(window) = web_sys::window() {
163 if let Some(document) = window.document() {
164 if let Some(element) = document.get_element_by_id(&id) {
165 for (key, value) in attrs.iter() {
166 let attr_name = format!("data-{}", key);
167 let _ = element.set_attribute(&attr_name, value);
168 }
169 }
170 }
171 }
172 });
173 }
174 }
175 }
176 #[cfg(not(target_arch = "wasm32"))]
177 {
178 let _ = data_attributes;
180 }
181
182 let placeholder_str = placeholder.unwrap_or_default();
183 let config = use_config();
184 let form_control = use_form_item_control();
185 let controlled_by_prop = value.is_some();
186
187 let resolved_size = size.unwrap_or_else(|| InputSize::from_global(config.size));
189
190 let resolved_variant = variant_from_bordered(bordered, variant);
192
193 let initial_inner = default_value.clone().unwrap_or_default();
195 let inner_value = use_signal(|| initial_inner);
196
197 let current_value = resolve_current_value(&form_control, value.clone(), inner_value);
198 let is_disabled =
199 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
200
201 let has_prefix = prefix.is_some();
202 let has_user_suffix = suffix.is_some();
203 let has_clear = allow_clear && !current_value.is_empty() && !is_disabled;
204 let has_any_suffix = has_user_suffix || has_clear || show_count;
205 let has_addon_before = addon_before.is_some();
206 let has_addon_after = addon_after.is_some();
207 let has_addon = has_addon_before || has_addon_after;
208
209 let control_for_change = form_control.clone();
211 let on_change_cb = on_change;
212 let on_press_enter_cb = on_press_enter;
213 let controlled_flag = controlled_by_prop;
214 let mut inner_for_change = inner_value;
215
216 let mut input_class_list = vec!["adui-input".to_string()];
218 input_class_list.push_semantic(&class_names, InputSemantic::Input);
219
220 let input_class_attr = input_class_list.join(" ");
221
222 let mut input_style = String::new();
224 input_style.append_semantic(&styles, InputSemantic::Input);
225
226 let char_count = current_value.chars().count();
228 let count_text = if let Some(max) = max_length {
229 format!("{}/{}", char_count, max)
230 } else {
231 char_count.to_string()
232 };
233
234 let input_id_val = input_id.read().clone();
235 let input_node = {
236 let max_len_attr = max_length.map(|m| m.to_string());
237 rsx! {
238 input {
239 id: "{input_id_val}",
240 class: "{input_class_attr}",
241 style: "{input_style}",
242 disabled: is_disabled,
243 value: "{current_value}",
244 placeholder: "{placeholder_str}",
245 maxlength: max_len_attr,
246 oninput: move |evt| {
247 let next = evt.value();
248 apply_input_value(
249 next,
250 &control_for_change,
251 controlled_flag,
252 &mut inner_for_change,
253 on_change_cb,
254 );
255 },
256 onkeydown: move |evt: KeyboardEvent| {
257 if matches!(evt.key(), Key::Enter)
258 && let Some(cb) = on_press_enter_cb
259 {
260 cb.call(());
261 }
262 }
263 }
264 }
265 };
266
267 let build_wrapper_classes = |extra_class: &str| {
269 let mut classes = vec!["adui-input-affix-wrapper".to_string()];
270 classes.push(resolved_size.as_class().to_string());
271 classes.push(resolved_variant.class_for("adui-input"));
272 if is_disabled {
273 classes.push("adui-input-disabled".into());
274 }
275 push_status_class(&mut classes, status);
276 if !extra_class.is_empty() {
277 classes.push(extra_class.to_string());
278 }
279 classes.push_semantic(&class_names, InputSemantic::Root);
280 if let Some(extra) = class.clone() {
281 classes.push(extra);
282 }
283 if let Some(extra) = root_class_name.clone() {
284 classes.push(extra);
285 }
286 classes
287 .into_iter()
288 .filter(|s| !s.is_empty())
289 .collect::<Vec<_>>()
290 .join(" ")
291 };
292
293 let build_wrapper_style = || {
294 let mut s = style.clone().unwrap_or_default();
295 s.append_semantic(&styles, InputSemantic::Root);
296 s
297 };
298
299 if has_addon {
301 let wrapper_class = build_wrapper_classes("");
302 let wrapper_style = build_wrapper_style();
303
304 let control_for_clear = form_control;
305 let mut inner_for_clear = inner_value;
306 let on_change_for_clear = on_change_cb;
307
308 rsx! {
309 div { class: "adui-input-group adui-input-group-wrapper",
310 if let Some(before) = addon_before {
311 span { class: "adui-input-group-addon", {before} }
312 }
313 div {
314 class: "{wrapper_class}",
315 style: "{wrapper_style}",
316 if let Some(icon) = prefix {
317 span { class: "adui-input-prefix", {icon} }
318 }
319 {input_node}
320 if has_any_suffix {
321 span {
322 class: "adui-input-suffix",
323 if let Some(icon) = suffix {
324 {icon}
325 }
326 if has_clear {
327 span {
328 class: "adui-input-clear",
329 onclick: move |_| {
330 apply_input_value(
331 String::new(),
332 &control_for_clear,
333 controlled_flag,
334 &mut inner_for_clear,
335 on_change_for_clear,
336 );
337 },
338 "×"
339 }
340 }
341 if show_count {
342 span { class: "adui-input-count", "{count_text}" }
343 }
344 }
345 }
346 }
347 if let Some(after) = addon_after {
348 span { class: "adui-input-group-addon", {after} }
349 }
350 }
351 }
352 } else if has_prefix || has_any_suffix {
353 let wrapper_class = build_wrapper_classes("");
355 let wrapper_style = build_wrapper_style();
356
357 let control_for_clear = form_control;
358 let mut inner_for_clear = inner_value;
359 let on_change_for_clear = on_change_cb;
360
361 rsx! {
362 div {
363 class: "{wrapper_class}",
364 style: "{wrapper_style}",
365 if let Some(icon) = prefix {
366 span { class: "adui-input-prefix", {icon} }
367 }
368 {input_node}
369 if has_any_suffix {
370 span {
371 class: "adui-input-suffix",
372 if let Some(icon) = suffix {
373 {icon}
374 }
375 if has_clear {
376 span {
377 class: "adui-input-clear",
378 onclick: move |_| {
379 apply_input_value(
380 String::new(),
381 &control_for_clear,
382 controlled_flag,
383 &mut inner_for_clear,
384 on_change_for_clear,
385 );
386 },
387 "×"
388 }
389 }
390 if show_count {
391 span { class: "adui-input-count", "{count_text}" }
392 }
393 }
394 }
395 }
396 }
397 } else {
398 let mut class_list = vec!["adui-input".to_string()];
400 class_list.push(resolved_size.as_class().to_string());
401 class_list.push(resolved_variant.class_for("adui-input"));
402 if is_disabled {
403 class_list.push("adui-input-disabled".into());
404 }
405 push_status_class(&mut class_list, status);
406 class_list.push_semantic(&class_names, InputSemantic::Root);
407 if let Some(extra) = class {
408 class_list.push(extra);
409 }
410 if let Some(extra) = root_class_name {
411 class_list.push(extra);
412 }
413 let class_attr = class_list
414 .into_iter()
415 .filter(|s| !s.is_empty())
416 .collect::<Vec<_>>()
417 .join(" ");
418 let style_attr = build_wrapper_style();
419
420 let max_len_attr = max_length.map(|m| m.to_string());
421 let input_id_val = input_id.read().clone();
422
423 rsx! {
424 input {
425 id: "{input_id_val}",
426 class: "{class_attr}",
427 style: "{style_attr}",
428 disabled: is_disabled,
429 value: "{current_value}",
430 placeholder: "{placeholder_str}",
431 maxlength: max_len_attr,
432 oninput: move |evt| {
433 let next = evt.value();
434 apply_input_value(
435 next,
436 &form_control,
437 controlled_flag,
438 &mut inner_for_change,
439 on_change_cb,
440 );
441 },
442 onkeydown: move |evt: KeyboardEvent| {
443 if matches!(evt.key(), Key::Enter)
444 && let Some(cb) = on_press_enter
445 {
446 cb.call(());
447 }
448 }
449 }
450 }
451 }
452}
453
454#[derive(Props, Clone, PartialEq)]
460pub struct PasswordProps {
461 #[props(optional)]
462 pub value: Option<String>,
463 #[props(optional)]
464 pub default_value: Option<String>,
465 #[props(optional)]
466 pub placeholder: Option<String>,
467 #[props(default)]
468 pub disabled: bool,
469 #[props(optional)]
470 pub size: Option<InputSize>,
471 #[props(optional)]
472 pub variant: Option<Variant>,
473 #[props(optional)]
474 pub status: Option<ControlStatus>,
475 #[props(optional)]
476 pub prefix: Option<Element>,
477 #[props(default)]
479 pub visible: bool,
480 #[props(optional)]
482 pub icon_render: Option<Element>,
483 #[props(optional)]
484 pub class: Option<String>,
485 #[props(optional)]
486 pub style: Option<String>,
487 #[props(optional)]
488 pub class_names: Option<InputClassNames>,
489 #[props(optional)]
490 pub styles: Option<InputStyles>,
491 #[props(optional)]
492 pub on_change: Option<EventHandler<String>>,
493 #[props(optional)]
494 pub on_press_enter: Option<EventHandler<()>>,
495 #[props(optional)]
497 pub on_visible_change: Option<EventHandler<bool>>,
498}
499
500#[component]
502pub fn Password(props: PasswordProps) -> Element {
503 let PasswordProps {
504 value,
505 default_value,
506 placeholder,
507 disabled,
508 size,
509 variant,
510 status,
511 prefix,
512 visible: initial_visible,
513 icon_render,
514 class,
515 style,
516 class_names,
517 styles,
518 on_change,
519 on_press_enter,
520 on_visible_change,
521 } = props;
522
523 let visible_signal = use_signal(|| initial_visible);
524 let is_visible = *visible_signal.read();
525
526 let visibility_icon = icon_render.unwrap_or_else(|| {
527 if is_visible {
528 rsx! { Icon { kind: IconKind::Eye } }
529 } else {
530 rsx! { Icon { kind: IconKind::EyeInvisible } }
531 }
532 });
533
534 let on_visible_cb = on_visible_change;
535
536 let suffix = rsx! {
537 span {
538 class: "adui-input-password-icon",
539 style: "cursor: pointer;",
540 onclick: move |_| {
541 let mut sig = visible_signal;
542 let next = !*sig.read();
543 sig.set(next);
544 if let Some(cb) = on_visible_cb {
545 cb.call(next);
546 }
547 },
548 {visibility_icon}
549 }
550 };
551
552 rsx! {
553 InputInternal {
554 value: value,
555 default_value: default_value,
556 placeholder: placeholder,
557 disabled: disabled,
558 size: size,
559 variant: variant,
560 status: status,
561 prefix: prefix,
562 suffix: Some(suffix),
563 is_password: !is_visible,
564 class: class,
565 style: style,
566 class_names: class_names,
567 styles: styles,
568 on_change: on_change,
569 on_press_enter: on_press_enter,
570 extra_class: Some("adui-input-password".to_string()),
571 }
572 }
573}
574
575#[derive(Props, Clone, PartialEq)]
581pub struct SearchProps {
582 #[props(optional)]
583 pub value: Option<String>,
584 #[props(optional)]
585 pub default_value: Option<String>,
586 #[props(optional)]
587 pub placeholder: Option<String>,
588 #[props(default)]
589 pub disabled: bool,
590 #[props(optional)]
591 pub size: Option<InputSize>,
592 #[props(optional)]
593 pub variant: Option<Variant>,
594 #[props(optional)]
595 pub status: Option<ControlStatus>,
596 #[props(optional)]
597 pub prefix: Option<Element>,
598 #[props(default = true)]
600 pub enter_button: bool,
601 #[props(optional)]
603 pub enter_button_content: Option<Element>,
604 #[props(default)]
606 pub loading: bool,
607 #[props(optional)]
608 pub class: Option<String>,
609 #[props(optional)]
610 pub style: Option<String>,
611 #[props(optional)]
612 pub class_names: Option<InputClassNames>,
613 #[props(optional)]
614 pub styles: Option<InputStyles>,
615 #[props(optional)]
616 pub on_change: Option<EventHandler<String>>,
617 #[props(optional)]
619 pub on_search: Option<EventHandler<String>>,
620}
621
622#[component]
624pub fn Search(props: SearchProps) -> Element {
625 let SearchProps {
626 value,
627 default_value,
628 placeholder,
629 disabled,
630 size,
631 variant,
632 status,
633 prefix,
634 enter_button,
635 enter_button_content,
636 loading,
637 class,
638 style,
639 class_names,
640 styles,
641 on_change,
642 on_search,
643 } = props;
644
645 let config = use_config();
646 let form_control = use_form_item_control();
647 let controlled_by_prop = value.is_some();
648
649 let initial_inner = default_value.clone().unwrap_or_default();
650 let inner_value = use_signal(|| initial_inner);
651
652 let current_value = resolve_current_value(&form_control, value.clone(), inner_value);
653 let is_disabled =
654 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
655
656 let resolved_size = size.unwrap_or_else(|| InputSize::from_global(config.size));
657 let resolved_variant = variant.unwrap_or(Variant::Outlined);
658
659 let control_for_change = form_control.clone();
660 let on_change_cb = on_change;
661 let mut inner_for_change = inner_value;
662 let on_search_cb = on_search;
663
664 let mut wrapper_classes = vec![
666 "adui-input-search".to_string(),
667 "adui-input-affix-wrapper".to_string(),
668 ];
669 wrapper_classes.push(resolved_size.as_class().to_string());
670 wrapper_classes.push(resolved_variant.class_for("adui-input"));
671 if is_disabled {
672 wrapper_classes.push("adui-input-disabled".into());
673 }
674 if enter_button {
675 wrapper_classes.push("adui-input-search-with-button".into());
676 }
677 push_status_class(&mut wrapper_classes, status);
678 wrapper_classes.push_semantic(&class_names, InputSemantic::Root);
679 if let Some(extra) = class {
680 wrapper_classes.push(extra);
681 }
682 let wrapper_class = wrapper_classes
683 .into_iter()
684 .filter(|s| !s.is_empty())
685 .collect::<Vec<_>>()
686 .join(" ");
687 let mut wrapper_style = style.unwrap_or_default();
688 wrapper_style.append_semantic(&styles, InputSemantic::Root);
689
690 let search_icon = if loading {
691 rsx! { Icon { kind: IconKind::Loading, spin: true } }
692 } else {
693 rsx! { Icon { kind: IconKind::Search } }
694 };
695
696 let search_button_content = enter_button_content.unwrap_or(search_icon);
697
698 let value_for_keydown = current_value.clone();
700 let value_for_button = current_value.clone();
701 let value_for_icon = current_value.clone();
702
703 rsx! {
704 div {
705 class: "{wrapper_class}",
706 style: "{wrapper_style}",
707 if let Some(icon) = prefix {
708 span { class: "adui-input-prefix", {icon} }
709 }
710 input {
711 class: "adui-input",
712 disabled: is_disabled,
713 value: "{current_value}",
714 placeholder: placeholder.unwrap_or_default(),
715 oninput: move |evt| {
716 let next = evt.value();
717 apply_input_value(
718 next,
719 &control_for_change,
720 controlled_by_prop,
721 &mut inner_for_change,
722 on_change_cb,
723 );
724 },
725 onkeydown: move |evt: KeyboardEvent| {
726 if matches!(evt.key(), Key::Enter) {
727 if let Some(cb) = on_search_cb {
728 cb.call(value_for_keydown.clone());
729 }
730 }
731 }
732 }
733 if enter_button {
734 button {
735 class: "adui-input-search-button",
736 r#type: "button",
737 disabled: is_disabled || loading,
738 onclick: move |_| {
739 if let Some(cb) = on_search_cb {
740 cb.call(value_for_button.clone());
741 }
742 },
743 {search_button_content}
744 }
745 } else {
746 span {
747 class: "adui-input-suffix adui-input-search-icon",
748 style: "cursor: pointer;",
749 onclick: move |_| {
750 if let Some(cb) = on_search_cb {
751 cb.call(value_for_icon.clone());
752 }
753 },
754 {search_button_content}
755 }
756 }
757 }
758 }
759}
760
761#[derive(Props, Clone, PartialEq)]
767pub struct OTPProps {
768 #[props(default = 6)]
770 pub length: usize,
771 #[props(optional)]
773 pub value: Option<String>,
774 #[props(optional)]
776 pub default_value: Option<String>,
777 #[props(default)]
778 pub disabled: bool,
779 #[props(optional)]
780 pub size: Option<InputSize>,
781 #[props(optional)]
782 pub variant: Option<Variant>,
783 #[props(optional)]
784 pub status: Option<ControlStatus>,
785 #[props(default)]
787 pub mask: bool,
788 #[props(optional)]
789 pub class: Option<String>,
790 #[props(optional)]
791 pub style: Option<String>,
792 #[props(optional)]
794 pub on_change: Option<EventHandler<String>>,
795 #[props(optional)]
797 pub on_complete: Option<EventHandler<String>>,
798}
799
800#[component]
802pub fn OTP(props: OTPProps) -> Element {
803 let OTPProps {
804 length,
805 value,
806 default_value,
807 disabled,
808 size,
809 variant,
810 status,
811 mask,
812 class,
813 style,
814 on_change,
815 on_complete,
816 } = props;
817
818 let config = use_config();
819 let resolved_size = size.unwrap_or_else(|| InputSize::from_global(config.size));
820 let resolved_variant = variant.unwrap_or(Variant::Outlined);
821
822 let initial = default_value.unwrap_or_default();
823 let chars: Vec<char> = initial.chars().collect();
824 let initial_values: Vec<String> = (0..length)
825 .map(|i| chars.get(i).map(|c| c.to_string()).unwrap_or_default())
826 .collect();
827
828 let values_signal: Signal<Vec<String>> = use_signal(|| initial_values);
829 let is_controlled = value.is_some();
830
831 let mut wrapper_classes = vec!["adui-input-otp".to_string()];
833 wrapper_classes.push(resolved_size.as_class().to_string());
834 push_status_class(&mut wrapper_classes, status);
835 if disabled {
836 wrapper_classes.push("adui-input-otp-disabled".into());
837 }
838 if let Some(extra) = class {
839 wrapper_classes.push(extra);
840 }
841 let wrapper_class = wrapper_classes.join(" ");
842 let wrapper_style = style.unwrap_or_default();
843
844 let input_type = if mask { "password" } else { "text" };
845
846 let get_current_values = move || {
848 if let Some(v) = &value {
849 let chars: Vec<char> = v.chars().collect();
850 (0..length)
851 .map(|i| chars.get(i).map(|c| c.to_string()).unwrap_or_default())
852 .collect()
853 } else {
854 values_signal.read().clone()
855 }
856 };
857
858 rsx! {
859 div { class: "{wrapper_class}", style: "{wrapper_style}",
860 {(0..length).map(|idx| {
861 let current_values = get_current_values();
862 let cell_value = current_values.get(idx).cloned().unwrap_or_default();
863 let values_for_input = values_signal;
864 let on_change_cb = on_change;
865 let on_complete_cb = on_complete;
866
867 let mut cell_classes = vec!["adui-input-otp-cell".to_string(), "adui-input".to_string()];
868 cell_classes.push(resolved_variant.class_for("adui-input"));
869
870 rsx! {
871 input {
872 key: "{idx}",
873 class: cell_classes.join(" "),
874 r#type: "{input_type}",
875 disabled: disabled,
876 maxlength: "1",
877 value: "{cell_value}",
878 oninput: move |evt| {
879 let new_char = evt.value();
880 let char_val = new_char.chars().next().map(|c| c.to_string()).unwrap_or_default();
882
883 if !is_controlled {
884 let mut vals = values_for_input;
885 vals.write()[idx] = char_val.clone();
886 }
887
888 let mut updated = values_for_input.read().clone();
890 if idx < updated.len() {
891 updated[idx] = char_val;
892 }
893 let combined: String = updated.iter().map(|s| s.as_str()).collect();
894
895 if let Some(cb) = on_change_cb {
896 cb.call(combined.clone());
897 }
898
899 let all_filled = updated.iter().all(|s| !s.is_empty());
901 if all_filled {
902 if let Some(cb) = on_complete_cb {
903 cb.call(combined);
904 }
905 }
906 }
907 }
908 }
909 })}
910 }
911 }
912}
913
914#[derive(Props, Clone, PartialEq)]
920pub struct TextAreaProps {
921 #[props(optional)]
922 pub value: Option<String>,
923 #[props(optional)]
924 pub default_value: Option<String>,
925 #[props(optional)]
926 pub placeholder: Option<String>,
927 #[props(optional)]
928 pub rows: Option<u16>,
929 #[props(default)]
930 pub disabled: bool,
931 #[props(optional)]
932 pub size: Option<InputSize>,
933 #[props(optional)]
934 pub variant: Option<Variant>,
935 #[props(optional)]
936 pub status: Option<ControlStatus>,
937 #[props(optional)]
938 pub max_length: Option<usize>,
939 #[props(default)]
940 pub show_count: bool,
941 #[props(optional)]
942 pub class: Option<String>,
943 #[props(optional)]
944 pub style: Option<String>,
945 #[props(optional)]
946 pub class_names: Option<InputClassNames>,
947 #[props(optional)]
948 pub styles: Option<InputStyles>,
949 #[props(optional)]
950 pub on_change: Option<EventHandler<String>>,
951}
952
953#[component]
955pub fn TextArea(props: TextAreaProps) -> Element {
956 let TextAreaProps {
957 value,
958 default_value,
959 placeholder,
960 rows,
961 disabled,
962 size,
963 variant,
964 status,
965 max_length,
966 show_count,
967 class,
968 style,
969 class_names,
970 styles,
971 on_change,
972 } = props;
973
974 let placeholder_str = placeholder.unwrap_or_default();
975 let config = use_config();
976
977 let form_control = use_form_item_control();
978 let controlled_by_prop = value.is_some();
979 let initial_inner = default_value.clone().unwrap_or_default();
980 let inner_value = use_signal(|| initial_inner);
981
982 let current_value = resolve_current_value(&form_control, value.clone(), inner_value);
983 let is_disabled =
984 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
985 let line_rows = rows.unwrap_or(3);
986
987 let resolved_size = size.unwrap_or_else(|| InputSize::from_global(config.size));
988 let resolved_variant = variant.unwrap_or(Variant::Outlined);
989
990 let mut class_list = vec!["adui-input".to_string(), "adui-input-textarea".to_string()];
991 class_list.push(resolved_size.as_class().to_string());
992 class_list.push(resolved_variant.class_for("adui-input"));
993 if is_disabled {
994 class_list.push("adui-input-disabled".into());
995 }
996 push_status_class(&mut class_list, status);
997 class_list.push_semantic(&class_names, InputSemantic::Root);
998 if let Some(extra) = class {
999 class_list.push(extra);
1000 }
1001 let class_attr = class_list
1002 .into_iter()
1003 .filter(|s| !s.is_empty())
1004 .collect::<Vec<_>>()
1005 .join(" ");
1006
1007 let mut style_attr = style.unwrap_or_default();
1008 style_attr.append_semantic(&styles, InputSemantic::Root);
1009
1010 let control_for_change = form_control;
1011 let mut inner_for_change = inner_value;
1012 let on_change_cb = on_change;
1013
1014 let char_count = current_value.chars().count();
1015 let count_text = if let Some(max) = max_length {
1016 format!("{}/{}", char_count, max)
1017 } else {
1018 char_count.to_string()
1019 };
1020
1021 let max_len_attr = max_length.map(|m| m.to_string());
1022
1023 if show_count {
1024 rsx! {
1025 div { class: "adui-input-textarea-wrapper",
1026 textarea {
1027 class: "{class_attr}",
1028 style: "{style_attr}",
1029 disabled: is_disabled,
1030 rows: "{line_rows}",
1031 value: "{current_value}",
1032 placeholder: "{placeholder_str}",
1033 maxlength: max_len_attr,
1034 oninput: move |evt| {
1035 let next = evt.value();
1036 apply_input_value(
1037 next,
1038 &control_for_change,
1039 controlled_by_prop,
1040 &mut inner_for_change,
1041 on_change_cb,
1042 );
1043 }
1044 }
1045 span { class: "adui-input-textarea-count", "{count_text}" }
1046 }
1047 }
1048 } else {
1049 rsx! {
1050 textarea {
1051 class: "{class_attr}",
1052 style: "{style_attr}",
1053 disabled: is_disabled,
1054 rows: "{line_rows}",
1055 value: "{current_value}",
1056 placeholder: "{placeholder_str}",
1057 maxlength: max_len_attr,
1058 oninput: move |evt| {
1059 let next = evt.value();
1060 apply_input_value(
1061 next,
1062 &control_for_change,
1063 controlled_by_prop,
1064 &mut inner_for_change,
1065 on_change_cb,
1066 );
1067 }
1068 }
1069 }
1070 }
1071}
1072
1073#[derive(Props, Clone, PartialEq)]
1078struct InputInternalProps {
1079 #[props(optional)]
1080 value: Option<String>,
1081 #[props(optional)]
1082 default_value: Option<String>,
1083 #[props(optional)]
1084 placeholder: Option<String>,
1085 #[props(default)]
1086 disabled: bool,
1087 #[props(optional)]
1088 size: Option<InputSize>,
1089 #[props(optional)]
1090 variant: Option<Variant>,
1091 #[props(optional)]
1092 status: Option<ControlStatus>,
1093 #[props(optional)]
1094 prefix: Option<Element>,
1095 #[props(optional)]
1096 suffix: Option<Element>,
1097 #[props(default)]
1098 is_password: bool,
1099 #[props(optional)]
1100 class: Option<String>,
1101 #[props(optional)]
1102 style: Option<String>,
1103 #[props(optional)]
1104 class_names: Option<InputClassNames>,
1105 #[props(optional)]
1106 styles: Option<InputStyles>,
1107 #[props(optional)]
1108 on_change: Option<EventHandler<String>>,
1109 #[props(optional)]
1110 on_press_enter: Option<EventHandler<()>>,
1111 #[props(optional)]
1112 extra_class: Option<String>,
1113}
1114
1115#[component]
1116fn InputInternal(props: InputInternalProps) -> Element {
1117 let InputInternalProps {
1118 value,
1119 default_value,
1120 placeholder,
1121 disabled,
1122 size,
1123 variant,
1124 status,
1125 prefix,
1126 suffix,
1127 is_password,
1128 class,
1129 style,
1130 class_names,
1131 styles,
1132 on_change,
1133 on_press_enter,
1134 extra_class,
1135 } = props;
1136
1137 let config = use_config();
1138 let form_control = use_form_item_control();
1139 let controlled_by_prop = value.is_some();
1140
1141 let resolved_size = size.unwrap_or_else(|| InputSize::from_global(config.size));
1142 let resolved_variant = variant.unwrap_or(Variant::Outlined);
1143
1144 let initial_inner = default_value.clone().unwrap_or_default();
1145 let inner_value = use_signal(|| initial_inner);
1146
1147 let current_value = resolve_current_value(&form_control, value.clone(), inner_value);
1148 let is_disabled =
1149 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
1150
1151 let control_for_change = form_control;
1152 let on_change_cb = on_change;
1153 let on_press_enter_cb = on_press_enter;
1154 let controlled_flag = controlled_by_prop;
1155 let mut inner_for_change = inner_value;
1156
1157 let mut wrapper_classes = vec!["adui-input-affix-wrapper".to_string()];
1158 wrapper_classes.push(resolved_size.as_class().to_string());
1159 wrapper_classes.push(resolved_variant.class_for("adui-input"));
1160 if is_disabled {
1161 wrapper_classes.push("adui-input-disabled".into());
1162 }
1163 push_status_class(&mut wrapper_classes, status);
1164 wrapper_classes.push_semantic(&class_names, InputSemantic::Root);
1165 if let Some(extra) = extra_class {
1166 wrapper_classes.push(extra);
1167 }
1168 if let Some(extra) = class {
1169 wrapper_classes.push(extra);
1170 }
1171 let wrapper_class = wrapper_classes
1172 .into_iter()
1173 .filter(|s| !s.is_empty())
1174 .collect::<Vec<_>>()
1175 .join(" ");
1176
1177 let mut wrapper_style = style.unwrap_or_default();
1178 wrapper_style.append_semantic(&styles, InputSemantic::Root);
1179
1180 let placeholder_str = placeholder.unwrap_or_default();
1181 let input_type = if is_password { "password" } else { "text" };
1182
1183 rsx! {
1184 div {
1185 class: "{wrapper_class}",
1186 style: "{wrapper_style}",
1187 if let Some(icon) = prefix {
1188 span { class: "adui-input-prefix", {icon} }
1189 }
1190 input {
1191 class: "adui-input",
1192 r#type: "{input_type}",
1193 disabled: is_disabled,
1194 value: "{current_value}",
1195 placeholder: "{placeholder_str}",
1196 oninput: move |evt| {
1197 let next = evt.value();
1198 apply_input_value(
1199 next,
1200 &control_for_change,
1201 controlled_flag,
1202 &mut inner_for_change,
1203 on_change_cb,
1204 );
1205 },
1206 onkeydown: move |evt: KeyboardEvent| {
1207 if matches!(evt.key(), Key::Enter)
1208 && let Some(cb) = on_press_enter_cb
1209 {
1210 cb.call(());
1211 }
1212 }
1213 }
1214 if let Some(icon) = suffix {
1215 span { class: "adui-input-suffix", {icon} }
1216 }
1217 }
1218 }
1219}
1220
1221fn resolve_current_value(
1226 form_control: &Option<FormItemControlContext>,
1227 prop_value: Option<String>,
1228 inner: Signal<String>,
1229) -> String {
1230 if let Some(ctx) = form_control {
1231 return form_value_to_string(ctx.value());
1232 }
1233 if let Some(v) = prop_value {
1234 return v;
1235 }
1236 inner.read().clone()
1237}
1238
1239fn apply_input_value(
1240 next: String,
1241 form_control: &Option<FormItemControlContext>,
1242 controlled_by_prop: bool,
1243 inner: &mut Signal<String>,
1244 on_change: Option<EventHandler<String>>,
1245) {
1246 if let Some(ctx) = form_control {
1247 ctx.set_string(next.clone());
1248 } else if !controlled_by_prop {
1249 let mut state = *inner;
1250 state.set(next.clone());
1251 }
1252 if let Some(cb) = on_change {
1253 cb.call(next);
1254 }
1255}
1256
1257fn rand_id() -> u32 {
1259 #[cfg(target_arch = "wasm32")]
1261 {
1262 use js_sys::Math;
1263 (Math::random() * 1_000_000.0) as u32
1264 }
1265
1266 #[cfg(not(target_arch = "wasm32"))]
1267 {
1268 use std::time::{SystemTime, UNIX_EPOCH};
1269 SystemTime::now()
1270 .duration_since(UNIX_EPOCH)
1271 .map(|d| d.subsec_nanos())
1272 .unwrap_or(0)
1273 }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use super::*;
1279
1280 #[test]
1281 fn input_size_class_mapping() {
1282 assert_eq!(InputSize::Small.as_class(), "adui-input-sm");
1283 assert_eq!(InputSize::Middle.as_class(), "");
1284 assert_eq!(InputSize::Large.as_class(), "adui-input-lg");
1285 }
1286
1287 #[test]
1288 fn input_size_from_global() {
1289 assert_eq!(
1290 InputSize::from_global(ComponentSize::Small),
1291 InputSize::Small
1292 );
1293 assert_eq!(
1294 InputSize::from_global(ComponentSize::Middle),
1295 InputSize::Middle
1296 );
1297 assert_eq!(
1298 InputSize::from_global(ComponentSize::Large),
1299 InputSize::Large
1300 );
1301 }
1302
1303 #[test]
1309 fn input_size_variants() {
1310 assert_eq!(InputSize::Small.as_class(), "adui-input-sm");
1311 assert_eq!(InputSize::Middle.as_class(), "");
1312 assert_eq!(InputSize::Large.as_class(), "adui-input-lg");
1313 }
1314
1315 #[test]
1317 fn input_variant_integration() {
1318 use crate::foundation::variant_from_bordered;
1319
1320 assert_eq!(
1322 variant_from_bordered(Some(false), Some(Variant::Filled)),
1323 Variant::Filled
1324 );
1325
1326 assert_eq!(
1328 variant_from_bordered(Some(false), None),
1329 Variant::Borderless
1330 );
1331
1332 assert_eq!(variant_from_bordered(None, None), Variant::Outlined);
1334 }
1335
1336 #[test]
1338 fn input_character_count_calculation() {
1339 let text = "Hello";
1341 assert_eq!(text.chars().count(), 5);
1342
1343 let empty = "";
1345 assert_eq!(empty.chars().count(), 0);
1346
1347 let special = "Hello!@#";
1349 assert_eq!(special.chars().count(), 8);
1350
1351 let unicode = "你好";
1353 assert_eq!(unicode.chars().count(), 2);
1354 }
1355
1356 #[test]
1358 fn input_max_length_validation() {
1359 let max_len = 10;
1360 let short_text = "Hello";
1361 let long_text = "This is a very long text that exceeds the maximum length";
1362
1363 assert!(short_text.chars().count() <= max_len);
1364 assert!(long_text.chars().count() > max_len);
1365 }
1366
1367 #[test]
1369 fn input_clear_button_visibility() {
1370 let allow_clear = true;
1376 let has_value = !"test".is_empty();
1377 let is_disabled = false;
1378 let should_show = allow_clear && has_value && !is_disabled;
1379 assert!(should_show);
1380
1381 let is_disabled = true;
1383 let should_show = allow_clear && has_value && !is_disabled;
1384 assert!(!should_show);
1385
1386 let has_value = !"".is_empty();
1388 let is_disabled = false;
1389 let should_show = allow_clear && has_value && !is_disabled;
1390 assert!(!should_show);
1391 }
1392
1393 #[test]
1395 fn input_props_defaults() {
1396 let props = InputProps {
1397 value: None,
1398 default_value: None,
1399 placeholder: None,
1400 disabled: false,
1401 size: None,
1402 variant: None,
1403 bordered: None,
1404 status: None,
1405 prefix: None,
1406 suffix: None,
1407 addon_before: None,
1408 addon_after: None,
1409 allow_clear: false,
1410 max_length: None,
1411 show_count: false,
1412 class: None,
1413 root_class_name: None,
1414 style: None,
1415 class_names: None,
1416 styles: None,
1417 on_change: None,
1418 on_press_enter: None,
1419 data_attributes: None,
1420 };
1421
1422 assert_eq!(props.disabled, false);
1423 assert_eq!(props.allow_clear, false);
1424 assert_eq!(props.show_count, false);
1425 }
1426
1427 #[test]
1428 fn input_size_default() {
1429 assert_eq!(InputSize::default(), InputSize::Middle);
1430 }
1431
1432 #[test]
1433 fn input_size_all_variants() {
1434 assert_eq!(InputSize::Small, InputSize::Small);
1435 assert_eq!(InputSize::Middle, InputSize::Middle);
1436 assert_eq!(InputSize::Large, InputSize::Large);
1437 assert_ne!(InputSize::Small, InputSize::Large);
1438 }
1439
1440 #[test]
1441 fn input_size_equality() {
1442 let size1 = InputSize::Small;
1443 let size2 = InputSize::Small;
1444 let size3 = InputSize::Large;
1445 assert_eq!(size1, size2);
1446 assert_ne!(size1, size3);
1447 }
1448
1449 #[test]
1450 fn input_character_count_with_emoji() {
1451 let text = "Hello 😀";
1452 assert_eq!(text.chars().count(), 7);
1453 }
1454
1455 #[test]
1456 fn input_character_count_with_mixed_unicode() {
1457 let text = "Hello 世界";
1458 assert_eq!(text.chars().count(), 8);
1459 }
1460
1461 #[test]
1462 fn input_max_length_formatting() {
1463 let char_count = 5;
1464 let max_len = 10;
1465 let count_text = format!("{}/{}", char_count, max_len);
1466 assert_eq!(count_text, "5/10");
1467 }
1468
1469 #[test]
1470 fn input_max_length_no_limit() {
1471 let char_count = 15;
1472 let count_text = char_count.to_string();
1473 assert_eq!(count_text, "15");
1474 }
1475
1476 #[test]
1477 fn input_clear_button_logic_edge_cases() {
1478 assert!(!"".is_empty() == false);
1480
1481 assert!("test".is_empty() == false);
1483 }
1484
1485 #[test]
1486 fn input_size_class_empty_for_middle() {
1487 assert_eq!(InputSize::Middle.as_class(), "");
1489 }
1490
1491 #[test]
1492 fn input_variant_bordered_false() {
1493 use crate::foundation::variant_from_bordered;
1494 assert_eq!(
1495 variant_from_bordered(Some(false), None),
1496 Variant::Borderless
1497 );
1498 }
1499
1500 #[test]
1501 fn input_variant_bordered_true() {
1502 use crate::foundation::variant_from_bordered;
1503 assert_eq!(variant_from_bordered(Some(true), None), Variant::Outlined);
1504 }
1505
1506 #[test]
1507 fn input_variant_priority() {
1508 use crate::foundation::variant_from_bordered;
1509 assert_eq!(
1511 variant_from_bordered(Some(true), Some(Variant::Filled)),
1512 Variant::Filled
1513 );
1514 assert_eq!(
1515 variant_from_bordered(Some(false), Some(Variant::Filled)),
1516 Variant::Filled
1517 );
1518 }
1519
1520 #[test]
1521 fn input_character_count_unicode_boundary() {
1522 let text1 = "a";
1524 let text2 = "中";
1525 let text3 = "😀";
1526 assert_eq!(text1.chars().count(), 1);
1527 assert_eq!(text2.chars().count(), 1);
1528 assert_eq!(text3.chars().count(), 1);
1529 }
1530
1531 #[test]
1532 fn input_max_length_boundary_values() {
1533 let max_len = 0;
1534 let text = "";
1535 assert!(text.chars().count() <= max_len);
1536
1537 let max_len = 1;
1538 let text = "a";
1539 assert!(text.chars().count() <= max_len);
1540
1541 let text2 = "ab";
1542 assert!(text2.chars().count() > max_len);
1543 }
1544}