diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index cf79abddf7..d93c7bb91a 100644 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -34,7 +34,6 @@ import { } from '#/components/Autocomplete' import { AutosizedTextarea, - type AutosizedTextareaHeightApi, type AutosizedTextareaProps, } from '#/components/forms/AutosizedTextarea' import {Span, Text} from '#/components/Typography' @@ -151,13 +150,6 @@ export function Composer({ */ 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(null) - /* * Expose imperative internal API */ @@ -168,7 +160,6 @@ export function Composer({ clear: () => { tapper.inputProps.onChangeText('') inputScrollSharedValue.value = 0 - heightApiRef.current?.resetHeight() }, insert: tapper.insert, setAutocompleteAnchor: sift.refs.setAnchor, @@ -330,7 +321,6 @@ export function Composer({ )} { rest.onBlur?.(e) setActiveFacet(null) diff --git a/src/components/forms/AutosizedTextarea.tsx b/src/components/forms/AutosizedTextarea.tsx index 7863f05eb6..d81265f4da 100644 --- a/src/components/forms/AutosizedTextarea.tsx +++ b/src/components/forms/AutosizedTextarea.tsx @@ -1,4 +1,4 @@ -import {useImperativeHandle, useMemo, useRef, useState} from 'react' +import {useEffect, useMemo, useRef, useState} from 'react' import { TextInput, type TextInputContentSizeChangeEvent, @@ -10,31 +10,32 @@ import {atoms as a, extractPadding, useAlf, web} from '#/alf' import {normalizeTextStyles} from '#/alf/typography' 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 & { ref?: React.Ref - heightApiRef?: React.Ref label: string minRows?: number maxRows?: number 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({ ref, - heightApiRef, label, minRows = 1, maxRows, onUpdateHeight, + rawValue, onChangeText: onChangeTextOuter, onContentSizeChange: onContentSizeChangeOuter, @@ -118,16 +119,6 @@ export function AutosizedTextarea({ * directly drive the `height`. */ const [nativeHeight, setNativeHeight] = useState(minInputHeight) - useImperativeHandle( - heightApiRef, - () => ({ - resetHeight: () => { - setNativeHeight(minInputHeight) - onUpdateHeight?.(minInputHeight) - }, - }), - [minInputHeight, onUpdateHeight], - ) const onContentSizeChange = (e: TextInputContentSizeChangeEvent) => { const contentSize = Math.ceil(e.nativeEvent.contentSize.height) // ios reports the content size without padding @@ -145,6 +136,22 @@ export function AutosizedTextarea({ 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 (