From 0ab6aaac1ca698a2fcbbcecdacf40ec028c37b0d Mon Sep 17 00:00:00 2001 From: Adrien Date: Thu, 21 Mar 2024 21:12:49 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20PasswordTextInput=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TextInput component has been reworked to factorize some pieces of code with PasswordTextInput. --- src/components/text_input.rs | 179 ++++++++++++++++++++++++++++----- src/components/text_input.scss | 143 ++++++++++++++++++++------ 2 files changed, 267 insertions(+), 55 deletions(-) diff --git a/src/components/text_input.rs b/src/components/text_input.rs index 6987721..8d9115c 100644 --- a/src/components/text_input.rs +++ b/src/components/text_input.rs @@ -1,8 +1,23 @@ use dioxus::prelude::*; +use dioxus_free_icons::icons::io_icons::IoEye; +use dioxus_free_icons::icons::io_icons::IoEyeOff; +use dioxus_free_icons::Icon; + +use super::icons::Pyramid; turf::style_sheet!("src/components/text_input.scss"); -#[derive(Debug)] +pub trait InputPropsData {} + +#[derive(Props)] +pub struct InputProps<'a, D: InputPropsData + 'a> { + value: Option<&'a str>, + placeholder: Option<&'a str>, + oninput: Option>>, + state: Option<&'a UseRef>, +} + +#[derive(PartialEq)] pub struct TextInputState { pub is_valid: bool, pub helper_text: Option, @@ -27,32 +42,27 @@ impl TextInputState { } } -#[derive(Props)] -pub struct TextInputProps<'a> { - id: Option<&'a str>, - r#type: Option<&'a str>, - value: Option<&'a str>, - placeholder: Option<&'a str>, - oninput: Option>>, - state: Option<&'a UseRef>, -} +impl InputPropsData for TextInputState {} -pub fn TextInput<'a>(cx: Scope<'a, TextInputProps<'a>>) -> Element<'a> { - let mut level_class = ""; - let mut helper_text: String = "".to_string(); +pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<'a> { + let mut criticity_class = ""; + let mut helper_text = "".to_string(); match cx.props.state { Some(state) => { - if !state.read().is_valid { - level_class = ClassName::INVALID; + let state = state.read(); + if !state.is_valid { + criticity_class = ClassName::INVALID; } - if let Some(text) = &state.read().helper_text { + if let Some(text) = &state.helper_text { helper_text = text.to_string(); } } None => {} } + let input_classes_str = [ClassName::TEXT_INPUT_INPUT, criticity_class].join(" "); + cx.render(rsx! { style { STYLE_SHEET }, @@ -60,29 +70,148 @@ pub fn TextInput<'a>(cx: Scope<'a, TextInputProps<'a>>) -> Element<'a> { class: ClassName::TEXT_INPUT, input { - class: level_class, - - id: cx.props.id.unwrap_or(""), + class: "{input_classes_str}", + r#type: "text", + placeholder: cx.props.placeholder, + value: cx.props.value, oninput: move |evt| { if let Some(cb) = &cx.props.oninput { cb.call(evt); } }, - r#type: cx.props.r#type, - placeholder: cx.props.placeholder, - value: cx.props.value, }, div { - class: ClassName::HELPER_TEXT, + class: ClassName::TEXT_INPUT_HELPER_TEXT, p { - class: level_class, - + class: criticity_class, helper_text } } } }) } + +#[derive(PartialEq, Props)] +pub struct PasswordInputState { + text_input_state: TextInputState, + #[props(default = 0.0)] + pub score: f64, +} + +impl PasswordInputState { + pub fn new() -> Self { + PasswordInputState { + text_input_state: TextInputState::new(), + score: 0.0, + } + } + + pub fn reset(&mut self) { + self.text_input_state.reset() + } + + pub fn invalidate(&mut self, helper_text: String) { + self.text_input_state.invalidate(helper_text) + } +} + +impl InputPropsData for PasswordInputState {} + +pub fn PasswordTextInput<'a>(cx: Scope<'a, InputProps<'a, PasswordInputState>>) -> Element<'a> { + let mut criticity_class = ""; + let mut helper_text: String = "".to_string(); + let mut score: Option = None; + + let show_password = use_state(cx, || false); + + match cx.props.state { + Some(state) => { + let state = state.read(); + if !state.text_input_state.is_valid { + criticity_class = ClassName::INVALID; + } + if let Some(text) = &state.text_input_state.helper_text { + helper_text = text.to_string(); + } + score = Some(state.score); + } + None => {} + } + + let text_input_classes = [ + ClassName::PASSWORD_TEXT_INPUT, + if score.is_none() { + ClassName::NO_STRENGTH + } else { + "" + }, + ] + .join(" "); + let input_classes = [ClassName::PASSWORD_TEXT_INPUT_INPUT, criticity_class].join(" "); + + cx.render(rsx! { + style { STYLE_SHEET }, + + div { + class: "{text_input_classes}", + + input { + class: "{input_classes}", + r#type: if **show_password { "text" } else { "password" }, + placeholder: cx.props.placeholder, + value: cx.props.value, + + oninput: move |evt| { + if let Some(cb) = &cx.props.oninput { + cb.call(evt); + } + }, + }, + + if let Some(score) = score { + rsx!( + div { + class: ClassName::PASSWORD_TEXT_INPUT_STRENGTH_LEVEL, + Pyramid { + ratio: score, + } + } + ) + }, + + div { + class: ClassName::PASSWORD_TEXT_INPUT_SHOW_TOGGLE, + onclick: move |_| { + show_password.set(!**show_password); + }, + + if **show_password { + rsx!( + Icon { + icon: IoEyeOff, + } + ) + } + else { + rsx!( + Icon { + icon: IoEye, + } + ) + } + }, + + div { + class: ClassName::PASSWORD_TEXT_INPUT_HELPER_TEXT, + + p { + class: criticity_class, + helper_text + } + }, + } + }) +} diff --git a/src/components/text_input.scss b/src/components/text_input.scss index 1825166..088fe46 100644 --- a/src/components/text_input.scss +++ b/src/components/text_input.scss @@ -1,47 +1,130 @@ @import "../_base.scss" -.root { +%base-text-input { + $horizontal-padding: 1vw; + height: 100%; + width: calc(100% - (2 * $horizontal-padding)); - input { - $horizontal-padding: 1vw; + border: $border-normal; + border-color: $color-primary-90; + border-radius: $border-radius; - padding-left: $horizontal-padding; - padding-right: $horizontal-padding; - padding-top: 0px; - padding-bottom: 0px; + padding-left: $horizontal-padding; + padding-right: $horizontal-padding; + padding-top: 0px; + padding-bottom: 0px; +} - height: calc(100% - (2 * ($border-normal-width))); - width: calc(100% - (2 * ($border-normal-width + $horizontal-padding))); +%base-input { + $horizontal-padding: 1vw; - margin: 0; + height: 100%; + width: 100%; - border: $border-normal; - border-color: $color-primary-90; - border-radius: $border-radius; + margin: 0; + padding: 0; + border: 0; - font-size: 2vh; + font-size: 2vh; - &.invalid { - border-color: $color-critical; - } + &:focus { + outline: none; } - .helper-text { + &.invalid { + border-color: $color-critical; + } +} + +%base-helper-text { + margin: 0; + margin-top: 0.3vh; + + font-size: 1.2vh; + + color: $color-primary-90; + + p { margin: 0; - margin-top: 0.3vh; - font-size: 1.2vh; - - color: $color-primary-90; - - p { - margin: 0; - padding-left: 1vw; - - &.invalid { - color: $color-critical; - } + &.invalid { + color: $color-critical; } } } + +.text-input { + @extend %base-text-input; + + &__input { + @extend %base-input; + } + + &__helper-text { + @extend %base-helper-text; + } +} + +.password-text-input { + @extend %base-text-input; + + display: grid; + grid-template-columns: auto 7.5% 1% 7.5%; + grid-template-rows: 100%; + grid-template-areas: + "input strength . toggle" + "helper helper helper helper" + ; + + transition: $transition-duration; + + &.no-strength { + grid-template-columns: auto 0% 0% 7.5%; + } + + &__input { + @extend %base-input; + + grid-area: input; + } + + %inner { + display: flex; + align-items: center; + justify-content: center; + } + + &__strength-level { + @extend %inner; + + grid-area: strength; + + p { + margin: 0; + text-align: center; + width: 100%; + + font-size: 2vh; + } + } + + &__show-toggle { + @extend %inner; + + grid-area: toggle; + + svg { + height: 100%; + width: 100%; + + color: $color-secondary-100; + } + } + + &__helper-text { + @extend %base-helper-text; + + grid-area: helper; + } +}