Add PasswordTextInput component

The TextInput component has been reworked to factorize some pieces of code with PasswordTextInput.
This commit is contained in:
2024-03-21 21:12:49 +01:00
parent 89b1f10b6e
commit 0ab6aaac1c
2 changed files with 267 additions and 55 deletions

View File

@@ -1,8 +1,23 @@
use dioxus::prelude::*; 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"); 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<EventHandler<'a, Event<FormData>>>,
state: Option<&'a UseRef<D>>,
}
#[derive(PartialEq)]
pub struct TextInputState { pub struct TextInputState {
pub is_valid: bool, pub is_valid: bool,
pub helper_text: Option<String>, pub helper_text: Option<String>,
@@ -27,32 +42,27 @@ impl TextInputState {
} }
} }
#[derive(Props)] impl InputPropsData for TextInputState {}
pub struct TextInputProps<'a> {
id: Option<&'a str>,
r#type: Option<&'a str>,
value: Option<&'a str>,
placeholder: Option<&'a str>,
oninput: Option<EventHandler<'a, Event<FormData>>>,
state: Option<&'a UseRef<TextInputState>>,
}
pub fn TextInput<'a>(cx: Scope<'a, TextInputProps<'a>>) -> Element<'a> { pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<'a> {
let mut level_class = ""; let mut criticity_class = "";
let mut helper_text: String = "".to_string(); let mut helper_text = "".to_string();
match cx.props.state { match cx.props.state {
Some(state) => { Some(state) => {
if !state.read().is_valid { let state = state.read();
level_class = ClassName::INVALID; 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(); helper_text = text.to_string();
} }
} }
None => {} None => {}
} }
let input_classes_str = [ClassName::TEXT_INPUT_INPUT, criticity_class].join(" ");
cx.render(rsx! { cx.render(rsx! {
style { STYLE_SHEET }, style { STYLE_SHEET },
@@ -60,29 +70,148 @@ pub fn TextInput<'a>(cx: Scope<'a, TextInputProps<'a>>) -> Element<'a> {
class: ClassName::TEXT_INPUT, class: ClassName::TEXT_INPUT,
input { input {
class: level_class, class: "{input_classes_str}",
r#type: "text",
id: cx.props.id.unwrap_or(""), placeholder: cx.props.placeholder,
value: cx.props.value,
oninput: move |evt| { oninput: move |evt| {
if let Some(cb) = &cx.props.oninput { if let Some(cb) = &cx.props.oninput {
cb.call(evt); cb.call(evt);
} }
}, },
r#type: cx.props.r#type,
placeholder: cx.props.placeholder,
value: cx.props.value,
}, },
div { div {
class: ClassName::HELPER_TEXT, class: ClassName::TEXT_INPUT_HELPER_TEXT,
p { p {
class: level_class, class: criticity_class,
helper_text 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<f64> = 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
}
},
}
})
}

View File

@@ -1,47 +1,130 @@
@import "../_base.scss" @import "../_base.scss"
.root { %base-text-input {
$horizontal-padding: 1vw;
height: 100%; height: 100%;
width: calc(100% - (2 * $horizontal-padding));
input { border: $border-normal;
$horizontal-padding: 1vw; border-color: $color-primary-90;
border-radius: $border-radius;
padding-left: $horizontal-padding; padding-left: $horizontal-padding;
padding-right: $horizontal-padding; padding-right: $horizontal-padding;
padding-top: 0px; padding-top: 0px;
padding-bottom: 0px; padding-bottom: 0px;
}
height: calc(100% - (2 * ($border-normal-width))); %base-input {
width: calc(100% - (2 * ($border-normal-width + $horizontal-padding))); $horizontal-padding: 1vw;
margin: 0; height: 100%;
width: 100%;
border: $border-normal; margin: 0;
border-color: $color-primary-90; padding: 0;
border-radius: $border-radius; border: 0;
font-size: 2vh; font-size: 2vh;
&.invalid { &:focus {
border-color: $color-critical; 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: 0;
margin-top: 0.3vh;
font-size: 1.2vh; &.invalid {
color: $color-critical;
color: $color-primary-90;
p {
margin: 0;
padding-left: 1vw;
&.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;
}
}