From a49ab8da4e02b9bd12c5ddd2e547235abc737109 Mon Sep 17 00:00:00 2001 From: Doug MacKenzie Date: Tue, 22 Oct 2024 14:54:21 +1100 Subject: [PATCH] fix(TextArea): Refactor autogrow to remove visual jank --- .../src/TextArea/TextArea.module.scss | 55 ++++++++------ packages/components/src/TextArea/TextArea.tsx | 74 ++++++------------- 2 files changed, 53 insertions(+), 76 deletions(-) diff --git a/packages/components/src/TextArea/TextArea.module.scss b/packages/components/src/TextArea/TextArea.module.scss index a56fb7adb5f..37d9a3bff7d 100644 --- a/packages/components/src/TextArea/TextArea.module.scss +++ b/packages/components/src/TextArea/TextArea.module.scss @@ -9,23 +9,43 @@ $input-disabled-opacity: 0.3; $input-disabled-border-alpha: 50%; .wrapper { - position: relative; + font-family: $typography-paragraph-body-font-family; + font-size: $typography-paragraph-body-font-size; + font-weight: $typography-paragraph-body-font-weight; + line-height: $typography-paragraph-body-line-height; + letter-spacing: $typography-paragraph-body-letter-spacing; + color: $color-purple-800-rgb; } -.textarea { - @include form-input-reset; +.wrapperAutogrow { + display: grid; +} - border-radius: $border-solid-border-radius; - width: 100%; +.wrapperAutogrow::after { + content: attr(data-value) " "; + white-space: pre-wrap; + visibility: hidden; +} + +.textareaAutogrow, +.wrapperAutogrow::after { border: $border-solid-border-width $border-solid-border-style $color-gray-500; + border-radius: $border-solid-border-radius; padding: $spacing-sm; - color: $color-purple-800-rgb; + box-sizing: border-box; + width: 100%; + font: inherit; + grid-area: 2 / 1; +} + +.textarea { display: block; resize: vertical; - @include form-input-placeholder { - line-height: 1.5; - color: $dt-color-form-text-color-placeholder; + &:focus { + outline: $border-focus-ring-border-width $border-focus-ring-border-style + $color-blue-500; + outline-offset: 1px; } &:disabled { @@ -33,17 +53,8 @@ $input-disabled-border-alpha: 50%; } } -.textarea:focus + .focusRing { - $focus-ring-offset: 3px; - - position: absolute; - background: transparent; - border-radius: $border-focus-ring-border-radius; - border-width: $border-focus-ring-border-width; - border-style: $border-focus-ring-border-style; - border-color: transparent; - inset: -$focus-ring-offset; - pointer-events: none; +.textareaAutogrow { + overflow: hidden; } .textarea.default { @@ -52,10 +63,6 @@ $input-disabled-border-alpha: 50%; border-color: $color-gray-600; } - &:focus + .focusRing { - border-color: $color-blue-500; - } - &:not(.error, .caution) { &:disabled { border-color: rgba($color-gray-500-rgb, $input-disabled-opacity); diff --git a/packages/components/src/TextArea/TextArea.tsx b/packages/components/src/TextArea/TextArea.tsx index 32103e62d16..adc52e713ed 100644 --- a/packages/components/src/TextArea/TextArea.tsx +++ b/packages/components/src/TextArea/TextArea.tsx @@ -1,9 +1,4 @@ -import React, { - TextareaHTMLAttributes, - useEffect, - useRef, - useState, -} from "react" +import React, { TextareaHTMLAttributes, useRef, useState } from "react" import classnames from "classnames" import { OverrideClassName } from "~components/types/OverrideClassName" import styles from "./TextArea.module.scss" @@ -11,6 +6,8 @@ import styles from "./TextArea.module.scss" export type TextAreaProps = { textAreaRef?: React.RefObject status?: "default" | "error" | "caution" + // Grows the input height as more content is added + // Replace with CSS field-sizing once it's supported by all major browsers autogrow?: boolean reversed?: boolean /** @@ -32,73 +29,46 @@ export const TextArea = ({ onChange: propsOnChange, ...restProps }: TextAreaProps): JSX.Element => { - const [textAreaHeight, setTextAreaHeight] = useState("auto") - const [parentHeight, setParentHeight] = useState("auto") const [internalValue, setInternalValue] = useState< string | number | readonly string[] | undefined - >(autogrow ? defaultValue : undefined) + >(autogrow && !value ? defaultValue : undefined) // ^ holds an internal state of the value so that autogrow can still work with uncontrolled textareas - // essentially forces the textarea into an (interally) controlled mode if autogrow is true - const textAreaRef = propsTextAreaRef || useRef(null) - - useEffect(() => { - if (!autogrow) return - - const scrollHeight = textAreaRef.current!.scrollHeight - if (scrollHeight < 1) return - - const borderWidth = textAreaRef.current - ? parseInt(getComputedStyle(textAreaRef.current).borderTopWidth, 10) - : 0 - const newHeight = scrollHeight + borderWidth * 2 - setParentHeight(`${newHeight}px`) - setTextAreaHeight(`${newHeight}px`) - }, [internalValue]) - - const onChange = !autogrow - ? undefined - : (event: React.ChangeEvent): void => { - setTextAreaHeight("auto") - // ^ this is required to avoid the textarea height from building up indefinitely - // see https://medium.com/@lucasalgus/creating-a-custom-auto-resize-textarea-component-for-your-react-web-application-6959c0ad68bc#2dee - - setInternalValue(event.target.value) - if (propsOnChange) { - propsOnChange(event) - } - } - - const getWrapperStyle = (): { minHeight: string } | undefined => - autogrow ? { minHeight: parentHeight } : undefined - - const getTextAreaStyle = (): { height: string } | undefined => - autogrow ? { height: textAreaHeight } : undefined + // essentially forces the textarea into an (interally) controlled mode if autogrow is true and mode is uncontrolled const controlledValue = value || internalValue + const textAreaRef = propsTextAreaRef || useRef(null) + + const onChange = (event: React.ChangeEvent): void => { + propsOnChange && propsOnChange(event) + setInternalValue(event.target.value) + } return ( -
+