UNPKG

@mattermost/react-native-paste-input

Version:

React Native TextInput replacement to allow pasting files

434 lines (427 loc) 14.7 kB
"use strict"; import * as React from 'react'; import PasteTextInputNativeComponent, { Commands } from './PasteTextInputNativeComponent'; import { Platform, StyleSheet } from 'react-native'; import TextAncestor from 'react-native/Libraries/Text/TextAncestor'; import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; import usePressability from 'react-native/Libraries/Pressability/usePressability'; import flattenStyle from 'react-native/Libraries/StyleSheet/flattenStyle'; import nullthrows from 'nullthrows'; import { jsx as _jsx } from "react/jsx-runtime"; const emptyFunctionThatReturnsTrue = () => true; function useMergeRefs(...refs) { return React.useCallback(current => { for (const ref of refs) { if (ref != null) { if (typeof ref === 'function') { ref(current); } else { ref.current = current; } } } }, [...refs] // eslint-disable-line react-hooks/exhaustive-deps ); } function InternalTextInput(props) { const { 'aria-busy': ariaBusy, 'aria-checked': ariaChecked, 'aria-disabled': ariaDisabled, 'aria-expanded': ariaExpanded, 'aria-selected': ariaSelected, accessibilityState, id, tabIndex, 'selection': propsSelection, selectionColor, // selectionHandleColor, // cursorColor, ...otherProps } = props; const inputRef = React.useRef(null); const selection = React.useMemo(() => propsSelection == null ? null : { start: propsSelection.start, end: propsSelection.end ?? propsSelection.start }, [propsSelection]); const [mostRecentEventCount, setMostRecentEventCount] = React.useState(0); const [lastNativeText, setLastNativeText] = React.useState(props.value); const [lastNativeSelectionState, setLastNativeSelection] = React.useState({ selection: { start: -1, end: -1 }, mostRecentEventCount }); const lastNativeSelection = lastNativeSelectionState.selection; const text = typeof props.value === 'string' ? props.value : typeof props.defaultValue === 'string' ? props.defaultValue : ''; // This is necessary in case native updates the text and JS decides // that the update should be ignored and we should stick with the value // that we have in JS. React.useLayoutEffect(() => { const nativeUpdate = {}; if (lastNativeText !== props.value && typeof props.value === 'string') { nativeUpdate.text = props.value; setLastNativeText(props.value); } if (selection && lastNativeSelection && (lastNativeSelection.start !== selection.start || lastNativeSelection.end !== selection.end)) { nativeUpdate.selection = selection; setLastNativeSelection({ selection, mostRecentEventCount }); } if (Object.keys(nativeUpdate).length === 0) { return; } if (inputRef.current != null) { Commands.setTextAndSelection(inputRef.current, mostRecentEventCount, text, selection?.start ?? -1, selection?.end ?? -1); } }, [mostRecentEventCount, inputRef, props.value, props.defaultValue, lastNativeText, selection, lastNativeSelection, text]); React.useLayoutEffect(() => { const inputRefValue = inputRef.current; if (inputRefValue != null) { TextInputState.registerInput(inputRefValue); } return () => { if (inputRefValue != null) { TextInputState.unregisterInput(inputRefValue); } if (TextInputState.currentlyFocusedInput() === inputRefValue) { nullthrows(inputRefValue).blur(); } }; }, [inputRef]); const setLocalRef = React.useCallback(instance => { inputRef.current = instance; if (instance != null) { // $FlowFixMe[incompatible-use] - See the explanation above. Object.assign(instance, { clear() { if (inputRef.current != null) { Commands.setTextAndSelection(inputRef.current, mostRecentEventCount, '', 0, 0); } }, // TODO: Fix this returning true on null === null, when no input is focused isFocused() { return TextInputState.currentlyFocusedInput() === inputRef.current; }, getNativeRef() { return inputRef.current; }, setSelection(start, end) { if (inputRef.current != null) { Commands.setTextAndSelection(inputRef.current, mostRecentEventCount, null, start, end); } } }); } }, [mostRecentEventCount]); const ref = useMergeRefs(setLocalRef, props.forwardedRef); const _onChange = event => { const currentText = event.nativeEvent.text; props.onChange && props.onChange(event); props.onChangeText && props.onChangeText(currentText); if (inputRef.current == null) { // calling `props.onChange` or `props.onChangeText` // may clean up the input itself. Exits here. return; } setLastNativeText(currentText); // This must happen last, after we call setLastNativeText. // Different ordering can cause bugs when editing AndroidTextInputs // with multiple Fragments. // We must update this so that controlled input updates work. setMostRecentEventCount(event.nativeEvent.eventCount); }; const _onBlur = event => { TextInputState.blurInput(inputRef.current); if (props.onBlur) { props.onBlur(event); } }; const _onFocus = event => { TextInputState.focusInput(inputRef.current); if (props.onFocus) { props.onFocus(event); } }; const _onPaste = event => { if (props.onPaste) { const { data, error } = event.nativeEvent; props.onPaste(error?.message, data); } }; const _onScroll = event => { props.onScroll && props.onScroll(event); }; const _onSelectionChange = event => { props.onSelectionChange && props.onSelectionChange(event); if (inputRef.current == null) { // calling `props.onSelectionChange` // may clean up the input itself. Exits here. return; } setLastNativeSelection({ selection: event.nativeEvent.selection, mostRecentEventCount }); }; const multiline = props.multiline ?? false; let submitBehavior; if (props.submitBehavior != null) { // `submitBehavior` is set explicitly if (!multiline && props.submitBehavior === 'newline') { // For single line text inputs, `'newline'` is not a valid option submitBehavior = 'blurAndSubmit'; } else { submitBehavior = props.submitBehavior; } } else if (multiline) { if (props.blurOnSubmit === true) { submitBehavior = 'blurAndSubmit'; } else { submitBehavior = 'newline'; } } else { // Single line if (props.blurOnSubmit !== false) { submitBehavior = 'blurAndSubmit'; } else { submitBehavior = 'submit'; } } const accessible = props.accessible !== false; const focusable = props.focusable !== false; const { editable, hitSlop, onPress, onPressIn, onPressOut, rejectResponderTermination } = props; const config = React.useMemo(() => ({ hitSlop, onPress: event => { onPress?.(event); if (editable !== false) { if (inputRef.current != null) { inputRef.current.focus(); } } }, onPressIn: onPressIn, onPressOut: onPressOut, cancelable: Platform.OS === 'ios' ? !rejectResponderTermination : null }), [editable, hitSlop, onPress, onPressIn, onPressOut, rejectResponderTermination]); // Hide caret during test runs due to a flashing caret // makes screenshot tests flakey let caretHidden = props.caretHidden; if (Platform.isTesting) { caretHidden = true; } // TextInput handles onBlur and onFocus events // so omitting onBlur and onFocus pressability handlers here. // eslint-disable-next-line @typescript-eslint/no-unused-vars const { onBlur, onFocus, ...eventHandlers } = usePressability(config) || {}; let _accessibilityState; if (accessibilityState != null || ariaBusy != null || ariaChecked != null || ariaDisabled != null || ariaExpanded != null || ariaSelected != null) { _accessibilityState = { busy: ariaBusy ?? accessibilityState?.busy, checked: ariaChecked ?? accessibilityState?.checked, disabled: ariaDisabled ?? accessibilityState?.disabled, expanded: ariaExpanded ?? accessibilityState?.expanded, selected: ariaSelected ?? accessibilityState?.selected }; } // @ts-ignore const style = flattenStyle(props.style); const useMultilineDefaultStyle = props.multiline === true && (style == null || style.padding == null && style.paddingVertical == null && style.paddingTop == null); const textInput = /*#__PURE__*/_jsx(PasteTextInputNativeComponent, { ref: ref, ...otherProps, ...eventHandlers, accessibilityState: _accessibilityState, accessible: accessible, submitBehavior: submitBehavior, caretHidden: caretHidden, dataDetectorTypes: props.dataDetectorTypes, focusable: tabIndex !== undefined ? !tabIndex : focusable, mostRecentEventCount: mostRecentEventCount, nativeID: id ?? props.nativeID, onBlur: _onBlur, onChange: _onChange, onContentSizeChange: props.onContentSizeChange, onFocus: _onFocus, onPaste: _onPaste, onScroll: _onScroll, onSelectionChange: _onSelectionChange, onSelectionChangeShouldSetResponder: emptyFunctionThatReturnsTrue, selection: selection, selectionColor: selectionColor, style: StyleSheet.compose(useMultilineDefaultStyle ? styles.multilineDefault : null, style), text: text }); if (Platform.OS === 'ios') { return /*#__PURE__*/_jsx(TextAncestor.Provider, { value: true, children: textInput }); } return textInput; } const enterKeyHintToReturnTypeMap = { enter: 'default', done: 'done', go: 'go', next: 'next', previous: 'previous', search: 'search', send: 'send' }; const inputModeToKeyboardTypeMap = { none: 'default', text: 'default', decimal: 'decimal-pad', numeric: 'number-pad', tel: 'phone-pad', search: Platform.OS === 'ios' ? 'web-search' : 'default', email: 'email-address', url: 'url' }; // Map HTML autocomplete values to Android autoComplete values const autoCompleteWebToAutoCompleteAndroidMap = { 'address-line1': 'postal-address-region', 'address-line2': 'postal-address-locality', 'bday': 'birthdate-full', 'bday-day': 'birthdate-day', 'bday-month': 'birthdate-month', 'bday-year': 'birthdate-year', 'cc-csc': 'cc-csc', 'cc-exp': 'cc-exp', 'cc-exp-month': 'cc-exp-month', 'cc-exp-year': 'cc-exp-year', 'cc-number': 'cc-number', 'country': 'postal-address-country', 'current-password': 'password', 'email': 'email', 'honorific-prefix': 'name-prefix', 'honorific-suffix': 'name-suffix', 'name': 'name', 'additional-name': 'name-middle', 'family-name': 'name-family', 'given-name': 'name-given', 'new-password': 'password-new', 'off': 'off', 'one-time-code': 'sms-otp', 'postal-code': 'postal-code', 'sex': 'gender', 'street-address': 'street-address', 'tel': 'tel', 'tel-country-code': 'tel-country-code', 'tel-national': 'tel-national', 'username': 'username' }; // Map HTML autocomplete values to iOS textContentType values const autoCompleteWebToTextContentTypeMap = { 'address-line1': 'streetAddressLine1', 'address-line2': 'streetAddressLine2', 'bday': 'birthdate', 'bday-day': 'birthdateDay', 'bday-month': 'birthdateMonth', 'bday-year': 'birthdateYear', 'cc-csc': 'creditCardSecurityCode', 'cc-exp-month': 'creditCardExpirationMonth', 'cc-exp-year': 'creditCardExpirationYear', 'cc-exp': 'creditCardExpiration', 'cc-given-name': 'creditCardGivenName', 'cc-additional-name': 'creditCardMiddleName', 'cc-family-name': 'creditCardFamilyName', 'cc-name': 'creditCardName', 'cc-number': 'creditCardNumber', 'cc-type': 'creditCardType', 'current-password': 'password', 'country': 'countryName', 'email': 'emailAddress', 'name': 'name', 'additional-name': 'middleName', 'family-name': 'familyName', 'given-name': 'givenName', 'nickname': 'nickname', 'honorific-prefix': 'namePrefix', 'honorific-suffix': 'nameSuffix', 'new-password': 'newPassword', 'off': 'none', 'one-time-code': 'oneTimeCode', 'organization': 'organizationName', 'organization-title': 'jobTitle', 'postal-code': 'postalCode', 'street-address': 'fullStreetAddress', 'tel': 'telephoneNumber', 'url': 'URL', 'username': 'username' }; const verticalAlignToTextAlignVerticalMap = { auto: 'auto', top: 'top', bottom: 'bottom', middle: 'center' }; const ExportedForwardRef = /*#__PURE__*/React.forwardRef(function PasteTextInput({ allowFontScaling = true, rejectResponderTermination = true, underlineColorAndroid = 'transparent', autoComplete, textContentType, readOnly, editable, enterKeyHint, returnKeyType, inputMode, showSoftInputOnFocus, keyboardType, ...restProps }, forwardedRef) { let style = flattenStyle(restProps.style); if (style?.verticalAlign != null) { style.textAlignVertical = verticalAlignToTextAlignVerticalMap[style.verticalAlign]; delete style.verticalAlign; } return /*#__PURE__*/_jsx(InternalTextInput, { allowFontScaling: allowFontScaling, rejectResponderTermination: rejectResponderTermination, underlineColorAndroid: underlineColorAndroid, editable: readOnly !== undefined ? !readOnly : editable, returnKeyType: enterKeyHint ? enterKeyHintToReturnTypeMap[enterKeyHint] : returnKeyType, keyboardType: inputMode ? inputModeToKeyboardTypeMap[inputMode] : keyboardType, showSoftInputOnFocus: inputMode == null ? showSoftInputOnFocus : inputMode !== 'none', autoComplete: Platform.OS === 'android' ? // @ts-ignore autoCompleteWebToAutoCompleteAndroidMap[autoComplete] ?? autoComplete : undefined, textContentType: textContentType != null ? textContentType : Platform.OS === 'ios' && autoComplete && autoComplete in autoCompleteWebToTextContentTypeMap ? // @ts-ignore autoCompleteWebToTextContentTypeMap[autoComplete] : textContentType, ...restProps, forwardedRef: forwardedRef, style: style }); }); ExportedForwardRef.displayName = 'PasteTextInput'; const styles = StyleSheet.create({ multilineDefault: { // This default top inset makes RCTMultilineTextInputView seem as close as possible // to single-line RCTSinglelineTextInputView defaults, using the system defaults // of font size 17 and a height of 31 points. paddingTop: 5 } }); export default ExportedForwardRef; //# sourceMappingURL=PasteTextInput.js.map