radix_leptos_primitives/components/
password_toggle_field.rs1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4use wasm_bindgen::JsCast;
5
6#[component]
8pub fn PasswordToggleField(
9 #[prop(optional)]
11 value: Option<String>,
12 #[prop(optional)]
14 placeholder: Option<String>,
15 #[prop(optional)]
17 disabled: Option<bool>,
18 #[prop(optional)]
20 required: Option<bool>,
21 #[prop(optional)]
23 readonly: Option<bool>,
24 #[prop(optional)]
26 visible: Option<bool>,
27 #[prop(optional)]
29 min_length: Option<usize>,
30 #[prop(optional)]
32 max_length: Option<usize>,
33 #[prop(optional)]
35 strength_requirements: Option<PasswordStrengthRequirements>,
36 #[prop(optional)]
38 on_change: Option<Callback<String>>,
39 #[prop(optional)]
41 on_visibility_toggle: Option<Callback<bool>>,
42 #[prop(optional)]
44 on_focus: Option<Callback<()>>,
45 #[prop(optional)]
47 on_blur: Option<Callback<()>>,
48 #[prop(optional)]
50 on_validation: Option<Callback<PasswordValidation>>,
51 #[prop(optional)]
53 class: Option<String>,
54 #[prop(optional)]
56 style: Option<String>,
57 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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Default)]
138pub enum PasswordStrengthLevel {
139 #[default]
140 VeryWeak,
141 Weak,
142 Fair,
143 Good,
144 Strong,
145}
146
147#[component]
149pub fn PasswordStrengthIndicator(
150 #[prop(optional)]
152 password: Option<String>,
153 #[prop(optional)]
155 requirements: Option<PasswordStrengthRequirements>,
156 #[prop(optional)]
158 show_details: Option<bool>,
159 #[prop(optional)]
161 class: Option<String>,
162 #[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#[component]
233pub fn PasswordRequirements(
234 #[prop(optional)]
236 requirements: Option<PasswordStrengthRequirements>,
237 #[prop(optional)]
239 show_checklist: Option<bool>,
240 #[prop(optional)]
242 class: Option<String>,
243 #[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
325fn 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 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
595 fn test_password_toggle_field_validation_performance() {}
596
597 #[test]
598 fn test_password_toggle_field_rendering_performance() {}
599
600 #[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}