From 7738a8fa782ec3ff25612b1911ad5cc5cb4478ef Mon Sep 17 00:00:00 2001 From: Reed Krantz Date: Mon, 12 Jan 2026 20:01:32 -0600 Subject: [PATCH] feat(components): add the Textarea component --- src/components/mod.rs | 1 + src/components/textarea/component.rs | 78 ++++++++++++++++++++++++++ src/components/textarea/mod.rs | 2 + src/components/textarea/style.css | 84 ++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 src/components/textarea/component.rs create mode 100644 src/components/textarea/mod.rs create mode 100644 src/components/textarea/style.css diff --git a/src/components/mod.rs b/src/components/mod.rs index 3c8ab79..a481b0c 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -17,3 +17,4 @@ pub use edit_quote::EditQuote; pub mod calendar; pub mod date_picker; pub mod popover; +pub mod textarea; diff --git a/src/components/textarea/component.rs b/src/components/textarea/component.rs new file mode 100644 index 0000000..7f4db3c --- /dev/null +++ b/src/components/textarea/component.rs @@ -0,0 +1,78 @@ +use dioxus::prelude::*; + +#[derive(Copy, Clone, PartialEq, Default)] +#[non_exhaustive] +pub enum TextareaVariant { + #[default] + Default, + Fade, + Outline, + Ghost, +} + +impl TextareaVariant { + pub fn class(&self) -> &'static str { + match self { + TextareaVariant::Default => "default", + TextareaVariant::Fade => "fade", + TextareaVariant::Outline => "outline", + TextareaVariant::Ghost => "ghost", + } + } +} + +#[component] +pub fn Textarea( + oninput: Option>, + onchange: Option>, + oninvalid: Option>, + onselect: Option>, + onselectionchange: Option>, + onfocus: Option>, + onblur: Option>, + onfocusin: Option>, + onfocusout: Option>, + onkeydown: Option>, + onkeypress: Option>, + onkeyup: Option>, + oncompositionstart: Option>, + oncompositionupdate: Option>, + oncompositionend: Option>, + oncopy: Option>, + oncut: Option>, + onpaste: Option>, + #[props(default)] variant: TextareaVariant, + #[props(extends=GlobalAttributes)] + #[props(extends=textarea)] + attributes: Vec, + children: Element, +) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + textarea { + class: "textarea", + "data-slot": "textarea", + "data-style": variant.class(), + oninput: move |e| _ = oninput.map(|callback| callback(e)), + onchange: move |e| _ = onchange.map(|callback| callback(e)), + oninvalid: move |e| _ = oninvalid.map(|callback| callback(e)), + onselect: move |e| _ = onselect.map(|callback| callback(e)), + onselectionchange: move |e| _ = onselectionchange.map(|callback| callback(e)), + onfocus: move |e| _ = onfocus.map(|callback| callback(e)), + onblur: move |e| _ = onblur.map(|callback| callback(e)), + onfocusin: move |e| _ = onfocusin.map(|callback| callback(e)), + onfocusout: move |e| _ = onfocusout.map(|callback| callback(e)), + onkeydown: move |e| _ = onkeydown.map(|callback| callback(e)), + onkeypress: move |e| _ = onkeypress.map(|callback| callback(e)), + onkeyup: move |e| _ = onkeyup.map(|callback| callback(e)), + oncompositionstart: move |e| _ = oncompositionstart.map(|callback| callback(e)), + oncompositionupdate: move |e| _ = oncompositionupdate.map(|callback| callback(e)), + oncompositionend: move |e| _ = oncompositionend.map(|callback| callback(e)), + oncopy: move |e| _ = oncopy.map(|callback| callback(e)), + oncut: move |e| _ = oncut.map(|callback| callback(e)), + onpaste: move |e| _ = onpaste.map(|callback| callback(e)), + ..attributes, + {children} + } + } +} diff --git a/src/components/textarea/mod.rs b/src/components/textarea/mod.rs new file mode 100644 index 0000000..2590c01 --- /dev/null +++ b/src/components/textarea/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; diff --git a/src/components/textarea/style.css b/src/components/textarea/style.css new file mode 100644 index 0000000..0a2c499 --- /dev/null +++ b/src/components/textarea/style.css @@ -0,0 +1,84 @@ +/* Base */ +.textarea { + width: 100%; + min-height: 4rem; + box-sizing: border-box; + padding: 8px 12px; + border: none; + border-radius: 0.5rem; + margin: 0; + appearance: none; + background: none; + color: var(--secondary-color-4); + font-family: inherit; + line-height: 1.5; + outline: none; + resize: vertical; + transition: background-color 100ms ease-out, border-color 100ms ease-out, box-shadow 100ms ease-out; +} + +.textarea:disabled { + color: var(--secondary-color-5); + cursor: not-allowed; +} + +.textarea::placeholder { + color: var(--secondary-color-5); +} + +/* Default Variant */ +.textarea[data-style="default"] { + background: var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); + box-shadow: inset 0 0 0 1px var(--light, var(--primary-color-6)) var(--dark, var(--primary-color-7)); +} + +.textarea[data-style="default"]:hover:not(:disabled), +.textarea[data-style="default"]:focus { + background: var(--light, var(--primary-color-4)) var(--dark, var(--primary-color-5)); + color: var(--secondary-color-1); +} + +/* Fade Variant */ +.textarea[data-style="fade"] { + background: var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); +} + +.textarea[data-style="fade"]:hover:not(:disabled), +.textarea[data-style="fade"]:focus { + background: var(--light, var(--primary-color-4)) var(--dark, var(--primary-color-5)); + color: var(--secondary-color-1); +} + +/* Outline Variant */ +.textarea[data-style="outline"] { + border: 1px solid var(--primary-color-6); + background-color: var(--light, var(--primary-color)) + var(--dark, var(--primary-color-3)); +} + +.textarea[data-style="outline"]:hover:not(:disabled, :focus) { + border-color: var(--primary-color-7); +} + +.textarea[data-style="outline"]:focus { + border-color: var(--focused-border-color); +} + +.textarea[data-style="outline"]:invalid, +.textarea[data-style="outline"][aria-invalid="true"] { + border-color: var(--primary-error-color); +} + +/* Ghost Variant */ +.textarea[data-style="ghost"] { + background-color: transparent; +} + +.textarea[data-style="ghost"]:hover:not(:disabled) { + background-color: var(--primary-color-5); + color: var(--secondary-color-1); +} + +.textarea[data-style="ghost"]:focus { + border-color: var(--focused-border-color); +}