Skip to content

Commit

Permalink
Merge pull request #121 from aversini/fix(TextArea)-broken-animation-…
Browse files Browse the repository at this point in the history
…when-scrollHeight-is-0

fix(TextArea): broken animation when scrollHeight is 0
  • Loading branch information
aversini authored Dec 5, 2023
2 parents e9aaeaa + 33a8488 commit ccf18fc
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 49 deletions.
95 changes: 46 additions & 49 deletions packages/ui-components/src/components/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import useUniqueId from "../../common/hooks/useUniqueId";
import { mergeRefs } from "../../common/utilities";
import { LiveRegion } from "../private/LiveRegion/LiveRegion";
import type { TextAreaProps } from "./TextAreaTypes";
import { getTextAreaClasses } from "./utilities";

const TRANSLATION_OFFSET = 12;
const ROW_HEIGHT = 24;
import { adjustLabelAndHelperText, getTextAreaClasses } from "./utilities";

export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
(
Expand Down Expand Up @@ -65,6 +62,11 @@ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
errorKind,
});

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setUserTextArea(e.target.value);
onChange && onChange(e);
};

useLayoutEffect(() => {
if (!raw && rightElementRef.current) {
setTextAreaPaddingRight(rightElementRef.current.offsetWidth + 18 + 10);
Expand All @@ -80,73 +82,68 @@ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
if (raw) {
return;
}

if (textAreaRef && textAreaRef.current) {
textAreaRef.current.style.height = "inherit";
// Set the height to match the content
textAreaRef.current.style.height =
textAreaRef.current.scrollHeight + "px";

/**
* If the height of the textarea has changed, we need to
* adjust the label and helper text to match the new height.
* This is done by calculating the difference in height and
* then adjusting the label and helper text by that amount.
* If the height of the textarea has changed, we
* need to adjust the label and helper text to match
* the new height.
* This is done by calculating the difference in
* height and then adjusting the label and helper
* text by that amount.
*/
if (textAreaHeightRef.current !== textAreaRef.current.scrollHeight) {
const diff =
textAreaRef.current.scrollHeight - textAreaHeightRef.current;
const totalRows = Math.abs(diff / ROW_HEIGHT);

/**
* The label and helper text are moved by the same amount
* as the textarea. This is done by multiplying the
* difference by the number of rows that have been added
* or removed.
* The label is moved in the opposite direction of the
* textarea, so that it appears to be moving up as the
* textarea grows.
* The helper text is moved in the same direction as the
* textarea, so that it appears to be moving down as the
* textarea grows.
*/
labelOffsetRef.current =
labelOffsetRef.current +
-1 * Math.sign(diff) * (TRANSLATION_OFFSET * totalRows);

helperTextOffsetRef.current =
helperTextOffsetRef.current +
Math.sign(diff) * (TRANSLATION_OFFSET * totalRows);

const { labelOffset, helperTextOffset, scrollHeight } =
adjustLabelAndHelperText({
scrollHeight: textAreaRef.current.scrollHeight,
currentHeight: textAreaHeightRef.current,
currentLabelOffset: labelOffsetRef.current,
currentHelperTextOffset: helperTextOffsetRef.current,
});

/* v8 ignore next 7 */
if (labelOffset) {
labelOffsetRef.current = labelOffset;
labelRef?.current?.style.setProperty(
"--av-text-area-label",
`${labelOffsetRef.current}px`,
`${labelOffset}px`,
);
}

/* v8 ignore next 7 */
if (helperTextOffset) {
helperTextOffsetRef.current = helperTextOffset;
helperTextRef?.current?.style.setProperty(
"--av-text-area-helper-text",
`${helperTextOffsetRef.current}px`,
`${helperTextOffset}px`,
);

textAreaHeightRef.current = textAreaRef.current.scrollHeight;
}

textAreaHeightRef.current = scrollHeight || textAreaHeightRef.current;
}

/**
* This section is to toggle the transitions.
* This is to prevent the label and helper text from animating
* when the user is typing. The animation is re-enabled
* when there is nothing in the textarea.
* This is to prevent the label and helper text from
* animating when the user is typing. The animation is
* re-enabled when there is nothing in the textarea.
*
* The reason for the timeout is to prevent it to be
* re-enabled too soon when the user clears out the
* whole textarea.
*/
labelRef?.current?.style.setProperty(
"--av-text-area-wrapper-transition",
userInput ? "none" : "all 0.2s ease-out",
);
setTimeout(() => {
labelRef?.current?.style.setProperty(
"--av-text-area-wrapper-transition",
!userInput ? "all 0.2s ease-out" : "none",
);
}, 0);
}, [userInput, raw]);

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setUserTextArea(e.target.value);
onChange && onChange(e);
};

return (
<div className={textTextAreaClassName.wrapper}>
<label
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { adjustLabelAndHelperText } from "../utilities";

describe("adjustLabelAndHelperText", () => {
it("should return no offsets if scrollHeight is 0", () => {
const res = adjustLabelAndHelperText({
scrollHeight: 0,
currentHeight: 0,
currentHelperTextOffset: 0,
currentLabelOffset: 0,
});
expect(res).toStrictEqual({
labelOffset: undefined,
helperTextOffset: undefined,
scrollHeight: 0,
});
});

it("should return offsets if scrollHeight and currentHeight are different and positive", () => {
const res = adjustLabelAndHelperText({
scrollHeight: 200,
currentHeight: 400,
currentHelperTextOffset: 0,
currentLabelOffset: 0,
});
expect(res).toStrictEqual({
labelOffset: 100,
helperTextOffset: -100,
scrollHeight: 200,
});
});
});
33 changes: 33 additions & 0 deletions packages/ui-components/src/components/TextArea/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,36 @@ export const getTextAreaClasses = ({
rightElement,
};
};

export const adjustLabelAndHelperText = ({
scrollHeight,
currentHeight,
currentLabelOffset = 0,
currentHelperTextOffset = 0,
}: {
scrollHeight: number;
currentHeight: number;
currentLabelOffset: number;
currentHelperTextOffset: number;
}) => {
const TRANSLATION_OFFSET = 12;
const ROW_HEIGHT = 24;
let labelOffset, helperTextOffset;

if (scrollHeight > 0 && scrollHeight !== currentHeight) {
const diff = scrollHeight - currentHeight;
const totalRows = Math.abs(diff / ROW_HEIGHT);

labelOffset =
currentLabelOffset +
-1 * Math.sign(diff) * (TRANSLATION_OFFSET * totalRows);
helperTextOffset =
currentHelperTextOffset +
Math.sign(diff) * (TRANSLATION_OFFSET * totalRows);
}
return {
labelOffset,
helperTextOffset,
scrollHeight,
};
};

0 comments on commit ccf18fc

Please sign in to comment.