@kietpt2003/react-native-core-ui
Version:
React Native Core UI components by KietPT
401 lines (400 loc) • 17.1 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import React from 'react';
import { View, TextInput, StyleSheet, Animated, Easing, } from 'react-native';
import { colors, fontSize } from '../themes';
import { WEB } from '../utils';
import Text from '../Texts/Text';
const DEFAULT_ROWS = 3;
const DEFAULT_MARGIN_TOP = 6;
const DEFAULT_PADDING = 12;
const DISABLED_COLOR = colors.disabled;
const FOCUSED_COLOR = colors.primary;
const ERROR_COLOR = colors.error;
const DEFAULT_COLOR = colors.gray_D1D2D4;
const MIN_HEIGHT = 48;
const TextField = (_a) => {
var { label, labelMinSize = fontSize._12, labelSize = fontSize._14, applyTopLabel = true, labelBackground, value, defaultValue, onChangeText, error, errorColor, focusedColor, helperText, helperTextColor, helperTextStyle, renderStartIcon, renderEndIcon, multiline = false, rows = DEFAULT_ROWS, maxRows, editable = true, containerStyle, inputContainerStyle, style, onFocus, onBlur } = _a, rest = __rest(_a, ["label", "labelMinSize", "labelSize", "applyTopLabel", "labelBackground", "value", "defaultValue", "onChangeText", "error", "errorColor", "focusedColor", "helperText", "helperTextColor", "helperTextStyle", "renderStartIcon", "renderEndIcon", "multiline", "rows", "maxRows", "editable", "containerStyle", "inputContainerStyle", "style", "onFocus", "onBlur"]);
const [internalValue, setInternalValue] = React.useState(defaultValue !== null && defaultValue !== void 0 ? defaultValue : '');
const [focused, setFocused] = React.useState(false);
const [containerWidth, setContainerWidth] = React.useState(0);
const [containerHeight, setContainerHeight] = React.useState(0);
const [labelHeight, setLabelHeight] = React.useState(0);
const [helperHeight, setHelperHeight] = React.useState(0);
const [contentHeight, setContentHeight] = React.useState(null);
const [startIconWidth, setStartIconWidth] = React.useState(0);
const [endIconWidth, setEndIconWidth] = React.useState(0);
const [webInputHeight, setWebInputHeight] = React.useState();
const inputRef = React.useRef(null); //Handling dragging on Web
const isApplyStartIcon = typeof renderStartIcon === 'function';
const isApplyEndIcon = typeof renderEndIcon === 'function';
const labelMaxWidth = containerWidth -
(isApplyStartIcon ? startIconWidth : 0) -
(isApplyEndIcon ? endIconWidth : 0) -
DEFAULT_PADDING * 2;
const isControlled = value !== undefined;
const textValue = isControlled ? value : internalValue;
const hasError = Boolean(error);
const errorMessage = typeof error === 'string' ? error : undefined;
const floatAnim = React.useRef(new Animated.Value(textValue ? 1 : 0)).current;
const handleChange = React.useCallback((text) => {
if (!isControlled) {
setInternalValue(text);
}
onChangeText === null || onChangeText === void 0 ? void 0 : onChangeText(text);
}, [isControlled, onChangeText]);
const handleHideTopLabel = () => {
if (applyTopLabel) {
return undefined;
}
return floatAnim.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
});
};
const resolvePaddingSide = (rawStyle, isTop) => {
const style = StyleSheet.flatten(rawStyle);
const side = isTop ? 'paddingTop' : 'paddingBottom';
if (typeof (style === null || style === void 0 ? void 0 : style.paddingVertical) === 'number') {
return style.paddingVertical;
}
if (typeof (style === null || style === void 0 ? void 0 : style.padding) === 'number') {
return style.padding;
}
if (typeof (style === null || style === void 0 ? void 0 : style[side]) === 'number') {
return style[side];
}
return 0;
};
const resolvePadding = (userStyle, fallbackStyle, isTop) => {
const userPadding = resolvePaddingSide(userStyle, isTop);
if (userPadding !== 0)
return userPadding;
return resolvePaddingSide(fallbackStyle, isTop);
};
const calculatePadding = (isTop) => {
const containerPadding = resolvePadding(containerStyle, styles.container, isTop);
const inputPadding = resolvePadding(inputContainerStyle, styles.inputContainer, isTop);
const textInputPadding = resolvePadding(style, styles.input, isTop);
return containerPadding + inputPadding + textInputPadding;
};
const getInsetY = (alignVertical) => {
switch (alignVertical) {
case 'center':
return containerHeight / 2 - labelHeight;
case 'bottom':
return containerHeight - helperHeight - DEFAULT_MARGIN_TOP - labelHeight - calculatePadding(false);
case 'top':
default:
return calculatePadding(true);
}
};
const resolveTextAlignVertical = () => {
var _a;
const textInputStyle = StyleSheet.flatten(style);
if (WEB) { //Web only support textAlignVertical top
return 'top';
}
return (_a = textInputStyle === null || textInputStyle === void 0 ? void 0 : textInputStyle.textAlignVertical) !== null && _a !== void 0 ? _a : 'top';
};
const textAlignVertical = resolveTextAlignVertical();
const multilineStyle = React.useMemo(() => {
if (!multiline || !contentHeight) {
return null;
}
const padding = resolvePadding(style, styles.input, true) +
resolvePadding(style, styles.input, false);
return {
minHeight: rows ? rows * (contentHeight - (WEB ? padding * 1.9 : padding / 1.3)) : undefined,
maxHeight: maxRows ? maxRows * (contentHeight - padding / 1.1) : undefined, // maxHeight not work for auto add new line with maxRows on Web. Use height instead.
};
}, [multiline, rows, maxRows, contentHeight]);
const multilineWebStyle = React.useMemo(() => {
if (!multiline || !WEB || !contentHeight || !webInputHeight) {
return null;
}
const padding = resolvePadding(style, styles.input, true) +
resolvePadding(style, styles.input, false);
const minHeight = rows ? rows * (contentHeight - padding * 1.9) : 0;
const maxHeight = maxRows ? maxRows * (contentHeight - padding * 1.9) : 0;
if (webInputHeight > maxHeight) {
return {
height: maxHeight
};
}
if (webInputHeight > minHeight && webInputHeight <= maxHeight) {
return {
height: webInputHeight
};
}
return null;
}, [multiline, rows, maxRows, contentHeight, webInputHeight]);
const labelInsetY = React.useMemo(() => getInsetY(textAlignVertical), [
textAlignVertical,
style,
inputContainerStyle,
containerStyle,
containerHeight,
helperHeight,
labelHeight
]);
const renderBackgroundLabel = () => {
if (labelBackground) {
return labelBackground;
}
if (!editable) {
return DISABLED_COLOR;
}
return colors.white;
};
const renderLabelColor = () => {
if (hasError) {
if (errorColor) {
return errorColor;
}
return ERROR_COLOR;
}
if (focused) {
if (focusedColor) {
return focusedColor;
}
return FOCUSED_COLOR;
}
return DEFAULT_COLOR;
};
const renderInputBorderColor = () => {
if (hasError) {
if (errorColor) {
return errorColor;
}
return ERROR_COLOR;
}
if (focused) {
if (focusedColor) {
return focusedColor;
}
return FOCUSED_COLOR;
}
return DEFAULT_COLOR;
};
const renderHelperTextColor = () => {
if (helperTextColor) {
return helperTextColor;
}
if (hasError) {
if (errorColor) {
return errorColor;
}
return ERROR_COLOR;
}
if (!editable) {
return DEFAULT_COLOR;
}
return colors.black;
};
const labelViewStyle = {
position: 'absolute',
left: isApplyStartIcon ? startIconWidth + DEFAULT_PADDING : DEFAULT_PADDING,
top: applyTopLabel ? floatAnim.interpolate({
inputRange: [0, 1],
outputRange: [labelInsetY, -labelMinSize / 1.5],
}) : labelInsetY,
maxWidth: labelMaxWidth,
overflow: 'hidden',
backgroundColor: renderBackgroundLabel(),
paddingHorizontal: DEFAULT_PADDING,
opacity: handleHideTopLabel(),
borderRadius: 5
};
const labelStyle = {
fontSize: applyTopLabel ? floatAnim.interpolate({
inputRange: [0, 1],
outputRange: [labelSize, labelMinSize],
}) : labelSize,
color: renderLabelColor(),
};
React.useEffect(() => {
Animated.timing(floatAnim, {
toValue: focused || textValue ? 1 : 0,
duration: 180,
easing: Easing.out(Easing.ease),
useNativeDriver: false,
}).start();
}, [focused, textValue, floatAnim]);
React.useEffect(() => {
if (!WEB || !multiline) {
return;
}
const el = inputRef.current;
if (!el) {
return;
}
let isDragging = false;
let startY = 0;
let startScrollTop = 0;
const onMouseDown = (e) => {
isDragging = true;
startY = e.clientY;
startScrollTop = el.scrollTop;
};
const onMouseMove = (e) => {
if (!isDragging) {
return;
}
el.scrollTop = startScrollTop - (e.clientY - startY);
};
const onMouseUp = () => {
isDragging = false;
};
el.addEventListener('mousedown', onMouseDown);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
return () => {
el.removeEventListener('mousedown', onMouseDown);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
}, [multiline]);
const multilineStyleSignature = React.useMemo(() => {
if (!style)
return '';
const s = Array.isArray(style)
? Object.assign({}, ...style)
: style;
return [
s.minHeight,
s.maxHeight,
s.paddingTop,
s.paddingBottom,
s.lineHeight,
s.fontSize,
].join('|');
}, [style]);
React.useEffect(() => {
let timeoutID;
if (contentHeight) {
handleChange('');
timeoutID = setTimeout(() => setContentHeight(null), 1000); //Wait until the text change for onContentSizeChange get the init height before reset multilineStyle
}
return () => {
if (timeoutID) {
clearTimeout(timeoutID);
}
};
}, [multilineStyleSignature]);
return (_jsxs(View, { onLayout: e => {
var _a, _b, _c, _d;
setContainerWidth((_b = (_a = e === null || e === void 0 ? void 0 : e.nativeEvent) === null || _a === void 0 ? void 0 : _a.layout) === null || _b === void 0 ? void 0 : _b.width);
setContainerHeight((_d = (_c = e === null || e === void 0 ? void 0 : e.nativeEvent) === null || _c === void 0 ? void 0 : _c.layout) === null || _d === void 0 ? void 0 : _d.height);
}, style: [styles.container, containerStyle], children: [_jsxs(View, { style: [
styles.inputContainer,
{ borderColor: renderInputBorderColor() },
!editable && styles.disabled,
inputContainerStyle,
], children: [label && (_jsx(Animated.View, { pointerEvents: 'none', onLayout: e => {
var _a, _b;
setLabelHeight((_b = (_a = e === null || e === void 0 ? void 0 : e.nativeEvent) === null || _a === void 0 ? void 0 : _a.layout) === null || _b === void 0 ? void 0 : _b.height);
}, style: labelViewStyle, children: _jsx(Animated.Text, { numberOfLines: 1, ellipsizeMode: "tail", style: labelStyle, children: label }) })), isApplyStartIcon && (_jsx(View, { style: styles.iconStart, onLayout: e => {
setStartIconWidth(e.nativeEvent.layout.width);
}, children: renderStartIcon({
focused: focused,
hasError: hasError,
editable: editable,
}) })), _jsx(TextInput, Object.assign({}, rest, { ref: inputRef, style: [
styles.input,
multiline && styles.multiline,
multilineStyle,
multilineWebStyle,
WEB && !editable && styles.webDisabled,
!editable && styles.inputDisabled,
style,
], value: textValue, onChangeText: handleChange, editable: editable, multiline: multiline, onFocus: e => {
if (!editable) {
return;
}
setFocused(true);
onFocus && onFocus(e);
}, onBlur: e => {
setFocused(false);
onBlur && onBlur(e);
}, onContentSizeChange: e => {
if (multiline && WEB) {
setWebInputHeight(e.nativeEvent.contentSize.height);
}
if (!multiline || contentHeight) {
return;
}
setContentHeight(e.nativeEvent.contentSize.height);
} })), isApplyEndIcon && (_jsx(View, { style: styles.iconEnd, onLayout: e => {
var _a, _b;
setEndIconWidth((_b = (_a = e.nativeEvent) === null || _a === void 0 ? void 0 : _a.layout) === null || _b === void 0 ? void 0 : _b.width);
}, children: renderEndIcon({
focused: focused,
hasError: hasError,
editable: editable,
}) }))] }), (errorMessage || helperText) && (_jsx(Text, { color: renderHelperTextColor(), onLayout: e => {
var _a, _b;
setHelperHeight((_b = (_a = e === null || e === void 0 ? void 0 : e.nativeEvent) === null || _a === void 0 ? void 0 : _a.layout) === null || _b === void 0 ? void 0 : _b.height);
}, style: [styles.helperText, helperTextStyle], children: errorMessage !== null && errorMessage !== void 0 ? errorMessage : helperText }))] }));
};
const styles = StyleSheet.create({
container: {
width: '100%',
},
inputContainer: {
minHeight: MIN_HEIGHT,
borderWidth: 1,
borderColor: DEFAULT_COLOR,
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: DEFAULT_PADDING,
backgroundColor: colors.white,
},
focusedBorder: {
borderColor: FOCUSED_COLOR,
},
input: Object.assign({ flex: 1, fontSize: fontSize._14, color: colors.black, paddingVertical: DEFAULT_PADDING }, (WEB
? {
outlineStyle: 'none',
overflow: 'hidden',
}
: {})),
multiline: {
textAlignVertical: 'top',
alignItems: 'flex-start'
},
iconStart: {
marginRight: DEFAULT_PADDING,
},
iconEnd: {
marginLeft: DEFAULT_PADDING,
},
helperText: {
marginTop: DEFAULT_MARGIN_TOP,
fontSize: WEB ? fontSize._10 : fontSize._12,
paddingHorizontal: DEFAULT_PADDING
},
disabled: {
backgroundColor: DISABLED_COLOR,
},
webDisabled: Object.assign({}, (WEB
? {
cursor: 'not-allowed',
}
: {})),
inputDisabled: {
color: DEFAULT_COLOR
}
});
export default TextField;