Skip to content

Commit

Permalink
fix(TextArea): Refactor autogrow to remove visual jank
Browse files Browse the repository at this point in the history
  • Loading branch information
dougmacknz committed Oct 22, 2024
1 parent 03bc020 commit a49ab8d
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 76 deletions.
55 changes: 31 additions & 24 deletions packages/components/src/TextArea/TextArea.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,52 @@ $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 {
resize: none;
}
}

.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 {
Expand All @@ -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);
Expand Down
74 changes: 22 additions & 52 deletions packages/components/src/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
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"

export type TextAreaProps = {
textAreaRef?: React.RefObject<HTMLTextAreaElement>
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
/**
Expand All @@ -32,73 +29,46 @@ export const TextArea = ({
onChange: propsOnChange,
...restProps
}: TextAreaProps): JSX.Element => {
const [textAreaHeight, setTextAreaHeight] = useState<string>("auto")
const [parentHeight, setParentHeight] = useState<string>("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<HTMLTextAreaElement>): 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<HTMLTextAreaElement>): void => {
propsOnChange && propsOnChange(event)
setInternalValue(event.target.value)
}

return (
<div className={styles.wrapper} style={getWrapperStyle()}>
<div
className={classnames(styles.wrapper, {
[styles.wrapperAutogrow]: autogrow,
})}
data-value={autogrow ? controlledValue : undefined}
>
<textarea
className={classnames(
styles.textarea,
styles[status],
reversed ? styles.reversed : styles.default,
disabled && styles.disabled
{
[styles.disabled]: disabled,
[styles.textareaAutogrow]: autogrow,
}
)}
rows={rows}
onChange={onChange || propsOnChange}
onChange={onChange}
value={controlledValue}
defaultValue={controlledValue ? undefined : defaultValue}
// ^ React throws a warning if you specify both a value and a defaultValue
ref={textAreaRef}
style={getTextAreaStyle()}
disabled={disabled}
{...restProps}
/>

{/* Textareas aren't able to have pseudo elements like ::after on them,
so we have to create an element ourselves for the focus ring */}
<div className={styles.focusRing} />
</div>
)
}
Expand Down

0 comments on commit a49ab8d

Please sign in to comment.