at main 128 lines 4.1 kB view raw
1//! Hex colour input with preview swatch. 2 3use dioxus::prelude::*; 4 5/// Props for HexColourInput. 6#[derive(Props, Clone, PartialEq)] 7pub struct HexColourInputProps { 8 /// Current hex value (without #). 9 pub value: String, 10 /// Callback when value changes. 11 pub onchange: EventHandler<String>, 12 /// Label for the input. 13 #[props(default)] 14 pub label: Option<String>, 15 /// Placeholder text. 16 #[props(default = "000000".to_string())] 17 pub placeholder: String, 18 /// Callback when input receives focus. 19 #[props(default)] 20 pub onfocus: Option<EventHandler<FocusEvent>>, 21} 22 23/// A hex colour input with a colour preview swatch. 24#[component] 25pub fn HexColourInput(props: HexColourInputProps) -> Element { 26 // Normalise the value for display (expand 3-char to 6-char). 27 let display_value = normalise_hex(&props.value); 28 29 // Check if we have a valid hex to display in the swatch. 30 let swatch_colour = if is_valid_hex(&display_value) { 31 format!("#{display_value}") 32 } else { 33 // Grey for invalid hex. 34 "var(--color-muted)".to_string() 35 }; 36 37 rsx! { 38 document::Link { rel: "stylesheet", href: asset!("/assets/styling/hex-colour-input.css") } 39 40 div { class: "hex-colour-input", 41 if let Some(label) = &props.label { 42 label { class: "hex-colour-input-label", "{label}" } 43 } 44 45 div { class: "hex-colour-input-field", 46 div { 47 class: "hex-colour-input-swatch", 48 style: "background-color: {swatch_colour}", 49 } 50 span { class: "hex-colour-input-hash", "#" } 51 input { 52 class: "hex-colour-input-text", 53 r#type: "text", 54 maxlength: "6", 55 placeholder: props.placeholder.clone(), 56 value: display_value.clone(), 57 oninput: move |e| { 58 // Filter to hex chars only and uppercase. 59 let filtered: String = e 60 .value() 61 .chars() 62 .filter(|c| c.is_ascii_hexdigit()) 63 .take(6) 64 .collect::<String>() 65 .to_uppercase(); 66 props.onchange.call(filtered); 67 }, 68 onfocus: move |e| { 69 if let Some(handler) = &props.onfocus { 70 handler.call(e); 71 } 72 }, 73 } 74 } 75 } 76 } 77} 78 79/// Normalise 3-char hex to 6-char (e.g., "ABC" -> "AABBCC"). 80fn normalise_hex(hex: &str) -> String { 81 let hex = hex.to_uppercase(); 82 if hex.len() == 3 && hex.chars().all(|c| c.is_ascii_hexdigit()) { 83 hex.chars().flat_map(|c| [c, c]).collect() 84 } else { 85 hex 86 } 87} 88 89/// Check if valid 6-digit hex. 90fn is_valid_hex(hex: &str) -> bool { 91 hex.len() == 6 && hex.chars().all(|c| c.is_ascii_hexdigit()) 92} 93 94#[cfg(test)] 95mod tests { 96 use super::*; 97 98 #[test] 99 fn test_normalise_hex_3_char() { 100 assert_eq!(normalise_hex("ABC"), "AABBCC"); 101 assert_eq!(normalise_hex("abc"), "AABBCC"); 102 assert_eq!(normalise_hex("123"), "112233"); 103 } 104 105 #[test] 106 fn test_normalise_hex_6_char() { 107 assert_eq!(normalise_hex("AABBCC"), "AABBCC"); 108 assert_eq!(normalise_hex("aabbcc"), "AABBCC"); 109 } 110 111 #[test] 112 fn test_normalise_hex_invalid() { 113 // Invalid 3-char (not all hex) stays as-is. 114 assert_eq!(normalise_hex("GHI"), "GHI"); 115 // Other lengths stay as-is. 116 assert_eq!(normalise_hex("AB"), "AB"); 117 assert_eq!(normalise_hex("ABCDE"), "ABCDE"); 118 } 119 120 #[test] 121 fn test_is_valid_hex() { 122 assert!(is_valid_hex("AABBCC")); 123 assert!(is_valid_hex("123456")); 124 assert!(!is_valid_hex("ABC")); // Too short. 125 assert!(!is_valid_hex("AABBCCDD")); // Too long. 126 assert!(!is_valid_hex("GHIJKL")); // Not hex. 127 } 128}