Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 166 lines 4.9 kB view raw
1import {useMemo, useRef, useState} from 'react' 2import { 3 TextInput, 4 type TextInputContentSizeChangeEvent, 5 type TextInputProps, 6} from 'react-native' 7 8import {mergeRefs} from '#/lib/merge-refs' 9import {atoms as a, extractPadding, useAlf, web} from '#/alf' 10import {normalizeTextStyles} from '#/alf/typography' 11import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env' 12 13export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & { 14 ref?: React.Ref<TextInput> 15 label: string 16 minRows?: number 17 maxRows?: number 18 onUpdateHeight?: (height: number) => void 19} 20 21export function AutosizedTextarea({ 22 ref, 23 label, 24 minRows = 1, 25 maxRows, 26 onUpdateHeight, 27 28 onChangeText: onChangeTextOuter, 29 onContentSizeChange: onContentSizeChangeOuter, 30 style: outerStyle, 31 ...rest 32}: AutosizedTextareaProps) { 33 const {theme: t, fonts} = useAlf() 34 const internalRef = useRef<TextInput>(null) 35 const {style, minInputHeight, maxInputHeight, verticalContentPadding} = 36 useMemo(() => { 37 const normalizedStyles = normalizeTextStyles( 38 [a.text_md, a.leading_snug, t.atoms.text, outerStyle], 39 { 40 fontScale: fonts.scaleMultiplier, 41 fontFamily: fonts.family, 42 flags: {}, 43 }, 44 ) 45 const lineHeight = normalizedStyles.lineHeight || 20 46 const {paddingTop, paddingBottom} = extractPadding(normalizedStyles ?? {}) 47 const verticalContentPadding = paddingTop + paddingBottom 48 const minInputHeight = lineHeight * minRows + verticalContentPadding 49 const maxInputHeight = maxRows 50 ? lineHeight * maxRows + verticalContentPadding 51 : Infinity 52 53 /* 54 * iOS: minHeight/maxHeight works fine natively. 55 * Web + Android: we set an explicit initial height and resize dynamically 56 * (web via DOM measurement, Android via onContentSizeChange state). 57 * 58 * iOS also seems to need 1px headroom to actually expand to the correct 59 * maxHeight 60 */ 61 const heightConstraints = IS_IOS 62 ? {minHeight: minInputHeight, maxHeight: maxInputHeight + 1} 63 : {height: minInputHeight} 64 65 return { 66 style: { 67 ...normalizedStyles, 68 ...heightConstraints, 69 }, 70 minInputHeight, 71 maxInputHeight, 72 verticalContentPadding, 73 } 74 }, [t, fonts, outerStyle, minRows, maxRows]) 75 76 /* 77 * Web handling 78 */ 79 const prevWebHeight = useRef(0) 80 const handleResizeWeb = () => { 81 const el = internalRef.current as unknown as HTMLTextAreaElement 82 if (!el) return 83 // collapse to get natural scroll height 84 el.style.height = '0px' 85 const scrollHeight = Math.ceil(el.scrollHeight) 86 const nextHeight = Math.min( 87 Math.max(scrollHeight, minInputHeight), 88 maxInputHeight, 89 ) 90 // immediately update height to prevent flicker 91 el.style.height = `${nextHeight}px` 92 el.style.overflowY = scrollHeight > maxInputHeight ? 'auto' : 'hidden' 93 if (nextHeight !== prevWebHeight.current) { 94 prevWebHeight.current = nextHeight 95 onUpdateHeight?.(nextHeight) 96 } 97 } 98 const onChangeText = (text: string) => { 99 if (IS_WEB) handleResizeWeb() 100 onChangeTextOuter?.(text) 101 } 102 103 /* 104 * Native handling 105 * 106 * We track the height as state on native, and on Android, we use this to 107 * directly drive the `height`. 108 */ 109 const [nativeHeight, setNativeHeight] = useState(minInputHeight) 110 const onContentSizeChange = (e: TextInputContentSizeChangeEvent) => { 111 const contentSize = Math.ceil(e.nativeEvent.contentSize.height) 112 // ios reports the content size without padding 113 const height = IS_IOS ? contentSize + verticalContentPadding : contentSize 114 const nextHeight = Math.min( 115 Math.max(height, minInputHeight), 116 maxInputHeight, 117 ) 118 119 if (nextHeight !== nativeHeight) { 120 setNativeHeight(nextHeight) 121 onUpdateHeight?.(nextHeight) 122 } 123 124 onContentSizeChangeOuter?.(e) 125 } 126 127 return ( 128 <TextInput 129 multiline 130 placeholderTextColor={t.palette.contrast_500} 131 accessibilityLabel={label} 132 accessibilityHint={label} 133 placeholder={label} 134 keyboardAppearance={t.scheme} 135 submitBehavior="newline" 136 scrollEnabled={nativeHeight >= maxInputHeight} 137 style={[ 138 a.relative, 139 a.border_0, 140 { 141 textAlignVertical: 'top', 142 includeFontPadding: false, 143 }, 144 web({ 145 resize: 'none', 146 outline: 'none', 147 whiteSpace: 'pre-wrap', 148 wordBreak: 'break-word', 149 }), 150 style, 151 IS_ANDROID ? {height: nativeHeight} : {}, 152 ]} 153 {...rest} 154 ref={mergeRefs([ 155 (node: TextInput | null) => { 156 internalRef.current = node 157 // bop resize on first render 158 if (IS_WEB && node) handleResizeWeb() 159 }, 160 ref, 161 ])} 162 onChangeText={onChangeText} 163 onContentSizeChange={onContentSizeChange} 164 /> 165 ) 166}