mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-02 08:34:15 -05:00
feat: add TextArea component (#77)
This commit is contained in:
parent
4aeab59e6f
commit
559add3a3c
4 changed files with 217 additions and 1 deletions
|
@ -7,6 +7,7 @@ view! {
|
||||||
<Space vertical=true>
|
<Space vertical=true>
|
||||||
<Input value/>
|
<Input value/>
|
||||||
<Input value variant=InputVariant::Password placeholder="Password"/>
|
<Input value variant=InputVariant::Password placeholder="Password"/>
|
||||||
|
<TextArea value placeholder="Textarea"/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -19,6 +20,7 @@ let value = create_rw_signal(String::from("o"));
|
||||||
view! {
|
view! {
|
||||||
<Space vertical=true>
|
<Space vertical=true>
|
||||||
<Input value disabled=true/>
|
<Input value disabled=true/>
|
||||||
|
<TextArea value disabled=true/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -31,6 +33,7 @@ let value = create_rw_signal(String::from("o"));
|
||||||
view! {
|
view! {
|
||||||
<Space vertical=true>
|
<Space vertical=true>
|
||||||
<Input value invalid=true/>
|
<Input value invalid=true/>
|
||||||
|
<TextArea value invalid=true/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -119,3 +122,7 @@ view! {
|
||||||
| ----- | ----------- | ------------------------ |
|
| ----- | ----------- | ------------------------ |
|
||||||
| focus | `Fn(&self)` | Focus the input element. |
|
| focus | `Fn(&self)` | Focus the input element. |
|
||||||
| blur | `Fn(&self)` | Blur the input element. |
|
| blur | `Fn(&self)` | Blur the input element. |
|
||||||
|
|
||||||
|
### TextArea Props
|
||||||
|
|
||||||
|
Removes variant and slot from Input component.
|
|
@ -1,11 +1,14 @@
|
||||||
|
mod text_area;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
|
pub use text_area::{TextArea, TextAreaRef};
|
||||||
|
pub use theme::InputTheme;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
theme::{use_theme, Theme},
|
theme::{use_theme, Theme},
|
||||||
utils::{class_list::class_list, mount_style, ComponentRef},
|
utils::{class_list::class_list, mount_style, ComponentRef},
|
||||||
};
|
};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
pub use theme::InputTheme;
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub enum InputVariant {
|
pub enum InputVariant {
|
||||||
|
|
54
thaw/src/input/text-area.css
Normal file
54
thaw/src/input/text-area.css
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
.thaw-textarea {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--thaw-background-color);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--thaw-font-color);
|
||||||
|
border: 1px solid var(--thaw-border-color);
|
||||||
|
border-radius: var(--thaw-border-radius);
|
||||||
|
cursor: text;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-textarea--focus,
|
||||||
|
.thaw-textarea:hover:not(.thaw-textarea--disabled, .thaw-textarea--invalid) {
|
||||||
|
border-color: var(--thaw-border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-textarea--disabled,
|
||||||
|
.thaw-textarea--disabled .thaw-textarea__textarea-el {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--thaw-background-color-disabled);
|
||||||
|
color: var(--thaw-font-color-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-textarea--invalid {
|
||||||
|
border-color: var(--thaw-border-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-textarea--focus:not(.thaw-textarea--invalid) {
|
||||||
|
box-shadow: 0 0 0 2px var(--thaw-box-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-textarea--focus.thaw-textarea--invalid {
|
||||||
|
box-shadow: 0 0 0 2px var(--thaw-box-shadow-color-invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-textarea__textarea-el {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: var(--thaw-font-color);
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: 1.2;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-textarea__textarea-el::placeholder {
|
||||||
|
color: var(--thaw-placeholder-color);
|
||||||
|
}
|
152
thaw/src/input/text_area.rs
Normal file
152
thaw/src/input/text_area.rs
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
use crate::{
|
||||||
|
theme::{use_theme, Theme},
|
||||||
|
utils::{class_list::class_list, mount_style, ComponentRef},
|
||||||
|
};
|
||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TextArea(
|
||||||
|
#[prop(optional, into)] value: RwSignal<String>,
|
||||||
|
#[prop(optional, into)] allow_value: Option<Callback<String, bool>>,
|
||||||
|
#[prop(optional, into)] placeholder: MaybeSignal<String>,
|
||||||
|
#[prop(optional, into)] on_focus: Option<Callback<ev::FocusEvent>>,
|
||||||
|
#[prop(optional, into)] on_blur: Option<Callback<ev::FocusEvent>>,
|
||||||
|
#[prop(optional, into)] disabled: MaybeSignal<bool>,
|
||||||
|
#[prop(optional, into)] invalid: MaybeSignal<bool>,
|
||||||
|
#[prop(optional)] comp_ref: ComponentRef<TextAreaRef>,
|
||||||
|
#[prop(optional, into)] class: MaybeSignal<String>,
|
||||||
|
#[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let theme = use_theme(Theme::light);
|
||||||
|
mount_style("text-area", include_str!("./text-area.css"));
|
||||||
|
|
||||||
|
let value_trigger = create_trigger();
|
||||||
|
let on_input = move |ev| {
|
||||||
|
let input_value = event_target_value(&ev);
|
||||||
|
if let Some(allow_value) = allow_value.as_ref() {
|
||||||
|
if !allow_value.call(input_value.clone()) {
|
||||||
|
value_trigger.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value.set(input_value);
|
||||||
|
};
|
||||||
|
let is_focus = create_rw_signal(false);
|
||||||
|
let on_internal_focus = move |ev| {
|
||||||
|
is_focus.set(true);
|
||||||
|
if let Some(on_focus) = on_focus.as_ref() {
|
||||||
|
on_focus.call(ev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_internal_blur = move |ev| {
|
||||||
|
is_focus.set(false);
|
||||||
|
if let Some(on_blur) = on_blur.as_ref() {
|
||||||
|
on_blur.call(ev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let css_vars = create_memo(move |_| {
|
||||||
|
let mut css_vars = String::new();
|
||||||
|
theme.with(|theme| {
|
||||||
|
let border_color_hover = theme.common.color_primary.clone();
|
||||||
|
css_vars.push_str(&format!("--thaw-border-color-hover: {border_color_hover};"));
|
||||||
|
css_vars.push_str(&format!("--thaw-box-shadow-color: {border_color_hover}33;"));
|
||||||
|
let border_radius = theme.common.border_radius.clone();
|
||||||
|
css_vars.push_str(&format!("--thaw-border-radius: {border_radius};"));
|
||||||
|
css_vars.push_str(&format!(
|
||||||
|
"--thaw-background-color: {};",
|
||||||
|
theme.input.background_color
|
||||||
|
));
|
||||||
|
css_vars.push_str(&format!("--thaw-font-color: {};", theme.input.font_color));
|
||||||
|
css_vars.push_str(&format!(
|
||||||
|
"--thaw-border-color: {};",
|
||||||
|
theme.input.border_color
|
||||||
|
));
|
||||||
|
css_vars.push_str(&format!(
|
||||||
|
"--thaw-border-color-error: {};",
|
||||||
|
theme.common.color_error
|
||||||
|
));
|
||||||
|
css_vars.push_str(&format!(
|
||||||
|
"--thaw-placeholder-color: {};",
|
||||||
|
theme.input.placeholder_color
|
||||||
|
));
|
||||||
|
css_vars.push_str(&format!(
|
||||||
|
"--thaw-background-color-disabled: {};",
|
||||||
|
theme.input.background_color_disabled
|
||||||
|
));
|
||||||
|
css_vars.push_str(&format!(
|
||||||
|
"--thaw-font-color-disabled: {};",
|
||||||
|
theme.input.font_color_disabled
|
||||||
|
));
|
||||||
|
css_vars.push_str(&format!(
|
||||||
|
"--thaw-box-shadow-color-invalid: {}33;",
|
||||||
|
theme.common.color_error
|
||||||
|
));
|
||||||
|
});
|
||||||
|
css_vars
|
||||||
|
});
|
||||||
|
let textarea_ref = create_node_ref::<html::Textarea>();
|
||||||
|
textarea_ref.on_load(move |_| {
|
||||||
|
comp_ref.load(TextAreaRef { textarea_ref });
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
const INNER_ATTRS: [&'static str; 3] = ["class", "disabled", "placeholder"];
|
||||||
|
attrs.iter().for_each(|attr| {
|
||||||
|
if INNER_ATTRS.contains(&attr.0) {
|
||||||
|
logging::warn!(
|
||||||
|
"Thaw: The '{}' attribute already exists on elements inside the TextArea component, which may cause conflicts.",
|
||||||
|
attr.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class=class_list![
|
||||||
|
"thaw-textarea", ("thaw-textarea--focus", move || is_focus.get()),
|
||||||
|
("thaw-textarea--disabled", move || disabled.get()), ("thaw-textarea--invalid", move ||
|
||||||
|
invalid.get()), move || class.get()
|
||||||
|
]
|
||||||
|
|
||||||
|
style=move || css_vars.get()
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
{..attrs}
|
||||||
|
prop:value=move || {
|
||||||
|
value_trigger.track();
|
||||||
|
value.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
on:input=on_input
|
||||||
|
on:focus=on_internal_focus
|
||||||
|
on:blur=on_internal_blur
|
||||||
|
class="thaw-textarea__textarea-el"
|
||||||
|
disabled=move || disabled.get()
|
||||||
|
placeholder=move || placeholder.get()
|
||||||
|
ref=textarea_ref
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TextAreaRef {
|
||||||
|
textarea_ref: NodeRef<html::Textarea>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextAreaRef {
|
||||||
|
pub fn focus(&self) {
|
||||||
|
if let Some(textarea_el) = self.textarea_ref.get_untracked() {
|
||||||
|
_ = textarea_el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blur(&self) {
|
||||||
|
if let Some(textarea_el) = self.textarea_ref.get_untracked() {
|
||||||
|
_ = textarea_el.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue