import * as React from 'react';
import {
View,
TextInput as NativeTextInput,
StyleSheet,
I18nManager,
Platform,
TextStyle,
ColorValue,
StyleProp,
ViewStyle,
} from 'react-native';
import { AdornmentType, AdornmentSide } from './Adornment/enums';
import TextInputAdornment, {
getAdornmentConfig,
getAdornmentStyleAdjustmentForNativeInput,
TextInputAdornmentProps,
} from './Adornment/TextInputAdornment';
import {
MAXIMIZED_LABEL_FONT_SIZE,
MINIMIZED_LABEL_FONT_SIZE,
LABEL_WIGGLE_X_OFFSET,
ADORNMENT_SIZE,
OUTLINE_MINIMIZED_LABEL_Y_OFFSET,
LABEL_PADDING_TOP,
MIN_DENSE_HEIGHT_OUTLINED,
LABEL_PADDING_TOP_DENSE,
} from './constants';
import {
calculateLabelTopPosition,
calculateInputHeight,
calculatePadding,
adjustPaddingOut,
Padding,
calculateOutlinedIconAndAffixTopPosition,
getOutlinedInputColors,
getConstants,
} from './helpers';
import InputLabel from './Label/InputLabel';
import LabelBackground from './Label/LabelBackground';
import type { RenderProps, ChildTextInputProps } from './types';
const TextInputOutlined = ({
disabled = false,
editable = true,
label,
error = false,
selectionColor,
underlineColor: _underlineColor,
outlineColor: customOutlineColor,
activeOutlineColor,
outlineStyle,
textColor,
dense,
style,
theme,
render = (props: RenderProps) => ,
multiline = false,
parentState,
innerRef,
onFocus,
forceFocus,
onBlur,
onChangeText,
onLayoutAnimatedText,
onLeftAffixLayoutChange,
onRightAffixLayoutChange,
left,
right,
placeholderTextColor,
testID = 'text-input-outlined',
contentStyle,
...rest
}: ChildTextInputProps) => {
const adornmentConfig = getAdornmentConfig({ left, right });
const { colors, isV3, roundness } = theme;
const font = isV3 ? theme.fonts.bodyLarge : theme.fonts.regular;
const hasActiveOutline = parentState.focused || error;
const { INPUT_PADDING_HORIZONTAL, MIN_HEIGHT, ADORNMENT_OFFSET } =
getConstants(isV3);
const {
fontSize: fontSizeStyle,
fontWeight,
lineHeight,
height,
backgroundColor = colors?.background,
textAlign,
...viewStyle
} = (StyleSheet.flatten(style) || {}) as TextStyle;
const fontSize = fontSizeStyle || MAXIMIZED_LABEL_FONT_SIZE;
const {
inputTextColor,
activeColor,
outlineColor,
placeholderColor,
errorColor,
} = getOutlinedInputColors({
activeOutlineColor,
customOutlineColor,
textColor,
disabled,
error,
theme,
});
const labelScale = MINIMIZED_LABEL_FONT_SIZE / fontSize;
const fontScale = MAXIMIZED_LABEL_FONT_SIZE / fontSize;
const labelWidth = parentState.labelLayout.width;
const labelHeight = parentState.labelLayout.height;
const labelHalfWidth = labelWidth / 2;
const labelHalfHeight = labelHeight / 2;
const baseLabelTranslateX =
(I18nManager.getConstants().isRTL ? 1 : -1) *
(labelHalfWidth -
(labelScale * labelWidth) / 2 -
(fontSize - MINIMIZED_LABEL_FONT_SIZE) * labelScale);
let labelTranslationXOffset = 0;
const isAdornmentLeftIcon = adornmentConfig.some(
({ side, type }) =>
side === AdornmentSide.Left && type === AdornmentType.Icon
);
if (isAdornmentLeftIcon) {
labelTranslationXOffset =
(I18nManager.getConstants().isRTL ? -1 : 1) *
(ADORNMENT_SIZE + ADORNMENT_OFFSET - (isV3 ? 0 : 8));
}
const minInputHeight =
(dense ? MIN_DENSE_HEIGHT_OUTLINED : MIN_HEIGHT) - LABEL_PADDING_TOP;
const inputHeight = calculateInputHeight(labelHeight, height, minInputHeight);
const topPosition = calculateLabelTopPosition(
labelHeight,
inputHeight,
LABEL_PADDING_TOP
);
if (height && typeof height !== 'number') {
// eslint-disable-next-line
console.warn('Currently we support only numbers in height prop');
}
const paddingSettings = {
height: height ? +height : null,
labelHalfHeight,
offset: LABEL_PADDING_TOP,
multiline: multiline ? multiline : null,
dense: dense ? dense : null,
topPosition,
fontSize,
lineHeight,
label,
scale: fontScale,
isAndroid: Platform.OS === 'android',
styles: StyleSheet.flatten(
dense ? styles.inputOutlinedDense : styles.inputOutlined
) as Padding,
};
const pad = calculatePadding(paddingSettings);
const paddingOut = adjustPaddingOut({ ...paddingSettings, pad });
const baseLabelTranslateY =
-labelHalfHeight - (topPosition + OUTLINE_MINIMIZED_LABEL_Y_OFFSET);
const placeholderOpacity = hasActiveOutline
? parentState.labeled
: parentState.labelLayout.measured
? 1
: 0;
const placeholderStyle = {
position: 'absolute',
left: 0,
paddingHorizontal: INPUT_PADDING_HORIZONTAL,
};
const labelProps = {
label,
onLayoutAnimatedText,
placeholderOpacity,
labelError: error,
placeholderStyle,
baseLabelTranslateY,
baseLabelTranslateX,
font,
fontSize,
lineHeight,
fontWeight,
labelScale,
wiggleOffsetX: LABEL_WIGGLE_X_OFFSET,
topPosition,
hasActiveOutline,
activeColor,
placeholderColor,
backgroundColor: backgroundColor as ColorValue,
errorColor,
labelTranslationXOffset,
roundness,
maxFontSizeMultiplier: rest.maxFontSizeMultiplier,
testID,
contentStyle,
opacity:
parentState.value || parentState.focused
? parentState.labelLayout.measured
? 1
: 0
: 1,
};
const minHeight = (height ||
(dense ? MIN_DENSE_HEIGHT_OUTLINED : MIN_HEIGHT)) as number;
const outlinedHeight =
inputHeight +
(!height ? (dense ? LABEL_PADDING_TOP_DENSE / 2 : LABEL_PADDING_TOP) : 0);
const { leftLayout, rightLayout } = parentState;
const leftAffixTopPosition = calculateOutlinedIconAndAffixTopPosition({
height: outlinedHeight,
affixHeight: leftLayout.height || 0,
labelYOffset: -OUTLINE_MINIMIZED_LABEL_Y_OFFSET,
});
const rightAffixTopPosition = calculateOutlinedIconAndAffixTopPosition({
height: outlinedHeight,
affixHeight: rightLayout.height || 0,
labelYOffset: -OUTLINE_MINIMIZED_LABEL_Y_OFFSET,
});
const iconTopPosition = calculateOutlinedIconAndAffixTopPosition({
height: outlinedHeight,
affixHeight: ADORNMENT_SIZE,
labelYOffset: -OUTLINE_MINIMIZED_LABEL_Y_OFFSET,
});
const rightAffixWidth = right
? rightLayout.width || ADORNMENT_SIZE
: ADORNMENT_SIZE;
const leftAffixWidth = left
? leftLayout.width || ADORNMENT_SIZE
: ADORNMENT_SIZE;
const adornmentStyleAdjustmentForNativeInput =
getAdornmentStyleAdjustmentForNativeInput({
adornmentConfig,
rightAffixWidth,
leftAffixWidth,
mode: 'outlined',
isV3,
});
const affixTopPosition = {
[AdornmentSide.Left]: leftAffixTopPosition,
[AdornmentSide.Right]: rightAffixTopPosition,
};
const onAffixChange = {
[AdornmentSide.Left]: onLeftAffixLayoutChange,
[AdornmentSide.Right]: onRightAffixLayoutChange,
};
let adornmentProps: TextInputAdornmentProps = {
adornmentConfig,
forceFocus,
topPosition: {
[AdornmentType.Icon]: iconTopPosition,
[AdornmentType.Affix]: affixTopPosition,
},
onAffixChange,
isTextInputFocused: parentState.focused,
maxFontSizeMultiplier: rest.maxFontSizeMultiplier,
disabled,
};
if (adornmentConfig.length) {
adornmentProps = {
...adornmentProps,
left,
right,
textStyle: { ...font, fontSize, lineHeight, fontWeight },
visible: parentState.labeled,
};
}
return (
{/*
Render the outline separately from the container
This is so that the label can overlap the outline
Otherwise the border will cut off the label on Android
*/}
{label ? (
) : null}
{render?.({
testID,
...rest,
ref: innerRef,
onChangeText,
placeholder: label ? parentState.placeholder : rest.placeholder,
placeholderTextColor: placeholderTextColor || placeholderColor,
editable: !disabled && editable,
selectionColor:
typeof selectionColor === 'undefined'
? activeColor
: selectionColor,
onFocus,
onBlur,
underlineColorAndroid: 'transparent',
multiline,
style: [
styles.input,
!multiline || (multiline && height)
? { height: inputHeight }
: {},
paddingOut,
{
...font,
fontSize,
lineHeight,
fontWeight,
color: inputTextColor,
textAlignVertical: multiline ? 'top' : 'center',
textAlign: textAlign
? textAlign
: I18nManager.getConstants().isRTL
? 'right'
: 'left',
paddingHorizontal: INPUT_PADDING_HORIZONTAL,
},
Platform.OS === 'web' && { outline: 'none' },
adornmentStyleAdjustmentForNativeInput,
contentStyle,
],
} as RenderProps)}
);
};
export default TextInputOutlined;
type OutlineProps = {
isV3: boolean;
activeColor: string;
backgroundColor: ColorValue;
hasActiveOutline?: boolean;
focused?: boolean;
outlineColor?: string;
roundness?: number;
style?: StyleProp;
};
const Outline = ({
isV3,
activeColor,
backgroundColor,
hasActiveOutline,
focused,
outlineColor,
roundness,
style,
}: OutlineProps) => (
);
const styles = StyleSheet.create({
outline: {
position: 'absolute',
left: 0,
right: 0,
top: 6,
bottom: 0,
},
labelContainer: {
paddingBottom: 0,
},
input: {
margin: 0,
zIndex: 1,
},
inputOutlined: {
paddingTop: 8,
paddingBottom: 8,
},
inputOutlinedDense: {
paddingTop: 4,
paddingBottom: 4,
},
});