radix_leptos_primitives/components/
password_toggle_field.rs

1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4use wasm_bindgen::JsCast;
5
6/// Password Toggle Field component with visibility toggle
7#[component]
8pub fn PasswordToggleField(
9    /// Input value
10    #[prop(optional)]
11    value: Option<String>,
12    /// Placeholder text
13    #[prop(optional)]
14    placeholder: Option<String>,
15    /// Whether the field is disabled
16    #[prop(optional)]
17    disabled: Option<bool>,
18    /// Whether the field is required
19    #[prop(optional)]
20    required: Option<bool>,
21    /// Whether the field is read-only
22    #[prop(optional)]
23    readonly: Option<bool>,
24    /// Whether the password is visible
25    #[prop(optional)]
26    visible: Option<bool>,
27    /// Minimum password length
28    #[prop(optional)]
29    min_length: Option<usize>,
30    /// Maximum password length
31    #[prop(optional)]
32    max_length: Option<usize>,
33    /// Password strength requirements
34    #[prop(optional)]
35    strength_requirements: Option<PasswordStrengthRequirements>,
36    /// Callback when value changes
37    #[prop(optional)]
38    on_change: Option<Callback<String>>,
39    /// Callback when visibility toggles
40    #[prop(optional)]
41    on_visibility_toggle: Option<Callback<bool>>,
42    /// Callback when field is focused
43    #[prop(optional)]
44    on_focus: Option<Callback<()>>,
45    /// Callback when field is blurred
46    #[prop(optional)]
47    on_blur: Option<Callback<()>>,
48    /// Callback when field is validated
49    #[prop(optional)]
50    on_validation: Option<Callback<PasswordValidation>>,
51    /// Additional CSS classes
52    #[prop(optional)]
53    class: Option<String>,
54    /// Inline styles
55    #[prop(optional)]
56    style: Option<String>,
57    /// Children content
58    children: Option<Children>,
59) -> impl IntoView {
60    let _value = value.unwrap_or_default();
61    let _placeholder = placeholder.unwrap_or_else(|| "Enter password...".to_string());
62    let _disabled = disabled.unwrap_or(false);
63    let _required = required.unwrap_or(false);
64    let _readonly = readonly.unwrap_or(false);
65    let _visible = visible.unwrap_or(false);
66    let _min_length = min_length.unwrap_or(0);
67    let _max_length = max_length.unwrap_or(usize::MAX);
68    let _strength_requirements = strength_requirements.unwrap_or_default();
69
70    let class = "password-toggle-field".to_string();
71
72    let style = style.unwrap_or_default();
73
74    let _handle_input = move |event: web_sys::Event| {
75        if let Some(input) = event
76            .target()
77            .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
78        {
79            if let Some(callback) = on_change {
80                callback.run(input.value());
81            }
82        }
83    };
84
85    let handle_focus = move |_: ()| {
86        if let Some(callback) = on_focus {
87            callback.run(());
88        }
89    };
90
91    let handle_blur = move |_: ()| {
92        if let Some(callback) = on_blur {
93            callback.run(());
94        }
95    };
96
97    let handle_visibility_toggle = move |_| {
98        if let Some(callback) = on_visibility_toggle {
99            callback.run(!visible.unwrap_or(false));
100        }
101    };
102
103    view! {
104        <div class=class style=style>
105            <div class="password-field-container">
106                <input
107                    class="password-input"
108                    on:click=handle_visibility_toggle
109                />
110            </div>
111        </div>
112    }
113}
114
115/// Password strength requirements
116#[derive(Debug, Clone, PartialEq, Default)]
117pub struct PasswordStrengthRequirements {
118    pub min_length: usize,
119    pub require_uppercase: bool,
120    pub require_lowercase: bool,
121    pub require_numbers: bool,
122    pub require_symbols: bool,
123    pub min_strength_score: usize,
124}
125
126/// Password validation result
127#[derive(Debug, Clone, PartialEq, Default)]
128pub struct PasswordValidation {
129    pub is_valid: bool,
130    pub strength_score: usize,
131    pub strength_level: PasswordStrengthLevel,
132    pub errors: Vec<String>,
133    pub warnings: Vec<String>,
134}
135
136/// Password strength levels
137#[derive(Debug, Clone, Copy, PartialEq, Default)]
138pub enum PasswordStrengthLevel {
139    #[default]
140    VeryWeak,
141    Weak,
142    Fair,
143    Good,
144    Strong,
145}
146
147/// Password strength indicator component
148#[component]
149pub fn PasswordStrengthIndicator(
150    /// Password value
151    #[prop(optional)]
152    password: Option<String>,
153    /// Strength requirements
154    #[prop(optional)]
155    requirements: Option<PasswordStrengthRequirements>,
156    /// Whether to show detailed feedback
157    #[prop(optional)]
158    show_details: Option<bool>,
159    /// Additional CSS classes
160    #[prop(optional)]
161    class: Option<String>,
162    /// Inline styles
163    #[prop(optional)]
164    style: Option<String>,
165) -> impl IntoView {
166    let password = password.unwrap_or_default();
167    let requirements = requirements.unwrap_or_default();
168    let show_details = show_details.unwrap_or(true);
169
170    let validation = validate_password(&password, &requirements);
171    let strength_class = format!(
172        "strength-{}",
173        match validation.strength_level {
174            PasswordStrengthLevel::VeryWeak => "very-weak",
175            PasswordStrengthLevel::Weak => "weak",
176            PasswordStrengthLevel::Fair => "fair",
177            PasswordStrengthLevel::Good => "good",
178            PasswordStrengthLevel::Strong => "strong",
179        }
180    );
181
182    let class = format!(
183        "password-strength-indicator {} {}",
184        strength_class,
185        class.unwrap_or_default()
186    );
187    let style = style.unwrap_or_default();
188
189    view! {
190        <div class=class style=style>
191            <div class="strength-bar">
192                <div class="strength-fill" style=format!("width: {}%", validation.strength_score * 20)></div>
193            </div>
194            <div class="strength-label">
195                {format!("Strength: {:?}", validation.strength_level)}
196            </div>
197            {if show_details {
198                view! {
199                    <div class="strength-details">
200                        {if !validation.errors.is_empty() {
201                            view! {
202                                <div class="strength-errors">
203                                    {validation.errors.into_iter().map(|error| {
204                                        view! { <div class="error">{error}</div> }
205                                    }).collect::<Vec<_>>()}
206                                </div>
207                            }.into_any()
208                        } else {
209                            view! { <div></div> }.into_any()
210                        }}
211                        {if !validation.warnings.is_empty() {
212                            view! {
213                                <div class="strength-warnings">
214                                    {validation.warnings.into_iter().map(|warning| {
215                                        view! { <div class="warning">{warning}</div> }
216                                    }).collect::<Vec<_>>()}
217                                </div>
218                            }.into_any()
219                        } else {
220                            view! { <div></div> }.into_any()
221                        }}
222                    </div>
223                }.into_any()
224            } else {
225                view! { <div></div> }.into_any()
226            }}
227        </div>
228    }
229}
230
231/// Password requirements display component
232#[component]
233pub fn PasswordRequirements(
234    /// Strength requirements
235    #[prop(optional)]
236    requirements: Option<PasswordStrengthRequirements>,
237    /// Whether to show as checklist
238    #[prop(optional)]
239    show_checklist: Option<bool>,
240    /// Additional CSS classes
241    #[prop(optional)]
242    class: Option<String>,
243    /// Inline styles
244    #[prop(optional)]
245    style: Option<String>,
246) -> impl IntoView {
247    let requirements = requirements.unwrap_or_default();
248    let show_checklist = show_checklist.unwrap_or(true);
249    let class = format!("password-requirements {}", class.unwrap_or_default());
250    let style = style.unwrap_or_default();
251
252    view! {
253        <div class=class style=style>
254            <h4>"Password Requirements"</h4>
255            <ul>
256                <li>
257                    {if show_checklist {
258                        view! { <span class="checkmark">"✓"</span> }.into_any()
259                    } else {
260                        view! { <div></div> }.into_any()
261                    }}
262                    {format!("At least {} characters", requirements.min_length)}
263                </li>
264                {if requirements.require_uppercase {
265                    view! {
266                        <li>
267                            {if show_checklist {
268                                view! { <span class="checkmark">"✓"</span> }.into_any()
269                            } else {
270                                view! { <div></div> }.into_any()
271                            }}
272                            "At least one uppercase letter"
273                        </li>
274                    }.into_any()
275                } else {
276                    view! { <div></div> }.into_any()
277                }}
278                {if requirements.require_lowercase {
279                    view! {
280                        <li>
281                            {if show_checklist {
282                                view! { <span class="checkmark">"✓"</span> }.into_any()
283                            } else {
284                                view! { <div></div> }.into_any()
285                            }}
286                            "At least one lowercase letter"
287                        </li>
288                    }.into_any()
289                } else {
290                    view! { <div></div> }.into_any()
291                }}
292                {if requirements.require_numbers {
293                    view! {
294                        <li>
295                            {if show_checklist {
296                                view! { <span class="checkmark">"✓"</span> }.into_any()
297                            } else {
298                                view! { <div></div> }.into_any()
299                            }}
300                            "At least one number"
301                        </li>
302                    }.into_any()
303                } else {
304                    view! { <div></div> }.into_any()
305                }}
306                {if requirements.require_symbols {
307                    view! {
308                        <li>
309                            {if show_checklist {
310                                view! { <span class="checkmark">"✓"</span> }.into_any()
311                            } else {
312                                view! { <div></div> }.into_any()
313                            }}
314                            "At least one symbol"
315                        </li>
316                    }.into_any()
317                } else {
318                    view! { <div></div> }.into_any()
319                }}
320            </ul>
321        </div>
322    }
323}
324
325/// Helper function to validate password strength
326fn validate_password(
327    password: &str,
328    requirements: &PasswordStrengthRequirements,
329) -> PasswordValidation {
330    let mut errors = Vec::new();
331    let warnings = Vec::new();
332    let mut strength_score = 0;
333
334    // Check minimum length
335    if password.len() < requirements.min_length {
336        errors.push(format!(
337            "Password must be at least {} characters long",
338            requirements.min_length
339        ));
340    }
341
342    // Check for uppercase letters
343    if requirements.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
344        errors.push("Password must contain at least one uppercase letter".to_string());
345    } else if password.chars().any(|c| c.is_uppercase()) {
346        strength_score += 1;
347    }
348
349    // Check for lowercase letters
350    if requirements.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
351        errors.push("Password must contain at least one lowercase letter".to_string());
352    } else if password.chars().any(|c| c.is_lowercase()) {
353        strength_score += 1;
354    }
355
356    // Check for numbers
357    if requirements.require_numbers && !password.chars().any(|c| c.is_numeric()) {
358        errors.push("Password must contain at least one number".to_string());
359    } else if password.chars().any(|c| c.is_numeric()) {
360        strength_score += 1;
361    }
362
363    // Check for symbols
364    if requirements.require_symbols && !password.chars().any(|c| !c.is_alphanumeric()) {
365        errors.push("Password must contain at least one symbol".to_string());
366    } else if password.chars().any(|c| !c.is_alphanumeric()) {
367        strength_score += 1;
368    }
369
370    // Determine strength level
371    let strength_level = match strength_score {
372        0..=1 => PasswordStrengthLevel::VeryWeak,
373        2 => PasswordStrengthLevel::Weak,
374        3 => PasswordStrengthLevel::Fair,
375        4 => PasswordStrengthLevel::Good,
376        5 => PasswordStrengthLevel::Strong,
377        _ => PasswordStrengthLevel::Strong,
378    };
379
380    // Check if password meets minimum strength requirements
381    let is_valid = errors.is_empty() && strength_score >= requirements.min_strength_score;
382
383    PasswordValidation {
384        is_valid,
385        strength_score,
386        strength_level,
387        errors,
388        warnings,
389    }
390}
391
392#[cfg(test)]
393mod tests {
394
395    use crate::{PasswordStrengthLevel, PasswordStrengthRequirements, PasswordValidation};
396use crate::utils::{merge_optional_classes, generate_id};
397
398    // Component structure tests
399    #[test]
400    fn test_password_toggle_field_component_creation() {}
401
402    #[test]
403    fn test_password_strength_indicator_component_creation() {}
404
405    #[test]
406    fn test_password_requirements_component_creation() {}
407
408    // Data structure tests
409    #[test]
410    fn test_password_strength_requirements_struct() {
411        let requirements = PasswordStrengthRequirements {
412            min_length: 8,
413            require_uppercase: true,
414            require_lowercase: true,
415            require_numbers: true,
416            require_symbols: true,
417            min_strength_score: 4,
418        };
419        assert_eq!(requirements.min_length, 8);
420        assert!(requirements.require_uppercase);
421        assert!(requirements.require_lowercase);
422        assert!(requirements.require_numbers);
423        assert!(requirements.require_symbols);
424        assert_eq!(requirements.min_strength_score, 4);
425    }
426
427    #[test]
428    fn test_password_strength_requirements_default() {
429        let requirements = PasswordStrengthRequirements::default();
430        assert_eq!(requirements.min_length, 0);
431        assert!(!requirements.require_uppercase);
432        assert!(!requirements.require_lowercase);
433        assert!(!requirements.require_numbers);
434        assert!(!requirements.require_symbols);
435        assert_eq!(requirements.min_strength_score, 0);
436    }
437
438    #[test]
439    fn test_password_validation_struct() {
440        let validation = PasswordValidation {
441            is_valid: true,
442            strength_score: 4,
443            strength_level: PasswordStrengthLevel::Good,
444            errors: Vec::new(),
445            warnings: Vec::new(),
446        };
447        assert!(validation.is_valid);
448        assert_eq!(validation.strength_score, 4);
449        assert_eq!(validation.strength_level, PasswordStrengthLevel::Good);
450        assert!(validation.errors.is_empty());
451        assert!(validation.warnings.is_empty());
452    }
453
454    #[test]
455    fn test_password_validation_default() {
456        let validation = PasswordValidation::default();
457        assert!(!validation.is_valid);
458        assert_eq!(validation.strength_score, 0);
459        assert_eq!(validation.strength_level, PasswordStrengthLevel::VeryWeak);
460        assert!(validation.errors.is_empty());
461        assert!(validation.warnings.is_empty());
462    }
463
464    // Props and state tests
465    #[test]
466    fn test_password_toggle_field_props_handling() {}
467
468    #[test]
469    fn test_password_toggle_field_value_handling() {}
470
471    #[test]
472    fn test_password_toggle_field_placeholder() {}
473
474    #[test]
475    fn test_password_toggle_fielddisabled_state() {}
476
477    #[test]
478    fn test_password_toggle_fieldrequired_state() {}
479
480    #[test]
481    fn test_password_toggle_field_readonly_state() {}
482
483    #[test]
484    fn test_password_toggle_field_visibility_state() {}
485
486    #[test]
487    fn test_password_toggle_field_length_constraints() {}
488
489    // Event handling tests
490    #[test]
491    fn test_password_toggle_field_change_callback() {}
492
493    #[test]
494    fn test_password_toggle_field_visibility_toggle() {}
495
496    #[test]
497    fn test_password_toggle_field_focus_callback() {}
498
499    #[test]
500    fn test_password_toggle_field_blur_callback() {}
501
502    #[test]
503    fn test_password_toggle_field_validation_callback() {}
504
505    // Password validation tests
506    #[test]
507    fn test_password_validation_min_length() {}
508
509    #[test]
510    fn test_password_validation_uppercase_requirement() {}
511
512    #[test]
513    fn test_password_validation_lowercase_requirement() {}
514
515    #[test]
516    fn test_password_validation_numbers_requirement() {}
517
518    #[test]
519    fn test_password_validation_symbols_requirement() {}
520
521    #[test]
522    fn test_password_validation_strength_scoring() {}
523
524    #[test]
525    fn test_password_validation_strength_levels() {}
526
527    // Security tests
528    #[test]
529    fn test_password_toggle_field_security() {}
530
531    #[test]
532    fn test_password_toggle_field_input_type_switching() {}
533
534    #[test]
535    fn test_password_toggle_field_aria_labels() {}
536
537    // Accessibility tests
538    #[test]
539    fn test_password_toggle_field_accessibility() {}
540
541    #[test]
542    fn test_password_toggle_field_keyboard_navigation() {}
543
544    #[test]
545    fn test_password_toggle_field_screen_reader_support() {}
546
547    #[test]
548    fn test_password_toggle_field_focus_management() {}
549
550    // Strength indicator tests
551    #[test]
552    fn test_password_strength_indicator_display() {}
553
554    #[test]
555    fn test_password_strength_indicator_colors() {}
556
557    #[test]
558    fn test_password_strength_indicator_details() {}
559
560    // Requirements display tests
561    #[test]
562    fn test_password_requirements_display() {}
563
564    #[test]
565    fn test_password_requirements_checklist() {}
566
567    #[test]
568    fn test_password_requirements_customization() {}
569
570    // Integration tests
571    #[test]
572    fn test_password_toggle_field_full_workflow() {}
573
574    #[test]
575    fn test_password_toggle_field_with_strength_indicator() {}
576
577    #[test]
578    fn test_password_toggle_field_with_requirements() {}
579
580    // Edge case tests
581    #[test]
582    fn test_password_toggle_field_empty_password() {}
583
584    #[test]
585    fn test_password_toggle_field_very_long_password() {}
586
587    #[test]
588    fn test_password_toggle_field_special_characters() {}
589
590    #[test]
591    fn test_password_toggle_field_unicode_characters() {}
592
593    // Performance tests
594    #[test]
595    fn test_password_toggle_field_validation_performance() {}
596
597    #[test]
598    fn test_password_toggle_field_rendering_performance() {}
599
600    // Styling tests
601    #[test]
602    fn test_password_toggle_field_custom_classes() {}
603
604    #[test]
605    fn test_password_toggle_field_custom_styles() {}
606
607    #[test]
608    fn test_password_toggle_field_responsive_design() {}
609
610    #[test]
611    fn test_password_toggle_field_icon_display() {}
612}