Checkpoint autosize
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useMemo, useRef, useState} from 'react'
|
import {useMemo, useRef, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
TextInput,
|
TextInput,
|
||||||
type TextInputContentSizeChangeEvent,
|
type TextInputContentSizeChangeEvent,
|
||||||
@@ -46,16 +46,14 @@ export function AutosizedTextarea({
|
|||||||
const mh = lineHeight * minRows + verticalSpace
|
const mh = lineHeight * minRows + verticalSpace
|
||||||
const xh = maxRows ? lineHeight * maxRows + verticalSpace : Infinity
|
const xh = maxRows ? lineHeight * maxRows + verticalSpace : Infinity
|
||||||
/*
|
/*
|
||||||
* On web, we set an initial height and auto-resize via DOM measurement.
|
* iOS: minHeight/maxHeight works fine natively.
|
||||||
* On Android, we drive height explicitly via onContentSizeChange to avoid
|
* Web + Android: we set an explicit initial height and resize dynamically
|
||||||
* sub-pixel oscillation that causes layout jumpiness.
|
* (web via DOM measurement, Android via onContentSizeChange state).
|
||||||
* On iOS, minHeight/maxHeight works fine natively.
|
*
|
||||||
|
* iOS also seems to need 1px headroom to actually expand to the correct
|
||||||
|
* maxHeight
|
||||||
*/
|
*/
|
||||||
const tas = IS_WEB
|
const tas = IS_IOS ? {minHeight: mh, maxHeight: xh + 1} : {height: mh}
|
||||||
? {height: mh}
|
|
||||||
: IS_ANDROID
|
|
||||||
? {height: mh}
|
|
||||||
: {minHeight: mh, maxHeight: xh}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
processedStyle: {
|
processedStyle: {
|
||||||
@@ -76,16 +74,12 @@ export function AutosizedTextarea({
|
|||||||
const [androidInputHeight, setAndroidInputHeight] = useState(minHeight)
|
const [androidInputHeight, setAndroidInputHeight] = useState(minHeight)
|
||||||
|
|
||||||
const prevHeight = useRef(0)
|
const prevHeight = useRef(0)
|
||||||
const onChangeText = (text: string) => {
|
const resizeWeb = () => {
|
||||||
if (IS_WEB) {
|
|
||||||
const el = textInputRef.current as unknown as HTMLTextAreaElement
|
const el = textInputRef.current as unknown as HTMLTextAreaElement
|
||||||
if (el) {
|
if (!el) return
|
||||||
el.style.height = '0px'
|
el.style.height = '0px'
|
||||||
const scrollHeight = el.scrollHeight
|
const scrollHeight = el.scrollHeight
|
||||||
const nextHeight = Math.min(
|
const nextHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight)
|
||||||
Math.max(scrollHeight, minHeight),
|
|
||||||
maxHeight,
|
|
||||||
)
|
|
||||||
el.style.height = `${nextHeight}px`
|
el.style.height = `${nextHeight}px`
|
||||||
el.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden'
|
el.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden'
|
||||||
if (nextHeight !== prevHeight.current) {
|
if (nextHeight !== prevHeight.current) {
|
||||||
@@ -93,40 +87,29 @@ export function AutosizedTextarea({
|
|||||||
onUpdateHeight?.(nextHeight)
|
onUpdateHeight?.(nextHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (IS_IOS) {
|
|
||||||
textInputRef.current?.measure((_x, _y, _w, h) => {
|
|
||||||
if (h !== prevHeight.current) {
|
|
||||||
prevHeight.current = h
|
|
||||||
onUpdateHeight?.(h)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const onChangeText = (text: string) => {
|
||||||
|
if (IS_WEB) resizeWeb()
|
||||||
onChangeTextOuter?.(text)
|
onChangeTextOuter?.(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Native height tracking: on Android we ceil to stabilize sub-pixel
|
||||||
|
* oscillation and drive height via state; on iOS we just notify.
|
||||||
|
*/
|
||||||
const onContentSizeChange = (e: TextInputContentSizeChangeEvent) => {
|
const onContentSizeChange = (e: TextInputContentSizeChangeEvent) => {
|
||||||
if (IS_ANDROID) {
|
|
||||||
const h = Math.ceil(e.nativeEvent.contentSize.height)
|
const h = Math.ceil(e.nativeEvent.contentSize.height)
|
||||||
const nextHeight = Math.min(Math.max(h, minHeight), maxHeight)
|
const nextHeight = Math.min(Math.max(h, minHeight), maxHeight)
|
||||||
if (nextHeight !== androidInputHeight) {
|
|
||||||
setAndroidInputHeight(nextHeight)
|
if (nextHeight !== prevHeight.current) {
|
||||||
}
|
prevHeight.current = nextHeight
|
||||||
|
if (IS_ANDROID) setAndroidInputHeight(nextHeight)
|
||||||
|
onUpdateHeight?.(nextHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentSizeChangeOuter?.(e)
|
onContentSizeChangeOuter?.(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* On Android, height is driven by onContentSizeChange (see the TextInput
|
|
||||||
* below), so we update the autocomplete position when that height changes.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (IS_ANDROID) {
|
|
||||||
onUpdateHeight?.(androidInputHeight)
|
|
||||||
}
|
|
||||||
}, [androidInputHeight, onUpdateHeight])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
multiline
|
multiline
|
||||||
@@ -149,12 +132,18 @@ export function AutosizedTextarea({
|
|||||||
outline: 'none',
|
outline: 'none',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
overscrollBehavior: 'none',
|
|
||||||
}),
|
}),
|
||||||
processedStyle,
|
processedStyle,
|
||||||
]}
|
]}
|
||||||
{...rest}
|
{...rest}
|
||||||
ref={mergeRefs([textInputRef, ref])}
|
ref={mergeRefs([
|
||||||
|
(node: TextInput | null) => {
|
||||||
|
textInputRef.current = node
|
||||||
|
// bop resize on first render
|
||||||
|
if (IS_WEB && node) resizeWeb()
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
])}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
onContentSizeChange={onContentSizeChange}
|
onContentSizeChange={onContentSizeChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function Forms() {
|
|||||||
|
|
||||||
<View style={[a.gap_md, a.align_start, a.w_full]}>
|
<View style={[a.gap_md, a.align_start, a.w_full]}>
|
||||||
<AutosizedTextarea
|
<AutosizedTextarea
|
||||||
label="AutosizedTextarea minRows=1 maxRows=5"
|
label="minRows=1 maxRows=5"
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.p_md,
|
a.p_md,
|
||||||
@@ -51,7 +51,19 @@ export function Forms() {
|
|||||||
maxRows={5}
|
maxRows={5}
|
||||||
/>
|
/>
|
||||||
<AutosizedTextarea
|
<AutosizedTextarea
|
||||||
label="AutosizedTextarea minRows=3 maxRows=10"
|
label="defaultValue minRows=1 maxRows=2"
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.p_md,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_medium,
|
||||||
|
]}
|
||||||
|
maxRows={2}
|
||||||
|
defaultValue="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec auctor, nisl eget ultricies lacinia, nunc nisl aliquam nisl, eget aliquam nunc nisl eget nunc."
|
||||||
|
/>
|
||||||
|
<AutosizedTextarea
|
||||||
|
label="minRows=3 maxRows=10"
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.p_md,
|
a.p_md,
|
||||||
|
|||||||
Reference in New Issue
Block a user