Merge pull request 'feat: ui/auto-resizing-textarea' (#4562) from ui/auto-resizing-textarea into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4562
This commit is contained in:
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,11 +15,86 @@ 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 (
|
||||
<TextField
|
||||
ref={(el: HTMLDivElement) => {
|
||||
// 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,
|
||||
@@ -31,7 +107,27 @@ export const TextArea = (props: TextAreaProps) => (
|
||||
descriptionComponent={TextField.Description}
|
||||
{...props}
|
||||
/>
|
||||
<TextField.TextArea {...props.input} />
|
||||
<TextField.TextArea
|
||||
class={cx(input.class, {
|
||||
"auto-resize": input.autoResize,
|
||||
})}
|
||||
onInput={(e) => {
|
||||
autoResize();
|
||||
|
||||
if (!input.onInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call original onInput if it exists
|
||||
if (typeof input.onInput === "function") {
|
||||
input.onInput(e);
|
||||
} else if (Array.isArray(input.onInput)) {
|
||||
input.onInput.forEach((handler) => handler(e));
|
||||
}
|
||||
}}
|
||||
{...textareaProps}
|
||||
/>
|
||||
</Orienter>
|
||||
</TextField>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
Reference in New Issue
Block a user