From fb01c3e80c1accb78fd85709f27e5c32192306af Mon Sep 17 00:00:00 2001 From: b-avb Date: Mon, 28 Oct 2024 09:45:55 -0500 Subject: [PATCH] feat: base UI for new components --- src/components/atoms/autocomplete.rs | 134 ++++++++++++++++++++++ src/components/atoms/autocomplete_item.rs | 14 +++ src/components/atoms/avatar.rs | 6 +- src/components/atoms/button.rs | 2 +- src/components/atoms/checkbox.rs | 28 +++++ src/components/atoms/combo_input.rs | 24 ++-- src/components/atoms/dropdown.rs | 42 ++++++- src/components/atoms/file_drop_area.rs | 76 ++++++++++++ src/components/atoms/icon_button.rs | 6 + src/components/atoms/input.rs | 36 +++++- src/components/atoms/key_value.rs | 8 +- src/components/atoms/mod.rs | 12 ++ src/components/atoms/popover_button.rs | 35 ++++++ src/components/atoms/step.rs | 3 + src/components/atoms/switch.rs | 30 +++++ src/components/atoms/tab.rs | 6 +- src/components/atoms/textarea.rs | 4 + 17 files changed, 434 insertions(+), 32 deletions(-) create mode 100644 src/components/atoms/autocomplete.rs create mode 100644 src/components/atoms/autocomplete_item.rs create mode 100644 src/components/atoms/checkbox.rs create mode 100644 src/components/atoms/file_drop_area.rs create mode 100644 src/components/atoms/popover_button.rs create mode 100644 src/components/atoms/switch.rs diff --git a/src/components/atoms/autocomplete.rs b/src/components/atoms/autocomplete.rs new file mode 100644 index 0000000..7251c60 --- /dev/null +++ b/src/components/atoms/autocomplete.rs @@ -0,0 +1,134 @@ +use crate::components::atoms::Input; +use dioxus::prelude::*; + +use super::dropdown::ElementSize; +#[derive(PartialEq, Debug, Clone, Default)] +pub struct AutocompleteItem { + pub key: String, + pub value: String, +} +#[derive(PartialEq, Props, Clone)] +pub struct AutocompleteProps { + value: Option, + label: Option, + on_change: EventHandler, + on_input: EventHandler, + #[props(!optional)] + default: Option, + #[props(default = false)] + disabled: bool, + #[props(default = false)] + reverse: bool, + #[props(default = "".to_string())] + class: String, + placeholder: String, + help: Option, + left_icon: Option, + #[props(default = ElementSize::Medium)] + size: ElementSize, + body: Vec, + add_element: Option, +} +pub fn Autocomplete(props: AutocompleteProps) -> Element { + let mut is_active = use_signal::(|| false); + let mut search = use_signal::(|| String::new()); + + let disabled = if props.disabled { + "button--disabled" + } else { + "" + }; + let placeholder = if let None = props.value { + "autocomplete__placeholder" + } else { + "" + }; + let size = match props.size { + ElementSize::Big => "autocomplete__container--big", + ElementSize::Medium => "autocomplete__container--medium", + ElementSize::Small => "autocomplete__container--small", + }; + rsx!( + section { class: "autocomplete {props.class}", + if let Some(value) = props.label { + label { class: "input__label", + "{value}" + } + } + if is_active() { + Input { + message: search(), + placeholder: "".to_string(), + size: ElementSize::Small, + autofocus: true, + focus: is_active(), + error: None, + on_input: move |event: Event| { + search.set(event.value()); + props.on_input.call(event); + }, + on_keypress: move |_| {}, + on_click: move |_| {}, + on_focus: move |_| { + is_active.set(true) + }, + on_blur: move |_| { + // is_active.set(false) + } + } + } else { + div { class: "dropdown__container {size}", + button { + class: "dropdown__wrapper {disabled}", + disabled: props.disabled, + onclick: move |_| { + if !props.disabled { + is_active.toggle(); + } + }, + onblur: move |_| { + // is_active.set(false); + }, + {props.left_icon}, + span { class: "dropdown__content", + span { class: "dropdown__value {placeholder}", + match props.value { + Some(v) => {v.value}.to_string(), + None => props.placeholder + } + } + } + } + } + } + if is_active() { + { + rsx!( + ul { + class : "autocomplete__list", + class : if props.reverse { "autocomplete__list--reverse" }, + { + props.body.into_iter().enumerate().map(| (index, item) | { + rsx!( + li { + class : "autocomplete__item", + onclick : move | _ | { + log::info!("click item"); + is_active.toggle(); + props.on_change.call(index); + }, + { item } + } + ) + }) + } + if let Some(help) = props.add_element { + div { class: "autocomplete__item", {help} } + } + } + ) + } + } + } + ) +} diff --git a/src/components/atoms/autocomplete_item.rs b/src/components/atoms/autocomplete_item.rs new file mode 100644 index 0000000..29d2c40 --- /dev/null +++ b/src/components/atoms/autocomplete_item.rs @@ -0,0 +1,14 @@ +use dioxus::prelude::*; +#[derive(PartialEq, Props, Clone)] +pub struct AutocompleteItemProps { + title: String, + // on_click: EventHandler<()>, +} +pub fn AutocompleteItemButton(props: AutocompleteItemProps) -> Element { + rsx!( + div { + class: "autocomplete__item--recipient", + span { class: "autocomplete__item__alias", "{props.title}"} + } + ) +} diff --git a/src/components/atoms/avatar.rs b/src/components/atoms/avatar.rs index 2e773a2..b5c57d2 100644 --- a/src/components/atoms/avatar.rs +++ b/src/components/atoms/avatar.rs @@ -6,6 +6,8 @@ pub enum Variant { } #[derive(PartialEq, Props, Clone)] pub struct AvatarProps { + #[props(default = "".to_string())] + class: String, name: String, size: u8, #[props(!optional)] @@ -21,10 +23,10 @@ pub fn Avatar(props: AvatarProps) -> Element { Variant::SemiRound => "avatar--semi-round", }; rsx! { - match & props.uri { Some(uri) => rsx!(img { class : "avatar {variant}", style : + match & props.uri { Some(uri) => rsx!(img { class : "avatar {variant} {props.class}", style : "{avatar_style}", src : "{uri}" }), None => { let initial : Vec < char > = props .name.chars().collect(); let initial = initial[0].to_uppercase(); rsx!(div { - class : "avatar {variant}", style : "{avatar_style}", span { class : + class : "avatar {variant} {props.class}", style : "{avatar_style}", span { class : "avatar--initial", "{initial}" } }) } } } } diff --git a/src/components/atoms/button.rs b/src/components/atoms/button.rs index bf37c05..d0b4f5c 100644 --- a/src/components/atoms/button.rs +++ b/src/components/atoms/button.rs @@ -40,7 +40,7 @@ pub fn Button(props: ButtonProps) -> Element { Some(s) => { rsx!( button { - class: "button {props.class} {variant} {size} {loading}", + class: "button {variant} {size} {loading} {props.class}", disabled: true, "{s}" } diff --git a/src/components/atoms/checkbox.rs b/src/components/atoms/checkbox.rs new file mode 100644 index 0000000..e5e1c3f --- /dev/null +++ b/src/components/atoms/checkbox.rs @@ -0,0 +1,28 @@ +use dioxus::prelude::*; +#[derive(PartialEq, Props, Clone)] +pub struct CheckboxProps { + #[props(default = "".to_string())] + class: String, + id: String, + name: String, + checked: bool, + label: String, + #[props(default = false)] + disabled: bool, + on_change: EventHandler, +} +pub fn Checkbox(props: CheckboxProps) -> Element { + rsx!( + label { + class: "container {props.class}", + input { + r#type: "checkbox", + name: props.name, + checked: props.checked, + onchange: move |_| { props.on_change.call(()) } + } + span {class: "checkmark"} + span {class: "checkbox__label", {props.label} } + } + ) +} diff --git a/src/components/atoms/combo_input.rs b/src/components/atoms/combo_input.rs index c2c3b98..d14bb8e 100644 --- a/src/components/atoms/combo_input.rs +++ b/src/components/atoms/combo_input.rs @@ -1,9 +1,7 @@ +use super::dropdown::ElementSize; +use crate::components::atoms::{dropdown::DropdownItem, input::InputType, Dropdown, Input}; use dioxus::prelude::*; use dioxus_std::{i18n::use_i18, translate}; -use crate::components::atoms::{ - dropdown::DropdownItem, input::InputType, Dropdown, Input, -}; -use super::dropdown::ElementSize; #[derive(PartialEq, Clone, Debug)] pub enum ComboInputOption { Dropdown(DropdownItem), @@ -34,16 +32,12 @@ pub fn ComboInput(props: ComboInputProps) -> Element { let mut option_value = use_signal(|| props.value.option.clone()); let mut input_value = use_signal::(|| props.value.input.clone()); let mut items = vec![]; - let dropdown_options = use_signal::< - Vec, - >(|| { + let dropdown_options = use_signal::>(|| { let Some(options) = props.options else { - return vec![ - DropdownItem { - key: "Wallet".to_string(), - value: translate!(i18, "onboard.invite.form.wallet.label"), - }, - ]; + return vec![DropdownItem { + key: "Wallet".to_string(), + value: translate!(i18, "onboard.invite.form.wallet.label"), + }]; }; options }); @@ -68,6 +62,7 @@ pub fn ComboInput(props: ComboInputProps) -> Element { }, on_keypress: move |_| {}, on_click: move |_| {}, + on_focus: move |_| {}, on_blur: move |_| {} } ), ComboInputOption::Dropdown(value) => rsx!( @@ -105,7 +100,8 @@ pub fn ComboInput(props: ComboInputProps) -> Element { }) }, on_keypress: move |_| {}, - on_click: move |_| {} + on_click: move |_| {}, + on_focus: move |_| {}, on_blur: move |_| {} } } ) diff --git a/src/components/atoms/dropdown.rs b/src/components/atoms/dropdown.rs index d33f92f..1cf4075 100644 --- a/src/components/atoms/dropdown.rs +++ b/src/components/atoms/dropdown.rs @@ -1,5 +1,5 @@ -use dioxus::prelude::*; use crate::components::atoms::{Arrow, Icon}; +use dioxus::prelude::*; #[derive(PartialEq, Debug, Clone, Default)] pub struct DropdownItem { pub key: String, @@ -14,6 +14,7 @@ pub enum ElementSize { #[derive(PartialEq, Props, Clone)] pub struct DropdownProps { value: Option, + value_help: Option, label: Option, on_change: EventHandler, #[props(!optional)] @@ -23,14 +24,24 @@ pub struct DropdownProps { #[props(default = "".to_string())] class: String, placeholder: String, + help: Option, + left_icon: Option, #[props(default = ElementSize::Medium)] size: ElementSize, body: Vec, } pub fn Dropdown(props: DropdownProps) -> Element { let mut is_active = use_signal::(|| false); - let disabled = if props.disabled { "button--disabled" } else { "" }; - let placeholder = if let None = props.value { "dropdown__placeholder" } else { "" }; + let disabled = if props.disabled { + "button--disabled" + } else { + "" + }; + let placeholder = if let None = props.value { + "dropdown__placeholder" + } else { + "" + }; let size = match props.size { ElementSize::Big => "dropdown__container--big", ElementSize::Medium => "dropdown__container--medium", @@ -50,11 +61,29 @@ pub fn Dropdown(props: DropdownProps) -> Element { is_active.toggle(); } }, + onblur: move |_| { + // is_active.set(false); + }, + {props.left_icon}, span { class: "dropdown__content", span { class: "dropdown__value {placeholder}", match props.value { - Some(v) => {v.value}.to_string(), - None => props.placeholder + Some(v) => { + match props.value_help { + Some(value_help) => rsx!( + div { class: "card-send2__info", + h5 { class: "card-send3__info__title", + {v.value} + } + p { class: "card-send3__info__description", + {value_help} + } + } + ), + None => rsx!({v.value}), + } + }, + None => rsx!({props.placeholder}) } } Icon { @@ -73,6 +102,9 @@ pub fn Dropdown(props: DropdownProps) -> Element { is_active.toggle(); props.on_change.call(index) }, { item } }) }) } }) } } } + if let Some(help) = props.help { + div { class: "input--help", "{help}" } + } } ) } diff --git a/src/components/atoms/file_drop_area.rs b/src/components/atoms/file_drop_area.rs new file mode 100644 index 0000000..17fb00a --- /dev/null +++ b/src/components/atoms/file_drop_area.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use dioxus::prelude::*; +use dioxus_elements::{FileEngine, HasFileData}; + +use crate::components::atoms::{FileListLine, Icon}; + +struct UploadedFile { + name: String, +} + +#[derive(PartialEq, Props, Clone)] +pub struct FileDropAreaProps { + label: Option, + help: Option, + #[props(default = false)] + show_files_list: bool, +} + +pub fn FileDropArea(props: FileDropAreaProps) -> Element { + let mut files_uploaded = use_signal(|| Vec::new() as Vec); + let mut hovered = use_signal(|| false); + + let read_files = move |file_engine: Arc| async move { + let files = file_engine.files(); + for file_name in &files { + if let Some(_) = file_engine.read_file_to_string(file_name).await { + files_uploaded.write().push(UploadedFile { + name: file_name.clone(), + }); + } + } + }; + + rsx! { + section { class: "file-drop", + if let Some(value) = props.label { + label { class: "input__label", + "{value}" + } + } + div { + class: "drop-zone", + id: "drop-zone", + prevent_default: "ondragover ondrop", + class: if hovered() { "drop-zone--hovered" }, + ondragover: move |_| hovered.set(true), + ondragleave: move |_| hovered.set(false), + ondrop: move |evt| async move { + hovered.set(false); + if let Some(file_engine) = evt.files() { + read_files(file_engine).await; + } + }, + Icon { icon: FileListLine, height: 28, width: 28, fill: "var(--state-brand-primary)" } + div { class: "drop-zone__wrapper", + h3 { class: "drop-zone__title", "Drag & drop bills"} + span { class: "drop-zone__description", "We can grab your recipient’s details automatically"} + } + } + if let Some(help) = props.help { + div { class: "input--help", "{help}" } + } + + if props.show_files_list { + ul { + for file in files_uploaded.read().iter().rev() { + li { + span { "{file.name}" } + } + } + } + } + } + } +} diff --git a/src/components/atoms/icon_button.rs b/src/components/atoms/icon_button.rs index 221c88f..c8d97e9 100644 --- a/src/components/atoms/icon_button.rs +++ b/src/components/atoms/icon_button.rs @@ -4,6 +4,9 @@ use super::dropdown::ElementSize; pub enum Variant { Round, SemiRound, + Ghost, + Secondary, + Danger } #[derive(PartialEq, Props, Clone)] pub struct IconButtonProps { @@ -22,6 +25,9 @@ pub fn IconButton(props: IconButtonProps) -> Element { let variant = match props.variant { Variant::Round => "icon-button--round", Variant::SemiRound => "icon-button--semi-round", + Variant::Ghost => "icon-button--ghost", + Variant::Secondary => "icon-button--secondary", + Variant::Danger => "icon-button--danger", }; let size = match props.size { ElementSize::Big => "icon-button--big", diff --git a/src/components/atoms/input.rs b/src/components/atoms/input.rs index ca705ed..e1c67f7 100644 --- a/src/components/atoms/input.rs +++ b/src/components/atoms/input.rs @@ -1,8 +1,8 @@ +use super::dropdown::ElementSize; +use crate::components::atoms::{Icon, IconButton, Search, WarningSign}; use dioxus::prelude::*; use wasm_bindgen::JsCast; use web_sys::HtmlInputElement; -use crate::components::atoms::{Icon, IconButton, Search, WarningSign}; -use super::dropdown::ElementSize; #[derive(PartialEq, Clone)] pub enum InputType { Text, @@ -25,13 +25,20 @@ pub struct InputProps { label: Option, #[props(default = false)] required: bool, + #[props(default = false)] + autofocus: bool, + #[props(default = false)] + focus: bool, #[props(default = 100)] maxlength: u8, + label_help: Option, left_text: Option, right_text: Option, on_input: EventHandler, on_keypress: EventHandler, on_click: EventHandler, + on_focus: EventHandler, + on_blur: EventHandler, } pub fn Input(props: InputProps) -> Element { let mut input_ref = use_signal::>>(|| None); @@ -58,12 +65,26 @@ pub fn Input(props: InputProps) -> Element { "" }; let mut is_active = use_signal::(|| false); + use_effect(use_reactive(&props.focus, move |f| { + if f { + if let Some(input_element) = input_ref.read().as_ref() { + if let Err(e) = input_element.focus() { + log::warn!("Failed to focus input {:?}", e) + }; + } + } + })); rsx!( section { class: "input__wrapper {is_search}", class: if is_active() { "input__wrapper--active" }, if let Some(value) = props.label { - label { class: "input__label", "{value}" } + label { class: "input__label", + "{value}" + if let Some(label_help) = props.label_help { + {label_help} + } + } } div { class: "input-wrapper {size} {input_error_container}", {props.left_text}, @@ -82,18 +103,21 @@ pub fn Input(props: InputProps) -> Element { } } }, - onfocus: move |_| { + onfocus: move |event| { if let Some(input_element) = input_ref() { input_element.set_type(input_type) } + props.on_focus.call(event); }, - onblur: move |_| { + onblur: move |event| { if let Some(input_element) = input_ref() { input_element.set_type("text") } + props.on_blur.call(event); }, value: props.message, required: props.required, + autofocus: props.autofocus, maxlength: i64::from(props.maxlength), placeholder: if props.required { format!("{}*", props.placeholder) @@ -101,7 +125,7 @@ pub fn Input(props: InputProps) -> Element { format!("{}", props.placeholder) }, oninput: move |event| props.on_input.call(event), - onkeypress: move |event| props.on_keypress.call(event) + onkeypress: move |event| props.on_keypress.call(event), } {props.right_text}, if matches!(props.itype, InputType::Search) { diff --git a/src/components/atoms/key_value.rs b/src/components/atoms/key_value.rs index 3bc0c84..e379291 100644 --- a/src/components/atoms/key_value.rs +++ b/src/components/atoms/key_value.rs @@ -1,5 +1,5 @@ -use dioxus::prelude::*; use super::dropdown::ElementSize; +use dioxus::prelude::*; #[derive(PartialEq, Clone)] pub enum Variant { Primary, @@ -13,6 +13,8 @@ pub struct KeyValueProps { size: ElementSize, #[props(default = Variant::Primary)] variant: Variant, + #[props(default = false)] + is_spaced: bool, text: String, body: Element, } @@ -27,7 +29,9 @@ pub fn KeyValue(props: KeyValueProps) -> Element { Variant::Secondary => "key-value--secondary", }; rsx!( - span { class: "key-value {props.class} {size} {variant}", + span { + class: "key-value {props.class} {size} {variant}", + class: if props.is_spaced {"row"}, h4 { class: "key-value__key", "{props.text}" } div { class: "key-value__value", {props.body} } } diff --git a/src/components/atoms/mod.rs b/src/components/atoms/mod.rs index b64679e..eeed1f0 100644 --- a/src/components/atoms/mod.rs +++ b/src/components/atoms/mod.rs @@ -1,16 +1,20 @@ pub mod account; pub mod action_request; pub mod attach; +pub mod autocomplete; +pub mod autocomplete_item; pub mod avatar; pub mod badge; pub mod bar; pub mod button; pub mod card; pub mod card_skeleton; +pub mod checkbox; pub mod checkbox_card; pub mod combo_input; pub mod dropdown; pub mod dynamic_text; +pub mod file_drop_area; pub mod icon_button; pub mod icons; pub mod input; @@ -20,11 +24,13 @@ pub mod management_method; pub mod markdown; pub mod notification; pub mod payment_method; +pub mod popover_button; pub mod radio_button; pub mod search_input; pub mod step; pub mod step_card; pub mod subtitle; +pub mod switch; pub mod tab; pub mod textarea; pub mod title; @@ -32,16 +38,20 @@ pub mod tooltip; pub use account::AccountButton; pub use action_request::ActionRequest; pub use attach::Attach; +pub use autocomplete::Autocomplete; +pub use autocomplete_item::AutocompleteItemButton; pub use avatar::Avatar; pub use badge::Badge; pub use bar::Bar; pub use button::Button; pub use card::Card; pub use card_skeleton::CardSkeleton; +pub use checkbox::Checkbox; pub use checkbox_card::CheckboxCard; pub use combo_input::ComboInput; pub use dropdown::Dropdown; pub use dynamic_text::DynamicText; +pub use file_drop_area::FileDropArea; pub use icon_button::IconButton; pub use icons::*; pub use input::Input; @@ -51,11 +61,13 @@ pub use management_method::ManagementMethod; pub use markdown::Markdown; pub use notification::Notification; pub use payment_method::PaymentMethod; +pub use popover_button::Popover; pub use radio_button::RadioButton; pub use search_input::SearchInput; pub use step::Step; pub use step_card::StepCard; pub use subtitle::Subtitle; +pub use switch::Switch; pub use tab::Tab; pub use textarea::TextareaInput; pub use title::Title; diff --git a/src/components/atoms/popover_button.rs b/src/components/atoms/popover_button.rs new file mode 100644 index 0000000..5b261fb --- /dev/null +++ b/src/components/atoms/popover_button.rs @@ -0,0 +1,35 @@ +use crate::components::atoms::{icon_button::Variant, IconButton}; + +use super::dropdown::ElementSize; +use dioxus::prelude::*; + +#[derive(PartialEq, Props, Clone)] +pub struct PopoverProps { + body: Element, + #[props(default = "".to_string())] + class: String, + #[props(default = Variant::Round)] + variant: Variant, + #[props(default = ElementSize::Big)] + size: ElementSize, + text: String, +} +pub fn Popover(props: PopoverProps) -> Element { + let mut show_over = use_signal(|| false); + rsx!( + div { class: "popover", + if show_over() { + div { class: "popover__tooltip", + span {class: "popover__message", { props.text }} + } + } + IconButton { + class: "button--drop {props.class}", + variant: Variant::Round, + size: ElementSize::Big, + body: props.body, + on_click: move |_| { show_over.toggle(); } + } + } + ) +} diff --git a/src/components/atoms/step.rs b/src/components/atoms/step.rs index 2037fc1..9ba1c74 100644 --- a/src/components/atoms/step.rs +++ b/src/components/atoms/step.rs @@ -8,6 +8,8 @@ pub struct StepProps { is_completed: bool, #[props(default = false)] has_cube: bool, + #[props(default = false)] + is_column: bool, name: Option, on_click: EventHandler, } @@ -17,6 +19,7 @@ pub fn Step(props: StepProps) -> Element { class: "step", class: if props.is_active { "step--active" }, class: if props.is_completed { "step--complete" }, + class: if props.is_column { "step--column" }, onclick: move |event| props.on_click.call(event), if props.has_cube { div { class: "step__cube", diff --git a/src/components/atoms/switch.rs b/src/components/atoms/switch.rs new file mode 100644 index 0000000..6d6a0f0 --- /dev/null +++ b/src/components/atoms/switch.rs @@ -0,0 +1,30 @@ +use dioxus::prelude::*; +#[derive(PartialEq, Props, Clone)] +pub struct SwitchProps { + #[props(default = "".to_string())] + class: String, + id: String, + name: String, + checked: bool, + label: String, + #[props(default = false)] + disabled: bool, + on_change: EventHandler, +} +pub fn Switch(props: SwitchProps) -> Element { + rsx!( + label { + class: "switch {props.class}", + class: if props.checked { "switch--active" }, + input { + r#type: "checkbox", + name: props.name, + disabled: props.disabled, + checked: props.checked, + onchange: move |_| { props.on_change.call(()) } + } + span { class: "slider"} + span { class: "switch__label", {props.label} } + } + ) +} diff --git a/src/components/atoms/tab.rs b/src/components/atoms/tab.rs index edb0556..47f6669 100644 --- a/src/components/atoms/tab.rs +++ b/src/components/atoms/tab.rs @@ -10,7 +10,8 @@ pub struct TabProps { #[props(default = ElementSize::Medium)] size: ElementSize, on_click: EventHandler, - icon: Option, + left_icon: Option, + right_icon: Option, } pub fn Tab(props: TabProps) -> Element { let size = match props.size { @@ -23,8 +24,9 @@ pub fn Tab(props: TabProps) -> Element { class: "tab {size} {props.class}", class: if props.is_active { "tab--active" }, onclick: move |event| props.on_click.call(event), - {props.icon}, + {props.left_icon}, "{props.text}" + { props.right_icon } } ) } diff --git a/src/components/atoms/textarea.rs b/src/components/atoms/textarea.rs index e4fae8a..76ac473 100644 --- a/src/components/atoms/textarea.rs +++ b/src/components/atoms/textarea.rs @@ -4,6 +4,7 @@ pub struct TextareaInputProps { value: String, placeholder: String, label: Option, + help: Option, on_input: EventHandler, on_keypress: EventHandler, on_click: EventHandler, @@ -38,6 +39,9 @@ pub fn TextareaInput(props: TextareaInputProps) -> Element { } } } + if let Some(help) = props.help { + div { class: "input--help", "{help}" } + } } ) }