adui_dioxus/components/
input.rs

1//! Input component family aligned with Ant Design 6.0.
2//!
3//! Includes:
4//! - `Input` - Basic text input
5//! - `TextArea` - Multi-line text input
6//! - `Password` - Password input with visibility toggle
7//! - `Search` - Input with search button
8//! - `OTP` - One-time password input
9
10use 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/// Size variants for Input components.
24#[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/// Props for a single-line text input.
51#[derive(Props, Clone, PartialEq)]
52pub struct InputProps {
53    /// Controlled value. When set, the component will not manage internal state.
54    #[props(optional)]
55    pub value: Option<String>,
56    /// Initial value in uncontrolled mode.
57    #[props(optional)]
58    pub default_value: Option<String>,
59    #[props(optional)]
60    pub placeholder: Option<String>,
61    #[props(default)]
62    pub disabled: bool,
63    /// Component size.
64    #[props(optional)]
65    pub size: Option<InputSize>,
66    /// Visual variant (outlined/filled/borderless).
67    #[props(optional)]
68    pub variant: Option<Variant>,
69    /// @deprecated Use `variant="borderless"` instead.
70    #[props(optional)]
71    pub bordered: Option<bool>,
72    /// Optional status used to style the wrapper (success/warning/error).
73    #[props(optional)]
74    pub status: Option<ControlStatus>,
75    /// Leading element rendered inside the affix wrapper.
76    #[props(optional)]
77    pub prefix: Option<Element>,
78    /// Trailing element rendered inside the affix wrapper.
79    #[props(optional)]
80    pub suffix: Option<Element>,
81    /// @deprecated Use `Space.Compact` instead. Content before the input.
82    #[props(optional)]
83    pub addon_before: Option<Element>,
84    /// @deprecated Use `Space.Compact` instead. Content after the input.
85    #[props(optional)]
86    pub addon_after: Option<Element>,
87    /// Whether to show a clear icon when there is content.
88    #[props(default)]
89    pub allow_clear: bool,
90    /// Maximum length of input.
91    #[props(optional)]
92    pub max_length: Option<usize>,
93    /// Whether to show character count.
94    #[props(default)]
95    pub show_count: bool,
96    #[props(optional)]
97    pub class: Option<String>,
98    /// Extra class applied to root element.
99    #[props(optional)]
100    pub root_class_name: Option<String>,
101    #[props(optional)]
102    pub style: Option<String>,
103    /// Semantic class names for sub-parts.
104    #[props(optional)]
105    pub class_names: Option<InputClassNames>,
106    /// Semantic styles for sub-parts.
107    #[props(optional)]
108    pub styles: Option<InputStyles>,
109    /// Change event with the next string value.
110    #[props(optional)]
111    pub on_change: Option<EventHandler<String>>,
112    /// Triggered when pressing Enter.
113    #[props(optional)]
114    pub on_press_enter: Option<EventHandler<()>>,
115    /// Data attributes as a map of key-value pairs. Keys should be without the "data-" prefix.
116    /// For example, `data_attributes: Some([("test", "value")])` will render as `data-test="value"`.
117    #[props(optional)]
118    pub data_attributes: Option<Vec<(String, String)>>,
119}
120
121/// Ant Design flavored text input.
122#[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    // Generate a unique ID for this input to support data-* attributes via JavaScript interop
151    let input_id = use_signal(|| format!("adui-input-{}", rand_id()));
152
153    // Set data-* attributes via JavaScript interop if provided
154    #[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        // Suppress unused variable warning on non-wasm32 targets
179        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    // Resolve size
188    let resolved_size = size.unwrap_or_else(|| InputSize::from_global(config.size));
189
190    // Resolve variant
191    let resolved_variant = variant_from_bordered(bordered, variant);
192
193    // Local state used only when not controlled by Form or external value.
194    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    // Build shared handlers.
210    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    // Build input element classes
217    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    // Build input style
223    let mut input_style = String::new();
224    input_style.append_semantic(&styles, InputSemantic::Input);
225
226    // Count display
227    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    // Build wrapper classes
268    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    // Wrap with addon group if needed
300    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        // Affix wrapper variant
354        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        // Simple input variant
399        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// ============================================================================
455// Password Input
456// ============================================================================
457
458/// Props for Password input.
459#[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    /// Whether the password is visible by default.
478    #[props(default)]
479    pub visible: bool,
480    /// Custom icon for showing password.
481    #[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    /// Called when visibility changes.
496    #[props(optional)]
497    pub on_visible_change: Option<EventHandler<bool>>,
498}
499
500/// Password input with visibility toggle.
501#[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// ============================================================================
576// Search Input
577// ============================================================================
578
579/// Props for Search input.
580#[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    /// Whether to show enter button.
599    #[props(default = true)]
600    pub enter_button: bool,
601    /// Custom content for enter button.
602    #[props(optional)]
603    pub enter_button_content: Option<Element>,
604    /// Whether search is in loading state.
605    #[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    /// Triggered when search button is clicked or Enter is pressed.
618    #[props(optional)]
619    pub on_search: Option<EventHandler<String>>,
620}
621
622/// Search input with search button.
623#[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    // Build wrapper classes
665    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    // Clone value for event handlers
699    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// ============================================================================
762// OTP Input
763// ============================================================================
764
765/// Props for OTP (One-Time Password) input.
766#[derive(Props, Clone, PartialEq)]
767pub struct OTPProps {
768    /// Number of input fields (default: 6).
769    #[props(default = 6)]
770    pub length: usize,
771    /// Controlled value.
772    #[props(optional)]
773    pub value: Option<String>,
774    /// Default value.
775    #[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    /// Whether to mask input (like password).
786    #[props(default)]
787    pub mask: bool,
788    #[props(optional)]
789    pub class: Option<String>,
790    #[props(optional)]
791    pub style: Option<String>,
792    /// Called when all fields are filled.
793    #[props(optional)]
794    pub on_change: Option<EventHandler<String>>,
795    /// Called when input is complete.
796    #[props(optional)]
797    pub on_complete: Option<EventHandler<String>>,
798}
799
800/// One-Time Password input with multiple fields.
801#[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    // Build wrapper classes
832    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    // Helper to get current values
847    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                            // Only take first character
881                            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                            // Build combined value from signal
889                            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                            // Check if complete
900                            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// ============================================================================
915// TextArea
916// ============================================================================
917
918/// Multi-line text area component.
919#[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/// Ant Design flavored multi-line text area.
954#[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// ============================================================================
1074// Internal Input (shared implementation)
1075// ============================================================================
1076
1077#[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
1221// ============================================================================
1222// Helper functions
1223// ============================================================================
1224
1225fn 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
1257/// Generate a simple random ID for element identification.
1258fn rand_id() -> u32 {
1259    // Simple pseudo-random based on current time
1260    #[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    // Note: resolve_current_value and apply_input_value functions require Dioxus runtime
1304    // and cannot be easily tested in unit tests without a full runtime setup.
1305    // These functions are tested indirectly through integration tests and component demos.
1306
1307    // Test InputSize enum methods
1308    #[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 variant integration
1316    #[test]
1317    fn input_variant_integration() {
1318        use crate::foundation::variant_from_bordered;
1319
1320        // Test variant takes priority over bordered
1321        assert_eq!(
1322            variant_from_bordered(Some(false), Some(Variant::Filled)),
1323            Variant::Filled
1324        );
1325
1326        // Test bordered=false maps to Borderless
1327        assert_eq!(
1328            variant_from_bordered(Some(false), None),
1329            Variant::Borderless
1330        );
1331
1332        // Test default is Outlined
1333        assert_eq!(variant_from_bordered(None, None), Variant::Outlined);
1334    }
1335
1336    // Test character count calculation
1337    #[test]
1338    fn input_character_count_calculation() {
1339        // Test basic character count
1340        let text = "Hello";
1341        assert_eq!(text.chars().count(), 5);
1342
1343        // Test empty string
1344        let empty = "";
1345        assert_eq!(empty.chars().count(), 0);
1346
1347        // Test with special characters
1348        let special = "Hello!@#";
1349        assert_eq!(special.chars().count(), 8);
1350
1351        // Test with unicode characters
1352        let unicode = "你好";
1353        assert_eq!(unicode.chars().count(), 2);
1354    }
1355
1356    // Test max_length validation
1357    #[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 clear button visibility logic
1368    #[test]
1369    fn input_clear_button_visibility() {
1370        // Clear button should be visible when:
1371        // - allow_clear is true
1372        // - value is not empty
1373        // - input is not disabled
1374
1375        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        // Should not show when disabled
1382        let is_disabled = true;
1383        let should_show = allow_clear && has_value && !is_disabled;
1384        assert!(!should_show);
1385
1386        // Should not show when value is empty
1387        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 InputProps default values
1394    #[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        // Empty string should not show clear button
1479        assert!(!"".is_empty() == false);
1480
1481        // Non-empty string should show clear button when enabled
1482        assert!("test".is_empty() == false);
1483    }
1484
1485    #[test]
1486    fn input_size_class_empty_for_middle() {
1487        // Middle size should return empty string
1488        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        // Variant should take priority over bordered
1510        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        // Test with various unicode characters
1523        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}