Fix for AutosizedTextarea not resetting height after clear on Android (#10927)

This commit is contained in:
Eric Bailey
2026-06-16 16:16:27 -05:00
committed by GitHub
parent e057d3bd54
commit 9825057bef
2 changed files with 30 additions and 32 deletions
+1 -10
View File
@@ -34,7 +34,6 @@ import {
} from '#/components/Autocomplete' } from '#/components/Autocomplete'
import { import {
AutosizedTextarea, AutosizedTextarea,
type AutosizedTextareaHeightApi,
type AutosizedTextareaProps, type AutosizedTextareaProps,
} from '#/components/forms/AutosizedTextarea' } from '#/components/forms/AutosizedTextarea'
import {Span, Text} from '#/components/Typography' import {Span, Text} from '#/components/Typography'
@@ -151,13 +150,6 @@ export function Composer({
*/ */
const inputScrollSharedValue = useSharedValue(0) const inputScrollSharedValue = useSharedValue(0)
/*
* Imperative handle on the underlying textarea, used to reset its height when
* the input is cleared programmatically (Android doesn't fire
* `onContentSizeChange` in that case).
*/
const heightApiRef = useRef<AutosizedTextareaHeightApi>(null)
/* /*
* Expose imperative internal API * Expose imperative internal API
*/ */
@@ -168,7 +160,6 @@ export function Composer({
clear: () => { clear: () => {
tapper.inputProps.onChangeText('') tapper.inputProps.onChangeText('')
inputScrollSharedValue.value = 0 inputScrollSharedValue.value = 0
heightApiRef.current?.resetHeight()
}, },
insert: tapper.insert, insert: tapper.insert,
setAutocompleteAnchor: sift.refs.setAnchor, setAutocompleteAnchor: sift.refs.setAnchor,
@@ -330,7 +321,6 @@ export function Composer({
</View> </View>
)} )}
<AutosizedTextarea <AutosizedTextarea
heightApiRef={heightApiRef}
placeholderTextColor={t.palette.contrast_500} placeholderTextColor={t.palette.contrast_500}
accessibilityLabel={label} accessibilityLabel={label}
accessibilityHint={label} accessibilityHint={label}
@@ -356,6 +346,7 @@ export function Composer({
{...tapper.inputProps} {...tapper.inputProps}
{...sift.targetProps} {...sift.targetProps}
ref={mergeRefs([ref, tapper.inputProps.ref, sift.targetProps.ref])} ref={mergeRefs([ref, tapper.inputProps.ref, sift.targetProps.ref])}
rawValue={tapper.state.text}
onBlur={e => { onBlur={e => {
rest.onBlur?.(e) rest.onBlur?.(e)
setActiveFacet(null) setActiveFacet(null)
+29 -22
View File
@@ -1,4 +1,4 @@
import {useImperativeHandle, useMemo, useRef, useState} from 'react' import {useEffect, useMemo, useRef, useState} from 'react'
import { import {
TextInput, TextInput,
type TextInputContentSizeChangeEvent, type TextInputContentSizeChangeEvent,
@@ -10,31 +10,32 @@ import {atoms as a, extractPadding, useAlf, web} from '#/alf'
import {normalizeTextStyles} from '#/alf/typography' import {normalizeTextStyles} from '#/alf/typography'
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env' import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
export type AutosizedTextareaHeightApi = {
/**
* Reset the input back to its minimum height. Needed on Android, where the
* height is driven by state that only updates via `onContentSizeChange` -
* that event doesn't fire when the value is cleared programmatically.
*/
resetHeight: () => void
}
export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & { export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & {
ref?: React.Ref<TextInput> ref?: React.Ref<TextInput>
heightApiRef?: React.Ref<AutosizedTextareaHeightApi>
label: string label: string
minRows?: number minRows?: number
maxRows?: number maxRows?: number
onUpdateHeight?: (height: number) => void onUpdateHeight?: (height: number) => void
/**
* In some cases, like on native, we may not pass in an actual `value` prop.
* This prop allows Android to know if the field was cleared and thereby
* reset its height. This is a small hack required because we need to
* explicitly set the `{height: nativeHeight}` on Android.
*
* If you notice height calcuation issues after clearing the field on
* Android, check if this value is being populated and cleared properly in
* the parent component.
*/
rawValue?: string
} }
export function AutosizedTextarea({ export function AutosizedTextarea({
ref, ref,
heightApiRef,
label, label,
minRows = 1, minRows = 1,
maxRows, maxRows,
onUpdateHeight, onUpdateHeight,
rawValue,
onChangeText: onChangeTextOuter, onChangeText: onChangeTextOuter,
onContentSizeChange: onContentSizeChangeOuter, onContentSizeChange: onContentSizeChangeOuter,
@@ -118,16 +119,6 @@ export function AutosizedTextarea({
* directly drive the `height`. * directly drive the `height`.
*/ */
const [nativeHeight, setNativeHeight] = useState(minInputHeight) const [nativeHeight, setNativeHeight] = useState(minInputHeight)
useImperativeHandle(
heightApiRef,
() => ({
resetHeight: () => {
setNativeHeight(minInputHeight)
onUpdateHeight?.(minInputHeight)
},
}),
[minInputHeight, onUpdateHeight],
)
const onContentSizeChange = (e: TextInputContentSizeChangeEvent) => { const onContentSizeChange = (e: TextInputContentSizeChangeEvent) => {
const contentSize = Math.ceil(e.nativeEvent.contentSize.height) const contentSize = Math.ceil(e.nativeEvent.contentSize.height)
// ios reports the content size without padding // ios reports the content size without padding
@@ -145,6 +136,22 @@ export function AutosizedTextarea({
onContentSizeChangeOuter?.(e) onContentSizeChangeOuter?.(e)
} }
/*
* Manual height clearing required for Android because we're forced to set
* `{height: nativeHeight}` on Android, and even though `onContentSizeChange`
* fires, the height matches the existing height and we aren't able to reset.
*/
const prevRawValue = useRef(rawValue || '')
useEffect(() => {
if (!IS_ANDROID) return // everything else is fine
if (rawValue === undefined) return // uncontrolled
if (prevRawValue.current?.length && rawValue === '') {
setNativeHeight(minInputHeight)
onUpdateHeight?.(minInputHeight)
}
prevRawValue.current = rawValue
}, [rawValue, minInputHeight])
return ( return (
<TextInput <TextInput
multiline multiline