From 41c6e81d2be09a24b3f93673cfef9aee1c8f7327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=A9=E9=81=93?= Date: Fri, 9 Sep 2022 20:38:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(component-rn):=20=E8=A7=A3=E5=86=B3=20focus?= =?UTF-8?q?=20=E5=8F=98=E6=9B=B4,=E8=81=9A=E7=84=A6=E5=A4=B1=E7=84=A6?= =?UTF-8?q?=E4=B8=8D=E5=90=8C=E6=AD=A5=20(#12466)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Input/PropsType.tsx | 6 +- .../src/components/Input/index.tsx | 337 +++++++++--------- .../src/components/Textarea/index.tsx | 25 +- .../taro-components-rn/src/utils/index.ts | 22 ++ 4 files changed, 209 insertions(+), 181 deletions(-) diff --git a/packages/taro-components-rn/src/components/Input/PropsType.tsx b/packages/taro-components-rn/src/components/Input/PropsType.tsx index f43757f28501..3f61a71796d0 100644 --- a/packages/taro-components-rn/src/components/Input/PropsType.tsx +++ b/packages/taro-components-rn/src/components/Input/PropsType.tsx @@ -22,7 +22,7 @@ export interface InputState { export interface InputProps extends FormItemProps{ style?: StyleProp; value?: string; - type: 'text' | 'number' | 'idcard' | 'digit'; + type?: 'text' | 'number' | 'idcard' | 'digit'; password?: boolean; placeholder?: string; disabled?: boolean; @@ -32,8 +32,8 @@ export interface InputProps extends FormItemProps{ confirmType: 'done' | 'send' | 'search' | 'next' | 'go'; confirmHold?: boolean; cursor?: number; - selectionStart: number; - selectionEnd: number; + selectionStart?: number; + selectionEnd?: number; placeholderStyle?: string; placeholderTextColor?: string; onInput?: (evt: Event) => void; diff --git a/packages/taro-components-rn/src/components/Input/index.tsx b/packages/taro-components-rn/src/components/Input/index.tsx index 81c97bee01c8..9dfce49e8b39 100644 --- a/packages/taro-components-rn/src/components/Input/index.tsx +++ b/packages/taro-components-rn/src/components/Input/index.tsx @@ -33,8 +33,8 @@ import { TextInputContentSizeChangeEventData, KeyboardTypeOptions } from 'react-native' -import { noop, omit, parseStyles } from '../../utils' -import { InputProps, InputState } from './PropsType' +import { noop, omit, parseStyles, useUpdateEffect } from '../../utils' +import { InputProps } from './PropsType' const keyboardTypeMap: { [key: string]: string } = { text: 'default', @@ -47,47 +47,69 @@ const keyboardTypeMap: { [key: string]: string } = { }) || '' } -// const confirmTypeMap = { -// done: '完成', -// send: '发送', -// search: '搜索', -// next: '下一个', -// go: '前往', -// } +const defaultProps = { + type: 'text', + maxlength: 140, + confirmType: 'done', + selectionStart: -1, + selectionEnd: -1 +} -class _Input extends React.Component { - static displayName = '_Input' - static defaultProps = { - type: 'text', - maxlength: 140, - confirmType: 'done', - selectionStart: -1, - selectionEnd: -1 - } +const _Input = (props: InputProps) => { + const { + style, + value, + type = defaultProps.type, + password, + placeholder, + disabled, + maxlength = defaultProps.maxlength, + confirmType, + confirmHold, + cursor, + selectionStart = defaultProps.selectionStart, + selectionEnd = defaultProps.selectionEnd, + _multiline, + _autoHeight, + autoFocus, + focus, + placeholderStyle + } = props - static getDerivedStateFromProps(props: InputProps, state: InputState): InputState | null { - return props.value !== state.value - ? { - ...state, - returnValue: props.value - } - : null - } + const [returnValue, setReturnValue] = React.useState() + /** 用于保存输入框值 */ + const tmpValue = React.useRef() + const [_height, setHeight] = React.useState(0) + const lineCount = React.useRef(0) + const inputRef = React.useRef() - state: InputState = { - returnValue: undefined, - height: 0, - value: undefined - } + React.useEffect(() => { + tmpValue.current = value + setReturnValue(val => { + if (val !== value) { + return value + } + return val + }) + }, [returnValue, value]) - tmpValue?: string - lineCount = 0 + // fix: https://github.com/NervJS/taro/issues/11350 + useUpdateEffect(() => { + if (!inputRef.current) { + return + } + if (focus) { + inputRef.current.focus() + } else { + inputRef.current.blur() + } + }, [focus]) - onChangeText = (text: string): void => { - const { onInput, onChange } = this.props - const { returnValue } = this.state + const onChangeText = React.useCallback((text: string): void => { + const { onInput, onChange } = props const onEvent = onInput || onChange - this.tmpValue = text || '' + tmpValue.current = text || '' + if (onEvent) { const result: unknown = onEvent({ target: { value: text }, @@ -96,33 +118,32 @@ class _Input extends React.Component { // Be care of flickering // @see https://facebook.github.io/react-native/docs/textinput.html#value if (typeof result === 'string') { - this.tmpValue = result - this.setState({ returnValue: result }) + tmpValue.current = result + setReturnValue(result) } else if (returnValue) { - this.setState({ returnValue: undefined }) + // 为了处理输入不合法,setState 相同值时,状态不更新,UI 也得不到更新,重置状态进而更新 + setReturnValue(undefined) } } - } + }, [returnValue]) - onFocus = (): void => { - const { onFocus = noop } = this.props - // event.detail = { value, height } - const { returnValue } = this.state - this.tmpValue = returnValue || '' + const onFocus = React.useCallback((): void => { + const { onFocus = noop } = props onFocus({ - target: { value: this.tmpValue || '' }, - detail: { value: this.tmpValue || '' } + target: { value: tmpValue.current || '' }, + detail: { value: tmpValue.current || '' } }) - } + }, [returnValue]) + + const onBlur = React.useCallback((): void => { + const { onBlur = noop } = props - onBlur = (): void => { - const { onBlur = noop } = this.props onBlur({ - target: { value: this.tmpValue || '' }, - detail: { value: this.tmpValue || '' } + target: { value: tmpValue.current || '' }, + detail: { value: tmpValue.current || '' } }) - } + }, []) /** * Callback that is called when a key is pressed. @@ -131,147 +152,129 @@ class _Input extends React.Component { * the typed-in character otherwise including `' '` for space. * Fires before `onChange` callbacks. */ - onKeyPress = (event: NativeSyntheticEvent): void => { - const { onKeyDown = noop, onConfirm = noop } = this.props + const onKeyPress = React.useCallback((event: NativeSyntheticEvent): void => { + const { onKeyDown = noop, onConfirm = noop } = props const keyValue = event.nativeEvent.key let which keyValue === 'Enter' && (which = 13) keyValue === 'Backspace' && (which = 8) onKeyDown({ which, - target: { value: this.tmpValue || '' }, - detail: { value: this.tmpValue || '' } + target: { value: tmpValue.current || '' }, + detail: { value: tmpValue.current || '' } }) if (keyValue !== 'Enter') return onConfirm({ - target: { value: this.tmpValue || '' }, - detail: { value: this.tmpValue || '' } + target: { value: tmpValue.current || '' }, + detail: { value: tmpValue.current || '' } }) - } + }, []) - onSubmitEditing = (): void => { - const { onKeyDown = noop, onConfirm = noop, _multiline } = this.props + const onSubmitEditing = React.useCallback((): void => { + const { onKeyDown = noop, onConfirm = noop } = props if (_multiline) return onKeyDown({ which: 13, - target: { value: this.tmpValue || '' }, - detail: { value: this.tmpValue || '' } + target: { value: tmpValue.current || '' }, + detail: { value: tmpValue.current || '' } }) onConfirm({ - target: { value: this.tmpValue || '' }, - detail: { value: this.tmpValue || '' } + target: { value: tmpValue.current || '' }, + detail: { value: tmpValue.current || '' } }) - } + }, [_multiline]) - onContentSizeChange = (event: NativeSyntheticEvent): void => { + const onContentSizeChange = React.useCallback((event: NativeSyntheticEvent): void => { const { width, height } = event.nativeEvent.contentSize // One of width and height may be 0. if (width && height) { - const { _multiline, _autoHeight, _onLineChange = noop } = this.props - if (!_multiline || !_autoHeight || height === this.state.height) return - this.lineCount += height > this.state.height ? 1 : -1 - _onLineChange({ detail: { height, lineCount: this.lineCount } }) - this.setState({ height }) + const { _onLineChange = noop } = props + if (!_multiline || !_autoHeight || height === _height) return + lineCount.current += height > _height ? 1 : -1 + _onLineChange({ detail: { height, lineCount: lineCount.current } }) + setHeight(height) } - } + }, [_height, _multiline, _autoHeight]) - render(): JSX.Element { - let { - style, - value, - type, - password, - placeholder, - disabled, - maxlength, - confirmType, - confirmHold, - cursor, - selectionStart, - selectionEnd, - _multiline, - _autoHeight, - autoFocus, - focus, - placeholderStyle, - } = this.props - const keyboardType: KeyboardTypeOptions = keyboardTypeMap[type] as KeyboardTypeOptions + const keyboardType: KeyboardTypeOptions = keyboardTypeMap[type] as KeyboardTypeOptions + const selection = (() => { + if (selectionStart >= 0 && selectionEnd >= 0) { + return { start: selectionStart, end: selectionEnd } + } else if (typeof cursor === 'number') { + return { start: cursor, end: cursor } + } + })() - const selection = (() => { - if (selectionStart >= 0 && selectionEnd >= 0) { - return { start: selectionStart, end: selectionEnd } - } else if (typeof cursor === 'number') { - return { start: cursor, end: cursor } - } - })() + const defaultValue = type === 'number' && value ? value + '' : value - value = type === 'number' && value ? value + '' : value + // fix: https://reactnative.dev/docs/textinput#multiline + const textAlignVertical = _multiline ? 'top' : 'auto' + const placeholderTextColor = props.placeholderTextColor || parseStyles(placeholderStyle)?.color - // fix: https://reactnative.dev/docs/textinput#multiline - const textAlignVertical = _multiline ? 'top' : 'auto' + const inputProps = omit(props, [ + 'style', + 'value', + 'type', + 'password', + 'placeholder', + 'disabled', + 'maxlength', + 'confirmType', + 'confirmHold', + 'cursor', + 'selectionStart', + 'selectionEnd', + 'onInput', + 'onFocus', + 'onBlur', + 'onKeyDown', + 'onConfirm', + '_multiline', + '_autoHeight', + '_onLineChange', + 'placeholderStyle', + 'placeholderTextColor', + ]) - const placeholderTextColor = this.props.placeholderTextColor || parseStyles(placeholderStyle)?.color + return ( + + ) +} - const props = omit(this.props, [ - 'style', - 'value', - 'type', - 'password', - 'placeholder', - 'disabled', - 'maxlength', - 'confirmType', - 'confirmHold', - 'cursor', - 'selectionStart', - 'selectionEnd', - 'onInput', - 'onFocus', - 'onBlur', - 'onKeyDown', - 'onConfirm', - '_multiline', - '_autoHeight', - '_onLineChange', - 'placeholderStyle', - 'placeholderTextColor', - ]) +_Input.defaultProps = defaultProps as InputProps - return ( - - ) - } -} +_Input.displayName = '_Input' export default _Input diff --git a/packages/taro-components-rn/src/components/Textarea/index.tsx b/packages/taro-components-rn/src/components/Textarea/index.tsx index e69f74c86c38..7b8d67c6e020 100644 --- a/packages/taro-components-rn/src/components/Textarea/index.tsx +++ b/packages/taro-components-rn/src/components/Textarea/index.tsx @@ -16,25 +16,28 @@ import { omit } from '../../utils' import { TextareaProps } from './PropsType' const _Textarea: React.FC = (props: TextareaProps) => { - const { autoHeight, onLineChange, autoFocus, maxlength } = props + const { autoHeight, autoFocus, focus, maxlength, onLineChange } = props + const textearaProps = omit(props, [ + 'type', + 'password', + 'confirmType', + 'confirmHold', + // props + 'autoHeight', + 'onLineChange', + 'maxlength' + ]) + return ( Keyboard.dismiss()} - {...omit(props, [ - 'type', - 'password', - 'confirmType', - 'confirmHold', - // props - 'autoHeight', - 'onLineChange', - 'maxlength' - ])} + {...textearaProps} maxlength={maxlength} /> ) diff --git a/packages/taro-components-rn/src/utils/index.ts b/packages/taro-components-rn/src/utils/index.ts index 33095621da54..1bd34e25e38a 100644 --- a/packages/taro-components-rn/src/utils/index.ts +++ b/packages/taro-components-rn/src/utils/index.ts @@ -5,6 +5,8 @@ import { TextStyle } from 'react-native' +import * as React from 'react' + // @see https://facebook.github.io/react-native/docs/layout-props.html // @see https://facebook.github.io/react-native/docs/view-style-props.html // @todo According to the source code of ScrollView, ['alignItems','justifyContent'] should be set to contentContainerStyle @@ -80,9 +82,29 @@ export const parseStyles = (styles = ''): { [key: string]: string } => { // eslint-disable-next-line export const noop = (..._args: any[]): void => {} +export const useUpdateEffect = (effect, deps) => { + const isMounted = React.useRef(false) + + // for react-refresh + React.useEffect(() => { + return () => { + isMounted.current = false + } + }, []) + + React.useEffect(() => { + if (!isMounted.current) { + isMounted.current = true + } else { + return effect() + } + }, deps) +} + export default { omit, dismemberStyle, parseStyles, noop, + useUpdateEffect }