Skip to main content

kozan_core/html/
html_input_element.rs

1//! `HTMLInputElement` — the most versatile form control.
2//!
3//! Chrome equivalent: `HTMLInputElement` + `InputType` strategy pattern.
4//! Each input type (text, password, number, range, date, color, file, etc.)
5//! has different behavior. Chrome delegates to separate `InputType` subclasses.
6//!
7//! # Chrome's `InputType` strategy
8//!
9//! ```text
10//! HTMLInputElement
11//!   └── input_type_: InputType*
12//!         ├── TextFieldInputType
13//!         ├── RangeInputType
14//!         ├── NumberInputType
15//!         ├── ColorInputType
16//!         ├── FileInputType
17//!         └── ... (20+ types)
18//! ```
19//!
20//! For Kozan Phase 1: we store `input_type` as an enum and handle behavior
21//! centrally. The strategy pattern can be introduced when we need complex
22//! per-type behavior (shadow DOM, custom rendering).
23
24use super::form_control::{FormControlElement, TextControlElement};
25use crate::Handle;
26use kozan_macros::{Element, Props};
27
28/// The type of an `<input>` element.
29///
30/// Chrome equivalent: the `InputType` class hierarchy.
31/// Each variant maps to a different set of behaviors, rendering, and validation.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum InputType {
34    /// `<input type="text">` — single-line text field.
35    #[default]
36    Text,
37    /// `<input type="password">` — obscured text field.
38    Password,
39    /// `<input type="number">` — numeric input with spinner.
40    Number,
41    /// `<input type="email">` — email address.
42    Email,
43    /// `<input type="url">` — URL input.
44    Url,
45    /// `<input type="tel">` — telephone number.
46    Tel,
47    /// `<input type="search">` — search field.
48    Search,
49    /// `<input type="range">` — slider control.
50    Range,
51    /// `<input type="color">` — color picker.
52    Color,
53    /// `<input type="date">` — date picker.
54    Date,
55    /// `<input type="time">` — time picker.
56    Time,
57    /// `<input type="datetime-local">` — date+time picker.
58    DatetimeLocal,
59    /// `<input type="checkbox">` — boolean toggle.
60    Checkbox,
61    /// `<input type="radio">` — one-of-many selection.
62    Radio,
63    /// `<input type="file">` — file upload.
64    File,
65    /// `<input type="submit">` — form submit button.
66    Submit,
67    /// `<input type="reset">` — form reset button.
68    Reset,
69    /// `<input type="button">` — generic button (no default action).
70    Button,
71    /// `<input type="hidden">` — hidden data.
72    Hidden,
73    /// `<input type="image">` — image submit button.
74    Image,
75}
76
77impl InputType {
78    /// Parse an input type from its string representation.
79    ///
80    /// Unknown types default to `Text` (per HTML spec).
81    #[must_use]
82    pub fn parse(s: &str) -> Self {
83        match s.to_ascii_lowercase().as_str() {
84            "text" => Self::Text,
85            "password" => Self::Password,
86            "number" => Self::Number,
87            "email" => Self::Email,
88            "url" => Self::Url,
89            "tel" => Self::Tel,
90            "search" => Self::Search,
91            "range" => Self::Range,
92            "color" => Self::Color,
93            "date" => Self::Date,
94            "time" => Self::Time,
95            "datetime-local" => Self::DatetimeLocal,
96            "checkbox" => Self::Checkbox,
97            "radio" => Self::Radio,
98            "file" => Self::File,
99            "submit" => Self::Submit,
100            "reset" => Self::Reset,
101            "button" => Self::Button,
102            "hidden" => Self::Hidden,
103            "image" => Self::Image,
104            _ => Self::Text, // HTML spec: unknown type → text
105        }
106    }
107
108    /// Whether this input type is a text-editing type.
109    #[must_use]
110    pub fn is_text_type(&self) -> bool {
111        matches!(
112            self,
113            Self::Text
114                | Self::Password
115                | Self::Number
116                | Self::Email
117                | Self::Url
118                | Self::Tel
119                | Self::Search
120        )
121    }
122
123    /// Whether this input type has a checked state.
124    #[must_use]
125    pub fn is_checkable(&self) -> bool {
126        matches!(self, Self::Checkbox | Self::Radio)
127    }
128
129    /// Whether this input type is a button.
130    #[must_use]
131    pub fn is_button_type(&self) -> bool {
132        matches!(
133            self,
134            Self::Submit | Self::Reset | Self::Button | Self::Image
135        )
136    }
137
138    /// Whether this input type is focusable.
139    #[must_use]
140    pub fn is_focusable(&self) -> bool {
141        !matches!(self, Self::Hidden)
142    }
143}
144
145/// An input element (`<input>`).
146///
147/// Chrome equivalent: `HTMLInputElement`.
148/// The most versatile form control — behavior depends on `input_type`.
149#[derive(Copy, Clone, Element)]
150#[element(tag = "input", focusable, data = InputData)]
151pub struct HtmlInputElement(Handle);
152
153/// Element-specific data for `<input>`.
154#[derive(Default, Clone, Props)]
155#[props(element = HtmlInputElement)]
156pub struct InputData {
157    /// The input type (text, password, number, etc.).
158    #[prop]
159    pub input_type: InputType,
160    /// Whether the checkbox/radio is checked.
161    #[prop]
162    pub checked: bool,
163}
164
165impl FormControlElement for HtmlInputElement {}
166impl TextControlElement for HtmlInputElement {}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn input_type_parse() {
174        assert_eq!(InputType::parse("text"), InputType::Text);
175        assert_eq!(InputType::parse("PASSWORD"), InputType::Password);
176        assert_eq!(InputType::parse("number"), InputType::Number);
177        assert_eq!(InputType::parse("checkbox"), InputType::Checkbox);
178        assert_eq!(InputType::parse("unknown"), InputType::Text); // spec default
179    }
180
181    #[test]
182    fn text_type_classification() {
183        assert!(InputType::Text.is_text_type());
184        assert!(InputType::Password.is_text_type());
185        assert!(!InputType::Checkbox.is_text_type());
186        assert!(!InputType::File.is_text_type());
187    }
188
189    #[test]
190    fn checkable_classification() {
191        assert!(InputType::Checkbox.is_checkable());
192        assert!(InputType::Radio.is_checkable());
193        assert!(!InputType::Text.is_checkable());
194    }
195
196    #[test]
197    fn button_type_classification() {
198        assert!(InputType::Submit.is_button_type());
199        assert!(InputType::Reset.is_button_type());
200        assert!(InputType::Button.is_button_type());
201        assert!(InputType::Image.is_button_type());
202        assert!(!InputType::Text.is_button_type());
203        assert!(!InputType::Checkbox.is_button_type());
204    }
205
206    #[test]
207    fn focusable_classification() {
208        // All types are focusable except hidden.
209        assert!(InputType::Text.is_focusable());
210        assert!(InputType::Password.is_focusable());
211        assert!(InputType::Checkbox.is_focusable());
212        assert!(InputType::Submit.is_focusable());
213        assert!(!InputType::Hidden.is_focusable());
214    }
215
216    #[test]
217    fn input_type_default() {
218        assert_eq!(InputType::default(), InputType::Text);
219    }
220
221    #[test]
222    fn input_data_props() {
223        use crate::dom::document::Document;
224
225        let doc = Document::new();
226        let input = doc.create::<HtmlInputElement>();
227
228        // Default input_type is Text.
229        assert_eq!(input.input_type(), InputType::Text);
230        assert!(!input.checked());
231
232        // Set input_type via data prop.
233        input.set_input_type(InputType::Checkbox);
234        assert_eq!(input.input_type(), InputType::Checkbox);
235
236        // Set checked.
237        input.set_checked(true);
238        assert!(input.checked());
239    }
240
241    #[test]
242    fn input_text_control() {
243        use crate::dom::document::Document;
244
245        let doc = Document::new();
246        let input = doc.create::<HtmlInputElement>();
247
248        // TextControlElement: value, placeholder.
249        input.set_value("hello");
250        assert_eq!(input.value(), "hello");
251
252        input.set_placeholder("Enter text...");
253        assert_eq!(input.placeholder(), "Enter text...");
254    }
255
256    #[test]
257    fn input_form_control() {
258        use crate::dom::document::Document;
259
260        let doc = Document::new();
261        let input = doc.create::<HtmlInputElement>();
262
263        // FormControlElement: disabled, name, required.
264        assert!(!input.disabled());
265        input.set_disabled(true);
266        assert!(input.disabled());
267
268        input.set_name("username");
269        assert_eq!(input.name(), "username");
270    }
271}