UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

333 lines (330 loc) 10.5 kB
"use strict"; import * as React from 'react'; import { Animated, TextInput as NativeTextInput } from 'react-native'; import TextFieldAffix from "./TextField/Adornment/TextFieldAffix.js"; import TextFieldIcon from "./TextField/Adornment/TextFieldIcon.js"; import TextFieldFlat from "./TextField/TextFieldFlat.js"; import TextFieldOutlined from "./TextField/TextFieldOutlined.js"; import { useInternalTheme } from "./theming.js"; import { forwardRef } from "./utils/forwardRef.js"; import { roundLayoutSize } from "./utils/roundLayoutSize.js"; import { jsx as _jsx } from "react/jsx-runtime"; const BLUR_ANIMATION_DURATION = 180; const FOCUS_ANIMATION_DURATION = 150; const DefaultRenderer = props => /*#__PURE__*/_jsx(NativeTextInput, { ...props }); /** * A component to allow users to input text. * * ## Usage * ```js * import * as React from 'react'; * import { TextField } from './TextField'; * * const MyComponent = () => { * const [text, setText] = React.useState(""); * * return ( * <TextField * label="Email" * value={text} * onChangeText={text => setText(text)} * /> * ); * }; * * export default MyComponent; * ``` * * @extends TextInput props https://reactnative.dev/docs/textinput#props */ const TextField = forwardRef(({ mode = 'flat', dense = false, disabled = false, error: errorProp = false, multiline = false, editable = true, contentStyle, render = DefaultRenderer, theme: themeOverrides, ...rest }, ref) => { const theme = useInternalTheme(themeOverrides); const isControlled = rest.value !== undefined; const validInputValue = isControlled ? rest.value : rest.defaultValue; const { current: labeled } = React.useRef(new Animated.Value(validInputValue ? 0 : 1)); const { current: error } = React.useRef(new Animated.Value(errorProp ? 1 : 0)); const [focused, setFocused] = React.useState(false); const [displayPlaceholder, setDisplayPlaceholder] = React.useState(false); const [uncontrolledValue, setUncontrolledValue] = React.useState(validInputValue); // Use value from props instead of local state when input is controlled const value = isControlled ? rest.value : uncontrolledValue; const [labelTextLayout, setLabelTextLayout] = React.useState({ width: 33 }); const [inputContainerLayout, setInputContainerLayout] = React.useState({ width: 65 }); const [labelLayout, setLabelLayout] = React.useState({ measured: false, width: 0, height: 0 }); const [leftLayout, setLeftLayout] = React.useState({ width: null, height: null }); const [rightLayout, setRightLayout] = React.useState({ width: null, height: null }); const timer = React.useRef(undefined); const root = React.useRef(null); const { scale } = theme.animation; React.useImperativeHandle(ref, () => ({ focus: () => root.current?.focus(), clear: () => root.current?.clear(), setNativeProps: args => root.current?.setNativeProps(args), isFocused: () => root.current?.isFocused() || false, blur: () => root.current?.blur(), forceFocus: () => root.current?.focus(), setSelection: (start, end) => root.current?.setSelection(start, end) })); React.useEffect(() => { // When the input has an error, we wiggle the label and apply error styles if (errorProp) { // show error Animated.timing(error, { toValue: 1, duration: FOCUS_ANIMATION_DURATION * scale, // To prevent this - https://github.com/callstack/react-native-paper/issues/941 useNativeDriver: true }).start(); } else { // hide error { Animated.timing(error, { toValue: 0, duration: BLUR_ANIMATION_DURATION * scale, // To prevent this - https://github.com/callstack/react-native-paper/issues/941 useNativeDriver: true }).start(); } } }, [errorProp, scale, error]); React.useEffect(() => { // Show placeholder text only if the input is focused, or there's no label // We don't show placeholder if there's a label because the label acts as placeholder // When focused, the label moves up, so we can show a placeholder if (focused || !rest.label) { // If the user wants to use the contextMenu, when changing the placeholder, the contextMenu is closed // This is a workaround to mitigate this behavior in scenarios where the placeholder is not specified. if (rest.placeholder) { // Display placeholder in a delay to offset the label animation // If we show it immediately, they'll overlap and look ugly timer.current = setTimeout(() => setDisplayPlaceholder(true), 50); } } else { // hidePlaceholder setDisplayPlaceholder(false); } return () => { if (timer.current) { clearTimeout(timer.current); } }; }, [focused, rest.label, rest.placeholder]); React.useEffect(() => { labeled.stopAnimation(); // The label should be minimized if the text input is focused, or has text // In minimized mode, the label moves up and becomes small // workaround for animated regression for react native > 0.61 // https://github.com/callstack/react-native-paper/pull/1440 if (value || focused) { // minimize label Animated.timing(labeled, { toValue: 0, duration: BLUR_ANIMATION_DURATION * scale, // To prevent this - https://github.com/callstack/react-native-paper/issues/941 useNativeDriver: true }).start(); } else { // restore label Animated.timing(labeled, { toValue: 1, duration: FOCUS_ANIMATION_DURATION * scale, // To prevent this - https://github.com/callstack/react-native-paper/issues/941 useNativeDriver: true }).start(); } }, [focused, value, labeled, scale]); const onLeftAffixLayoutChange = React.useCallback(event => { const height = roundLayoutSize(event.nativeEvent.layout.height); const width = roundLayoutSize(event.nativeEvent.layout.width); if (width !== leftLayout.width || height !== leftLayout.height) { setLeftLayout({ width, height }); } }, [leftLayout.height, leftLayout.width]); const onRightAffixLayoutChange = React.useCallback(event => { const width = roundLayoutSize(event.nativeEvent.layout.width); const height = roundLayoutSize(event.nativeEvent.layout.height); if (width !== rightLayout.width || height !== rightLayout.height) { setRightLayout({ width, height }); } }, [rightLayout.height, rightLayout.width]); const handleFocus = args => { if (disabled || !editable) { return; } setFocused(true); rest.onFocus?.(args); }; const handleBlur = args => { if (!editable) { return; } setFocused(false); rest.onBlur?.(args); }; const handleChangeText = value => { if (!editable || disabled) { return; } if (!isControlled) { // Keep track of value in local state when input is not controlled setUncontrolledValue(value); } rest.onChangeText?.(value); }; const handleLayoutAnimatedText = React.useCallback(e => { const width = roundLayoutSize(e.nativeEvent.layout.width); const height = roundLayoutSize(e.nativeEvent.layout.height); if (width !== labelLayout.width || height !== labelLayout.height) { setLabelLayout({ width, height, measured: true }); } }, [labelLayout.height, labelLayout.width]); const handleLabelTextLayout = React.useCallback(({ nativeEvent }) => { setLabelTextLayout({ width: nativeEvent.lines.reduce((acc, line) => acc + Math.ceil(line.width), 0) }); }, []); const handleInputContainerLayout = React.useCallback(({ nativeEvent: { layout } }) => { setInputContainerLayout({ width: layout.width }); }, []); const forceFocus = React.useCallback(() => root.current?.focus(), []); const { maxFontSizeMultiplier = 1.5 } = rest; const scaledLabel = !!(value || focused); if (mode === 'outlined') { return /*#__PURE__*/_jsx(TextFieldOutlined, { dense: dense, disabled: disabled, error: errorProp, multiline: multiline, editable: editable, render: render, ...rest, theme: theme, value: value, parentState: { labeled, error, focused, displayPlaceholder, value, labelTextLayout, labelLayout, leftLayout, rightLayout, inputContainerLayout }, innerRef: ref => { root.current = ref; }, onFocus: handleFocus, forceFocus: forceFocus, onBlur: handleBlur, onChangeText: handleChangeText, onLayoutAnimatedText: handleLayoutAnimatedText, onInputLayout: handleInputContainerLayout, onLabelTextLayout: handleLabelTextLayout, onLeftAffixLayoutChange: onLeftAffixLayoutChange, onRightAffixLayoutChange: onRightAffixLayoutChange, maxFontSizeMultiplier: maxFontSizeMultiplier, contentStyle: contentStyle, scaledLabel: scaledLabel }); } return /*#__PURE__*/_jsx(TextFieldFlat, { dense: dense, disabled: disabled, error: errorProp, multiline: multiline, editable: editable, render: render, ...rest, theme: theme, value: value, parentState: { labeled, error, focused, displayPlaceholder, value, labelTextLayout, labelLayout, leftLayout, rightLayout, inputContainerLayout }, innerRef: ref => { root.current = ref; }, onFocus: handleFocus, forceFocus: forceFocus, onBlur: handleBlur, onInputLayout: handleInputContainerLayout, onChangeText: handleChangeText, onLayoutAnimatedText: handleLayoutAnimatedText, onLabelTextLayout: handleLabelTextLayout, onLeftAffixLayoutChange: onLeftAffixLayoutChange, onRightAffixLayoutChange: onRightAffixLayoutChange, maxFontSizeMultiplier: maxFontSizeMultiplier, contentStyle: contentStyle, scaledLabel: scaledLabel }); }); // @component ./TextField/Adornment/TextFieldIcon.tsx TextField.Icon = TextFieldIcon; // @component ./TextField/Adornment/TextFieldAffix.tsx // @ts-ignore Types of property 'theme' are incompatible. TextField.Affix = TextFieldAffix; export default TextField; //# sourceMappingURL=TextField.js.map