Skip to content

Commit 7d33744

Browse files
authored
fix: fix text area auto-expand when controlled (#912)
1 parent b24841d commit 7d33744

File tree

2 files changed

+91
-54
lines changed

2 files changed

+91
-54
lines changed

src/text-area/text-area.stories.mdx

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Meta, Story, Canvas, ArgsTable, Description } from '@storybook/addon-docs'
22

3+
import { Box } from '../box'
34
import { Stack } from '../stack'
45
import { Text } from '../text'
56
import { TextArea } from './'
@@ -182,20 +183,30 @@ Note that these variables are shared with other components such as `Textfield`,
182183
</Canvas>
183184

184185
export function AutoExpandStory(props) {
186+
const [value, setValue] = React.useState('')
185187
return (
186-
<Stack space="xxlarge" dividers="secondary" maxWidth="medium">
188+
<Stack space="large" dividers="secondary" maxWidth="medium">
187189
<TextArea
188190
{...props}
189-
label="What do you want to accomplish?"
190-
message="Write as much or as little as you want. The input area will auto-expand to fit what you've typed."
191+
label="Text area with auto-expand"
192+
auxiliaryLabel="(controlled)"
191193
autoExpand
194+
value={value}
195+
onChange={(event) => setValue(event.target.value)}
196+
message="Write as much or as little as you want. The input area will auto-expand to fit what you've typed."
197+
rows={1}
198+
onKeyDown={(event) => {
199+
if (event.key === 'Enter') {
200+
event.preventDefault()
201+
setValue('') // Clear the input programmatically
202+
}
203+
}}
192204
/>
193-
<TextArea
194-
{...props}
195-
label="What do you want to accomplish?"
196-
message="This one will not auto-expand."
197-
autoExpand={false}
198-
/>
205+
<Text size="caption" tone="secondary">
206+
If you press Enter, the input will be cleared. This allows you to test that
207+
auto-expand works when the input is cleared programmatically, shrinking the textarea
208+
to the new expected height.
209+
</Text>
199210
</Stack>
200211
)
201212
}
@@ -252,26 +263,12 @@ export function AutoExpandStory(props) {
252263
export function AutoExpandWithInitialValueStory(props) {
253264
const initialValue =
254265
'This is some text that takes up multiple lines. It should cause the textarea to render initially as large as needed to fit this text, even if its initial rows are not enough.'
255-
const [value, setValue] = React.useState(initialValue)
256266
return (
257267
<Stack space="xxlarge" dividers="secondary" maxWidth="medium">
258268
<TextArea
259269
{...props}
260-
label="What do you want to accomplish?"
261-
auxiliaryLabel={
262-
<Text tone="secondary" size="caption">
263-
{value.length}
264-
</Text>
265-
}
266-
message="Write as much or as little as you want. The input area will auto-expand to fit what you've typed."
267-
value={value}
268-
onChange={(event) => setValue(event.target.value)}
269-
rows={1}
270-
autoExpand
271-
/>
272-
<TextArea
273-
{...props}
274-
label="What do you want to accomplish?"
270+
label="Text area with auto-expand and initial value"
271+
auxiliaryLabel="(uncontrolled)"
275272
message="Write as much or as little as you want. The input area will auto-expand to fit what you've typed."
276273
defaultValue={initialValue}
277274
rows={1}

src/text-area/text-area.tsx

Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,18 @@ import styles from './text-area.module.css'
77

88
interface TextAreaProps
99
extends Omit<FieldComponentProps<HTMLTextAreaElement>, 'characterCountPosition'>,
10-
Omit<BaseFieldVariantProps, 'supportsStartAndEndSlots' | 'endSlot' | 'endSlotPosition'> {
10+
Omit<
11+
BaseFieldVariantProps,
12+
'supportsStartAndEndSlots' | 'endSlot' | 'endSlotPosition' | 'value'
13+
> {
14+
/**
15+
* The value of the text area.
16+
*
17+
* If this prop is not specified, the text area will be uncontrolled and the component will
18+
* manage its own state.
19+
*/
20+
value?: string
21+
1122
/**
1223
* The number of visible text lines for the text area.
1324
*
@@ -62,39 +73,13 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(function T
6273
const internalRef = React.useRef<HTMLTextAreaElement>(null)
6374
const combinedRef = useMergeRefs([ref, internalRef])
6475

76+
useAutoExpand({ value, autoExpand, containerRef, internalRef })
77+
6578
const textAreaClassName = classNames([
6679
autoExpand ? styles.disableResize : null,
6780
disableResize ? styles.disableResize : null,
6881
])
6982

70-
React.useEffect(
71-
function setupAutoExpand() {
72-
const containerElement = containerRef.current
73-
74-
function handleAutoExpand(value: string) {
75-
if (containerElement) {
76-
containerElement.dataset.replicatedValue = value
77-
}
78-
}
79-
80-
function handleInput(event: Event) {
81-
handleAutoExpand((event.currentTarget as HTMLTextAreaElement).value)
82-
}
83-
84-
const textAreaElement = internalRef.current
85-
if (!textAreaElement || !autoExpand) {
86-
return undefined
87-
}
88-
89-
// Apply change initially, in case the text area has a non-empty initial value
90-
handleAutoExpand(textAreaElement.value)
91-
92-
textAreaElement.addEventListener('input', handleInput)
93-
return () => textAreaElement.removeEventListener('input', handleInput)
94-
},
95-
[autoExpand],
96-
)
97-
9883
return (
9984
<BaseField
10085
variant={variant}
@@ -139,5 +124,60 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(function T
139124
)
140125
})
141126

127+
function useAutoExpand({
128+
value,
129+
autoExpand,
130+
containerRef,
131+
internalRef,
132+
}: {
133+
value: string | undefined
134+
autoExpand: boolean
135+
containerRef: React.RefObject<HTMLDivElement>
136+
internalRef: React.RefObject<HTMLTextAreaElement>
137+
}) {
138+
const isControlled = value !== undefined
139+
140+
React.useEffect(
141+
function setupAutoExpandWhenUncontrolled() {
142+
const textAreaElement = internalRef.current
143+
if (!textAreaElement || !autoExpand || isControlled) {
144+
return undefined
145+
}
146+
147+
const containerElement = containerRef.current
148+
149+
function handleAutoExpand(value: string) {
150+
if (containerElement) {
151+
containerElement.dataset.replicatedValue = value
152+
}
153+
}
154+
155+
function handleInput(event: Event) {
156+
handleAutoExpand((event.currentTarget as HTMLTextAreaElement).value)
157+
}
158+
159+
// Apply change initially, in case the text area has a non-empty initial value
160+
handleAutoExpand(textAreaElement.value)
161+
textAreaElement.addEventListener('input', handleInput)
162+
return () => textAreaElement.removeEventListener('input', handleInput)
163+
},
164+
[autoExpand, containerRef, internalRef, isControlled],
165+
)
166+
167+
React.useEffect(
168+
function setupAutoExpandWhenControlled() {
169+
if (!isControlled) {
170+
return
171+
}
172+
173+
const containerElement = containerRef.current
174+
if (containerElement) {
175+
containerElement.dataset.replicatedValue = value
176+
}
177+
},
178+
[value, containerRef, isControlled],
179+
)
180+
}
181+
142182
export { TextArea }
143183
export type { TextAreaProps }

0 commit comments

Comments
 (0)