diff --git a/pkgs/clan-app/ui/src/components/Form/TextArea.stories.tsx b/pkgs/clan-app/ui/src/components/Form/TextArea.stories.tsx index c08db1204..d251d6dcc 100644 --- a/pkgs/clan-app/ui/src/components/Form/TextArea.stories.tsx +++ b/pkgs/clan-app/ui/src/components/Form/TextArea.stories.tsx @@ -114,3 +114,30 @@ export const ReadOnly: Story = { "Good evening. I'm Ron Burgundy, and this is what's happening in your world tonight. ", }, }; + +export const AutoResize: Story = { + args: { + label: "Auto-resizing TextArea", + description: + "This textarea automatically adjusts its height based on content", + tooltip: "Try typing multiple lines to see it grow", + input: { + placeholder: "Start typing to see the textarea grow...", + autoResize: true, + minRows: 2, + maxRows: 10, + }, + }, +}; + +export const AutoResizeNoMax: Story = { + args: { + label: "Auto-resize without max height", + description: "This textarea grows indefinitely with content", + input: { + placeholder: "This will grow as much as needed...", + autoResize: true, + minRows: 3, + }, + }, +}; diff --git a/pkgs/clan-app/ui/src/components/Form/TextArea.tsx b/pkgs/clan-app/ui/src/components/Form/TextArea.tsx index b2931ddc0..fddbddbc0 100644 --- a/pkgs/clan-app/ui/src/components/Form/TextArea.tsx +++ b/pkgs/clan-app/ui/src/components/Form/TextArea.tsx @@ -7,6 +7,7 @@ import { import cx from "classnames"; import { Label } from "./Label"; import { PolymorphicProps } from "@kobalte/core/polymorphic"; +import { createEffect, createSignal, splitProps } from "solid-js"; import "./TextInput.css"; import { FieldProps } from "./Field"; @@ -14,24 +15,119 @@ import { Orienter } from "./Orienter"; export type TextAreaProps = FieldProps & TextFieldRootProps & { - input?: PolymorphicProps<"textarea", TextFieldTextAreaProps<"input">>; + input: PolymorphicProps<"textarea", TextFieldTextAreaProps<"input">> & { + autoResize?: boolean; + minRows?: number; + maxRows?: number; + }; }; -export const TextArea = (props: TextAreaProps) => ( - - - - -); +export const TextArea = (props: TextAreaProps) => { + let textareaRef: HTMLTextAreaElement; + + const [lineHeight, setLineHeight] = createSignal(0); + + const autoResize = () => { + const input = props.input; + + if (!(textareaRef && input.autoResize && lineHeight() > 0)) return; + + // Reset height to auto to get accurate scrollHeight + textareaRef.style.height = "auto"; + + // Calculate min and max heights based on rows + const minHeight = (input.minRows || 1) * lineHeight(); + const maxHeight = input.maxRows ? input.maxRows * lineHeight() : Infinity; + + // Set the height based on content, respecting min/max + const newHeight = Math.min( + Math.max(textareaRef.scrollHeight, minHeight), + maxHeight, + ); + + // Update the height + textareaRef.style.height = `${newHeight}px`; + textareaRef.style.maxHeight = `${maxHeight}px`; + + console.log("min/max height", minHeight, maxHeight); + console.log("textarea ref style", textareaRef.style); + }; + + // Set up auto-resize effect + createEffect(() => { + if (textareaRef && props.input.autoResize) { + // Get computed line height + const computedStyle = window.getComputedStyle(textareaRef); + const computedLineHeight = parseFloat(computedStyle.lineHeight); + if (!isNaN(computedLineHeight)) { + setLineHeight(computedLineHeight); + } + + // Initial resize + autoResize(); + } + }); + + // Watch for value changes to trigger resize + createEffect(() => { + if (props.input.autoResize && textareaRef) { + // Access the value to create a dependency + const _ = props.value || props.defaultValue || ""; + // Trigger resize on the next tick to ensure DOM is updated + setTimeout(autoResize, 0); + } + }); + + const input = props.input; + + // TextField.Textarea already has an `autoResize` prop + // We filter our props out to avoid conflicting behaviour + const [_, textareaProps] = splitProps(input, [ + "autoResize", + "minRows", + "maxRows", + ]); + + return ( + { + // for some reason capturing the ref directly on TextField.TextArea works in Chrome + // but not in webkit, so we capture the parent ref and query for the textarea + textareaRef = el.querySelector("textarea")! as HTMLTextAreaElement; + }} + class={cx("form-field", "textarea", props.size, props.orientation, { + inverted: props.inverted, + ghost: props.ghost, + })} + {...props} + > + + + + ); +}; diff --git a/pkgs/clan-app/ui/src/components/Form/TextInput.css b/pkgs/clan-app/ui/src/components/Form/TextInput.css index eceb5fe61..900c07a42 100644 --- a/pkgs/clan-app/ui/src/components/Form/TextInput.css +++ b/pkgs/clan-app/ui/src/components/Form/TextInput.css @@ -42,6 +42,11 @@ div.form-field { &[data-readonly] { @apply overflow-y-hidden; } + + &.auto-resize { + @apply resize-none overflow-y-auto; + transition: height 0.1s ease-out; + } } &.horizontal { diff --git a/pkgs/clan-app/ui/src/components/Sidebar/SidebarSection.css b/pkgs/clan-app/ui/src/components/Sidebar/SidebarSection.css index 9200dee88..fdcdb4c3a 100644 --- a/pkgs/clan-app/ui/src/components/Sidebar/SidebarSection.css +++ b/pkgs/clan-app/ui/src/components/Sidebar/SidebarSection.css @@ -10,6 +10,6 @@ div.sidebar-section { } & > div.content { - @apply w-full h-full px-1.5 py-3 rounded-md bg-inv-4; + @apply w-full h-fit px-1.5 py-3 rounded-md bg-inv-4; } } diff --git a/pkgs/clan-app/ui/src/routes/Machine/SectionGeneral.tsx b/pkgs/clan-app/ui/src/routes/Machine/SectionGeneral.tsx index aa5c455ae..9134daf93 100644 --- a/pkgs/clan-app/ui/src/routes/Machine/SectionGeneral.tsx +++ b/pkgs/clan-app/ui/src/routes/Machine/SectionGeneral.tsx @@ -116,7 +116,13 @@ export const SectionGeneral = () => { inverted readOnly={!editing} orientation="horizontal" - input={{ ...input, rows: 4, placeholder: "No description" }} + input={{ + ...input, + autoResize: true, + minRows: 2, + maxRows: 4, + placeholder: "No description", + }} /> )}