Skip to content

Commit

Permalink
fix: add ResizeObserver to change textarea height based on width changes
Browse files Browse the repository at this point in the history
  • Loading branch information
KelvinOm committed Dec 6, 2024
1 parent fd9efb8 commit 35ef576
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 37 deletions.
3 changes: 2 additions & 1 deletion app/client/packages/design-system/widgets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0",
"react-transition-group": "^4.4.5",
"remark-gfm": "^4.0.0"
"remark-gfm": "^4.0.0",
"usehooks-ts": "*"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function _TextAreaInput(
ref: React.Ref<HTMLTextAreaElement>,
) {
const {
className,
defaultValue,
isLoading,
isReadOnly,
Expand All @@ -34,7 +35,11 @@ function _TextAreaInput(
<Group className={styles.inputGroup}>
<HeadlessTextArea
{...rest}
className={clsx(styles.input, getTypographyClassName("body"))}
className={clsx(
styles.input,
getTypographyClassName("body"),
className,
)}
data-readonly={Boolean(isReadOnly) ? true : undefined}
data-size={Boolean(size) ? size : undefined}
defaultValue={defaultValue}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
background-color: var(--color-bg-neutral-subtle);
box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle);
isolation: isolate;
/* Delete overflow: hidden; when the max height is added */
overflow: hidden;
}

.input:has(> [data-select-text]) {
Expand All @@ -35,10 +37,6 @@
font-family: inherit;
}

.input:is(textarea)[rows="1"] {
min-block-size: initial;
}

.input:autofill,
.input:autofill:hover,
.input:autofill:focus,
Expand Down Expand Up @@ -93,29 +91,6 @@
position: absolute;
}

/* Note: the following calculations are done so that icon button in chat input is centered vertically */
.inputGroup:has(.input[rows="1"]) [data-input-suffix] {
--icon-size: calc(
var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) +
var(--inner-spacing-3) * 2
);
--icon-offset: calc((var(--input-height) - var(--icon-size)) / 2);

bottom: round(up, var(--icon-offset), 0.5px);
right: var(--icon-offset);
}

.inputGroup:has(.input[rows="1"]) [data-input-prefix] {
--icon-size: calc(
var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) +
var(--inner-spacing-3) * 2
);
--icon-offset: calc((var(--input-height) - var(--icon-size)) / 2);

bottom: var(--icon-offset);
left: var(--icon-offset);
}

.inputGroup :is([data-input-suffix], [data-input-prefix]) {
display: flex;
justify-content: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export interface TextAreaInputProps
extends Omit<HeadlessTextAreaProps, "prefix" | "size">,
CommonInputProps {
rows?: number;
className?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,34 @@ import {
inputFieldStyles,
TextAreaInput,
} from "@appsmith/wds";
import React, { useCallback, useRef } from "react";
import React, { useCallback, useRef, useEffect, useState } from "react";
import { useControlledState } from "@react-stately/utils";
import { chain, useLayoutEffect } from "@react-aria/utils";
import { TextField as HeadlessTextField } from "react-aria-components";
import { useDebounceCallback, useResizeObserver } from "usehooks-ts";

import type { TextAreaProps } from "./types";

// usehooks-ts does not export Size type, so declare it ourselves
interface Size {
width?: number;
}

export function TextArea(props: TextAreaProps) {
const {
contextualHelp,
errorMessage,
fieldClassName,
inputClassName,
isDisabled,
isInvalid,
isLoading,
isReadOnly,
isRequired,
label,
onChange,
rows = 3,
size,
suffix,
value,
...rest
Expand All @@ -31,9 +41,11 @@ export function TextArea(props: TextAreaProps) {
const [inputValue, setInputValue] = useControlledState(
props.value,
props.defaultValue ?? "",
() => {
//
},
() => {},
);

const [textFieldHeight, setTextFieldHeightHeight] = useState<number | null>(
null,
);

const onHeightChange = useCallback(() => {
Expand All @@ -56,9 +68,12 @@ export function TextArea(props: TextAreaProps) {
input.style.height = "auto";

const computedStyle = getComputedStyle(input);
const height = parseFloat(computedStyle.height) || 0;
const paddingTop = parseFloat(computedStyle.paddingTop);
const paddingBottom = parseFloat(computedStyle.paddingBottom);

setTextFieldHeightHeight(height + paddingTop + paddingBottom);

input.style.height = `${
// subtract comptued padding and border to get the actual content height
input.scrollHeight -
Expand All @@ -70,23 +85,50 @@ export function TextArea(props: TextAreaProps) {
input.style.overflow = prevOverflow;
input.style.alignSelf = prevAlignment;
}
}, [inputRef, props.height]);
}, [inputRef.current, props.height]);

useLayoutEffect(() => {
if (inputRef.current) {
onHeightChange();
}
}, [onHeightChange, inputValue]);

const [{ width }, setSize] = useState<Size>({
width: undefined,
});

const onResize = useDebounceCallback(setSize, 200);

useResizeObserver({
ref: inputRef,
onResize,
});

useEffect(
function updateHeight() {
onHeightChange();
},
[width],
);

const styles = {
// The --input-height it may be useful to align the prefix or suffix.
// Why can't we do this with CSS? Reason is that the height of the input is calculated based on the content.
"--input-height": Boolean(textFieldHeight)
? `${textFieldHeight}px`
: "auto",
} as React.CSSProperties;

return (
<HeadlessTextField
{...rest}
className={clsx(inputFieldStyles.field)}
className={clsx(inputFieldStyles.field, fieldClassName)}
isDisabled={isDisabled}
isInvalid={isInvalid}
isReadOnly={isReadOnly}
isRequired={isRequired}
onChange={chain(onChange, setInputValue)}
style={styles}
value={value}
>
<FieldLabel
Expand All @@ -97,9 +139,12 @@ export function TextArea(props: TextAreaProps) {
{label}
</FieldLabel>
<TextAreaInput
className={inputClassName}
isLoading={isLoading}
isReadOnly={isReadOnly}
ref={inputRef}
rows={rows}
size={size}
suffix={suffix}
value={value}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FieldProps } from "@appsmith/wds";
import type { FieldProps, SIZES } from "@appsmith/wds";
import type { ReactNode } from "react";
import type { TextFieldProps as AriaTextFieldProps } from "react-aria-components";

Expand All @@ -8,4 +8,7 @@ export interface TextAreaProps extends AriaTextFieldProps, FieldProps {
suffix?: ReactNode;
prefix?: ReactNode;
rows?: number;
fieldClassName?: string;
inputClassName?: string;
size?: Omit<keyof typeof SIZES, "xSmall">;
}
3 changes: 2 additions & 1 deletion app/client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ __metadata:
react-syntax-highlighter: ^15.5.0
react-transition-group: ^4.4.5
remark-gfm: ^4.0.0
usehooks-ts: "*"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
languageName: unknown
Expand Down Expand Up @@ -33469,7 +33470,7 @@ __metadata:
languageName: node
linkType: hard

"usehooks-ts@npm:^3.1.0":
"usehooks-ts@npm:*, usehooks-ts@npm:^3.1.0":
version: 3.1.0
resolution: "usehooks-ts@npm:3.1.0"
dependencies:
Expand Down

0 comments on commit 35ef576

Please sign in to comment.