diff --git a/example/src/Examples/TextInputExample.tsx b/example/src/Examples/TextInputExample.tsx index 04b3c01868..530c22f3ba 100644 --- a/example/src/Examples/TextInputExample.tsx +++ b/example/src/Examples/TextInputExample.tsx @@ -61,6 +61,8 @@ type AvoidingViewProps = { children: React.ReactNode; }; +type ExpandedId = string | number | undefined; + const TextInputAvoidingView = ({ children }: AvoidingViewProps) => { return Platform.OS === 'ios' ? ( { }); }; + const [expandedId, setExpandedId] = React.useState('flat'); + + const onAccordionPress = (id: string | number) => + setExpandedId(expandedId === id ? undefined : id); + return ( - - inputActionHandler('text', text)} - left={ - { - changeIconColor('flatLeftIcon'); - }} - /> - } - right={} - /> - inputActionHandler('customIconText', text)} - right={} - left={ - ( - { - changeIconColor('customIcon'); - }} - /> - )} - /> - } - /> - - inputActionHandler('largeText', largeText) - } - left={} - right={ - { - changeIconColor('flatRightIcon'); - }} - /> - } - /> - - inputActionHandler('flatTextPassword', flatTextPassword) - } - secureTextEntry={flatTextSecureEntry} - right={ - - dispatch({ - type: 'flatTextSecureEntry', - payload: !flatTextSecureEntry, - }) - } - forceTextInputFocus={false} - /> - } - /> - - - - inputActionHandler('outlinedText', outlinedText) - } - left={ - { - changeIconColor('outlineLeftIcon'); - }} - /> - } - right={} - /> - - inputActionHandler('outlinedLargeText', outlinedLargeText) - } - left={} - right={ - { - changeIconColor('outlineRightIcon'); - }} - /> - } - /> - - inputActionHandler('outlinedTextPassword', outlinedTextPassword) - } - secureTextEntry={outlineTextSecureEntry} - right={ - - dispatch({ - type: 'outlineTextSecureEntry', - payload: !outlineTextSecureEntry, - }) - } - /> - } - /> - - - - - { - changeIconColor('flatLeftIcon'); - }} - /> - } - right={} - /> - - - { - changeIconColor('flatLeftIcon'); - }} - /> - } - right={} - /> - - - - inputActionHandler('flatDenseText', flatDenseText) - } - left={} - right={ - - focused ? theme.colors?.primary : undefined - } - /> - } - /> - - inputActionHandler('flatDense', flatDense) - } - /> - - inputActionHandler('outlinedDenseText', outlinedDenseText) - } - left={} - /> - - inputActionHandler('outlinedDense', outlinedDense) - } - /> - - - - inputActionHandler('flatMultiline', flatMultiline) - } - /> - - inputActionHandler('flatTextArea', flatTextArea) - } - /> - + + inputActionHandler('text', text)} + left={ + { + changeIconColor('flatLeftIcon'); + }} + /> + } + maxLength={100} + right={} + /> + + inputActionHandler('customIconText', text) + } + maxLength={100} + right={} + left={ + ( + { + changeIconColor('customIcon'); + }} + /> + )} + /> + } + /> + + inputActionHandler('largeText', largeText) + } + left={} + right={ + { + changeIconColor('flatRightIcon'); + }} + /> + } + /> + + inputActionHandler('flatTextPassword', flatTextPassword) + } + secureTextEntry={flatTextSecureEntry} + right={ + + dispatch({ + type: 'flatTextSecureEntry', + payload: !flatTextSecureEntry, + }) + } + forceTextInputFocus={false} + /> + } /> - - - inputActionHandler('outlinedMultiline', outlinedMultiline) - } - /> - - inputActionHandler('outlinedTextArea', outlinedTextArea) - } - /> - + + + inputActionHandler('outlinedText', outlinedText) + } + left={ + { + changeIconColor('outlineLeftIcon'); + }} + /> + } + maxLength={100} + right={} + /> + + inputActionHandler('outlinedLargeText', outlinedLargeText) + } + left={} + right={ + { + changeIconColor('outlineRightIcon'); + }} + /> + } /> - - - - inputActionHandler('name', name)} + mode="outlined" + style={[styles.inputContainerStyle, styles.fontSize]} + label="Outlined large font" + placeholder="Type something" + value={outlinedTextPassword} + onChangeText={(outlinedTextPassword) => + inputActionHandler('outlinedTextPassword', outlinedTextPassword) + } + secureTextEntry={outlineTextSecureEntry} + right={ + + dispatch({ + type: 'outlineTextSecureEntry', + payload: !outlineTextSecureEntry, + }) + } + /> + } /> - - Error: Only letters are allowed - - - + + - inputActionHandler('maxLengthName', maxLengthName) - } - maxLength={MAX_LENGTH} + disabled + style={styles.inputContainerStyle} + label="Disabled flat input" /> - - - Error: Numbers and special characters are not allowed - - - {maxLengthName.length} / {MAX_LENGTH} - - - - - - * - {' '} - Label as component - - } - style={styles.noPaddingInput} - placeholder="Enter username, required" - value={nameRequired} - error={!nameRequired} - onChangeText={(nameRequired) => - inputActionHandler('nameRequired', nameRequired) + disabled + style={styles.inputContainerStyle} + label="Disabled flat input with value" + value="Disabled flat input value" + /> + { + changeIconColor('flatLeftIcon'); + }} + /> } + right={} + /> + + - - Error: Username is required - - - - - - inputActionHandler('flatUnderlineColors', flatUnderlineColors) - } - underlineColor={ - theme.isV3 ? MD3Colors.primary70 : MD2Colors.pink400 - } - activeUnderlineColor={ - theme.isV3 ? MD3Colors.tertiary50 : MD2Colors.amber900 - } - /> - - inputActionHandler('outlinedColors', outlinedColors) - } - outlineColor={theme.isV3 ? MD3Colors.primary70 : MD2Colors.pink400} - activeOutlineColor={ - theme.isV3 ? MD3Colors.tertiary50 : MD2Colors.amber900 - } - /> - - inputActionHandler('outlinedLongLabel', outlinedLongLabel) - } - /> - - - inputActionHandler('customStyleText', customStyleText) - } - contentStyle={styles.inputContentStyle} - /> - - - inputActionHandler('nameNoPadding', nameNoPadding) + style={styles.inputContainerStyle} + label="Flat input" + disabled + mode="outlined" + value="Disabled flat input with adornments" + left={ + { + changeIconColor('flatLeftIcon'); + }} + /> } + right={} /> - - Error: Only letters are allowed - - - - + + + + inputActionHandler('flatDenseText', flatDenseText) + } + left={} + right={ + + focused ? theme.colors?.primary : undefined + } + /> + } + /> + + inputActionHandler('flatDense', flatDense) + } + /> + + inputActionHandler('outlinedDenseText', outlinedDenseText) + } + left={} + /> + + inputActionHandler('outlinedDense', outlinedDense) + } + /> + + + inputActionHandler('flatMultiline', flatMultiline) + } /> - - + + inputActionHandler('flatTextArea', flatTextArea) + } + /> + + + + inputActionHandler('outlinedMultiline', outlinedMultiline) + } /> - - + inputActionHandler('outlinedTextArea', outlinedTextArea) + } + /> + + + + + + + inputActionHandler('name', name)} + /> + + Error: Only letters are allowed + + + + + inputActionHandler('maxLengthName', maxLengthName) + } + maxLength={MAX_LENGTH} + /> + + + Error: Numbers and special characters are not allowed + + + {maxLengthName.length} / {MAX_LENGTH} + + + + + + + * + {' '} + Label as component + + } + style={styles.noPaddingInput} + placeholder="Enter username, required" + value={nameRequired} + error={!nameRequired} + onChangeText={(nameRequired) => + inputActionHandler('nameRequired', nameRequired) + } + /> + + Error: Username is required + + + + + + inputActionHandler('flatUnderlineColors', flatUnderlineColors) + } + underlineColor={ + theme.isV3 ? MD3Colors.primary70 : MD2Colors.pink400 + } + activeUnderlineColor={ + theme.isV3 ? MD3Colors.tertiary50 : MD2Colors.amber900 + } /> - - + inputActionHandler('outlinedColors', outlinedColors) + } + outlineColor={ + theme.isV3 ? MD3Colors.primary70 : MD2Colors.pink400 + } + activeOutlineColor={ + theme.isV3 ? MD3Colors.tertiary50 : MD2Colors.amber900 + } /> - - + inputActionHandler('outlinedLongLabel', outlinedLongLabel) + } + /> + + + inputActionHandler('customStyleText', customStyleText) + } + contentStyle={styles.inputContentStyle} /> - - + + + + inputActionHandler('nameNoPadding', nameNoPadding) + } + /> + + Error: Only letters are allowed + + + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/src/components/TextInput/Addons/Outline.tsx b/src/components/TextInput/Addons/Outline.tsx new file mode 100644 index 0000000000..4529f5992e --- /dev/null +++ b/src/components/TextInput/Addons/Outline.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { + StyleSheet, + ColorValue, + StyleProp, + View, + ViewStyle, +} from 'react-native'; + +type OutlineProps = { + isV3: boolean; + activeColor: string; + backgroundColor: ColorValue; + hasActiveOutline?: boolean; + focused?: boolean; + outlineColor?: string; + roundness?: number; + style?: StyleProp; +}; + +export 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, + }, +}); diff --git a/src/components/TextInput/Addons/Underline.tsx b/src/components/TextInput/Addons/Underline.tsx new file mode 100644 index 0000000000..184cbe63b5 --- /dev/null +++ b/src/components/TextInput/Addons/Underline.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { Animated, StyleSheet, StyleProp, ViewStyle } from 'react-native'; + +import type { ThemeProp } from 'src/types'; + +import { useInternalTheme } from '../../../core/theming'; + +type UnderlineProps = { + parentState: { + focused: boolean; + }; + error?: boolean; + colors?: { + error?: string; + }; + activeColor: string; + underlineColorCustom?: string; + hasActiveOutline?: boolean; + style?: StyleProp; + theme?: ThemeProp; +}; + +export const Underline = ({ + parentState, + error, + colors, + activeColor, + underlineColorCustom, + hasActiveOutline, + style, + theme: themeOverrides, +}: UnderlineProps) => { + const { isV3 } = useInternalTheme(themeOverrides); + + let backgroundColor = parentState.focused + ? activeColor + : underlineColorCustom; + + if (error) backgroundColor = colors?.error; + + const activeScale = isV3 ? 2 : 1; + + return ( + + ); +}; + +const styles = StyleSheet.create({ + underline: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + height: 2, + zIndex: 1, + }, + md3Underline: { + height: 1, + }, +}); diff --git a/src/components/TextInput/TextInputFlat.tsx b/src/components/TextInput/TextInputFlat.tsx index 42d640e859..9ee30fac79 100644 --- a/src/components/TextInput/TextInputFlat.tsx +++ b/src/components/TextInput/TextInputFlat.tsx @@ -1,19 +1,14 @@ import * as React from 'react'; import { - Animated, I18nManager, Platform, - StyleProp, StyleSheet, TextInput as NativeTextInput, TextStyle, View, - ViewStyle, } from 'react-native'; -import type { ThemeProp } from 'src/types'; - -import { useInternalTheme } from '../../core/theming'; +import { Underline } from './Addons/Underline'; import { AdornmentSide, AdornmentType, InputMode } from './Adornment/enums'; import TextInputAdornment, { TextInputAdornmentProps, @@ -51,7 +46,7 @@ const TextInputFlat = ({ editable = true, label, error = false, - selectionColor, + selectionColor: customSelectionColor, cursorColor, underlineColor, underlineStyle, @@ -143,9 +138,11 @@ const TextInputFlat = ({ placeholderColor, errorColor, backgroundColor, + selectionColor, } = getFlatInputColors({ underlineColor, activeUnderlineColor, + customSelectionColor, textColor, disabled, error, @@ -372,29 +369,26 @@ const TextInputFlat = ({ /> ) : 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, + selectionColor, cursorColor: typeof cursorColor === 'undefined' ? activeColor : cursorColor, + placeholderTextColor: placeholderTextColor ?? placeholderColor, onFocus, onBlur, underlineColorAndroid: 'transparent', multiline, style: [ styles.input, - { paddingLeft, paddingRight }, !multiline || (multiline && height) ? { height: flatHeight } : {}, paddingFlat, { + paddingLeft, + paddingRight, ...font, fontSize, lineHeight, @@ -411,6 +405,7 @@ const TextInputFlat = ({ adornmentStyleAdjustmentForNativeInput, contentStyle, ], + testID, })} @@ -420,80 +415,11 @@ const TextInputFlat = ({ export default TextInputFlat; -type UnderlineProps = { - parentState: { - focused: boolean; - }; - error?: boolean; - colors?: { - error?: string; - }; - activeColor: string; - underlineColorCustom?: string; - hasActiveOutline?: boolean; - style?: StyleProp; - theme?: ThemeProp; -}; - -const Underline = ({ - parentState, - error, - colors, - activeColor, - underlineColorCustom, - hasActiveOutline, - style, - theme: themeOverrides, -}: UnderlineProps) => { - const { isV3 } = useInternalTheme(themeOverrides); - - let backgroundColor = parentState.focused - ? activeColor - : underlineColorCustom; - - if (error) backgroundColor = colors?.error; - - const activeScale = isV3 ? 2 : 1; - - return ( - - ); -}; - const styles = StyleSheet.create({ placeholder: { position: 'absolute', left: 0, }, - underline: { - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - height: 2, - zIndex: 1, - }, - md3Underline: { - height: 1, - }, labelContainer: { paddingTop: 0, paddingBottom: 0, diff --git a/src/components/TextInput/TextInputOutlined.tsx b/src/components/TextInput/TextInputOutlined.tsx index bee0a2dce0..80d8adcae7 100644 --- a/src/components/TextInput/TextInputOutlined.tsx +++ b/src/components/TextInput/TextInputOutlined.tsx @@ -7,10 +7,9 @@ import { Platform, TextStyle, ColorValue, - StyleProp, - ViewStyle, } from 'react-native'; +import { Outline } from './Addons/Outline'; import { AdornmentType, AdornmentSide } from './Adornment/enums'; import TextInputAdornment, { getAdornmentConfig, @@ -46,7 +45,7 @@ const TextInputOutlined = ({ editable = true, label, error = false, - selectionColor, + selectionColor: customSelectionColor, cursorColor, underlineColor: _underlineColor, outlineColor: customOutlineColor, @@ -100,9 +99,11 @@ const TextInputOutlined = ({ outlineColor, placeholderColor, errorColor, + selectionColor, } = getOutlinedInputColors({ activeOutlineColor, customOutlineColor, + customSelectionColor, textColor, disabled, error, @@ -338,19 +339,15 @@ const TextInputOutlined = ({ /> ) : 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, + selectionColor, cursorColor: typeof cursorColor === 'undefined' ? activeColor : cursorColor, + placeholderTextColor: placeholderTextColor || placeholderColor, onFocus, onBlur, underlineColorAndroid: 'transparent', @@ -379,6 +376,7 @@ const TextInputOutlined = ({ adornmentStyleAdjustmentForNativeInput, contentStyle, ], + testID, } as RenderProps)} @@ -389,52 +387,7 @@ const TextInputOutlined = ({ 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, }, diff --git a/src/components/TextInput/helpers.tsx b/src/components/TextInput/helpers.tsx index 291e1e40d7..9522bcfa25 100644 --- a/src/components/TextInput/helpers.tsx +++ b/src/components/TextInput/helpers.tsx @@ -1,3 +1,5 @@ +import { Platform } from 'react-native'; + import color from 'color'; import type { InternalTheme } from '../../types'; @@ -389,6 +391,24 @@ const getPlaceholderColor = ({ theme, disabled }: BaseProps) => { return theme.colors.placeholder; }; +const getSelectionColor = ({ + activeColor, + customSelectionColor, +}: { + activeColor: string; + customSelectionColor?: string; +}) => { + if (typeof customSelectionColor !== 'undefined') { + return customSelectionColor; + } + + if (Platform.OS === 'android') { + return color(activeColor).alpha(0.54).rgb().string(); + } + + return activeColor; +}; + const getFlatBackgroundColor = ({ theme, disabled }: BaseProps) => { if (theme.isV3) { if (disabled) { @@ -465,6 +485,7 @@ const getOutlinedOutlineInputColor = ({ export const getFlatInputColors = ({ underlineColor, activeUnderlineColor, + customSelectionColor, textColor, disabled, error, @@ -472,29 +493,33 @@ export const getFlatInputColors = ({ }: { underlineColor?: string; activeUnderlineColor?: string; + customSelectionColor?: string; textColor?: string; disabled?: boolean; error?: boolean; theme: InternalTheme; }) => { const baseFlatColorProps = { theme, disabled }; + const activeColor = getActiveColor({ + ...baseFlatColorProps, + error, + activeUnderlineColor, + mode: 'flat', + }); + return { inputTextColor: getInputTextColor({ ...baseFlatColorProps, textColor, mode: 'flat', }), - activeColor: getActiveColor({ - ...baseFlatColorProps, - error, - activeUnderlineColor, - mode: 'flat', - }), + activeColor, underlineColorCustom: getFlatUnderlineColor({ ...baseFlatColorProps, underlineColor, }), placeholderColor: getPlaceholderColor(baseFlatColorProps), + selectionColor: getSelectionColor({ activeColor, customSelectionColor }), errorColor: theme.colors.error, backgroundColor: getFlatBackgroundColor(baseFlatColorProps), }; @@ -503,6 +528,7 @@ export const getFlatInputColors = ({ export const getOutlinedInputColors = ({ activeOutlineColor, customOutlineColor, + customSelectionColor, textColor, disabled, error, @@ -510,12 +536,19 @@ export const getOutlinedInputColors = ({ }: { activeOutlineColor?: string; customOutlineColor?: string; + customSelectionColor?: string; textColor?: string; disabled?: boolean; error?: boolean; theme: InternalTheme; }) => { const baseOutlinedColorProps = { theme, disabled }; + const activeColor = getActiveColor({ + ...baseOutlinedColorProps, + error, + activeOutlineColor, + mode: 'outlined', + }); return { inputTextColor: getInputTextColor({ @@ -523,17 +556,13 @@ export const getOutlinedInputColors = ({ textColor, mode: 'outlined', }), - activeColor: getActiveColor({ - ...baseOutlinedColorProps, - error, - activeOutlineColor, - mode: 'outlined', - }), + activeColor, outlineColor: getOutlinedOutlineInputColor({ ...baseOutlinedColorProps, customOutlineColor, }), placeholderColor: getPlaceholderColor(baseOutlinedColorProps), + selectionColor: getSelectionColor({ activeColor, customSelectionColor }), errorColor: theme.colors.error, }; }; diff --git a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap index fed887da57..f32f1ad760 100644 --- a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -174,10 +174,6 @@ exports[`correctly applies a component as the text label 1`] = ` Object { "margin": 0, }, - Object { - "paddingLeft": 16, - "paddingRight": 16, - }, Object { "height": 56, }, @@ -192,6 +188,8 @@ exports[`correctly applies a component as the text label 1`] = ` "fontWeight": undefined, "letterSpacing": 0.15, "lineHeight": undefined, + "paddingLeft": 16, + "paddingRight": 16, "textAlign": "left", "textAlignVertical": "center", }, @@ -368,10 +366,6 @@ exports[`correctly applies cursorColor prop 1`] = ` Object { "margin": 0, }, - Object { - "paddingLeft": 16, - "paddingRight": 16, - }, Object { "height": 56, }, @@ -386,6 +380,8 @@ exports[`correctly applies cursorColor prop 1`] = ` "fontWeight": undefined, "letterSpacing": 0.15, "lineHeight": undefined, + "paddingLeft": 16, + "paddingRight": 16, "textAlign": "left", "textAlignVertical": "center", }, @@ -562,10 +558,6 @@ exports[`correctly applies default textAlign based on default RTL 1`] = ` Object { "margin": 0, }, - Object { - "paddingLeft": 16, - "paddingRight": 16, - }, Object { "height": 56, }, @@ -580,6 +572,8 @@ exports[`correctly applies default textAlign based on default RTL 1`] = ` "fontWeight": undefined, "letterSpacing": 0.15, "lineHeight": undefined, + "paddingLeft": 16, + "paddingRight": 16, "textAlign": "left", "textAlignVertical": "center", }, @@ -982,10 +976,6 @@ exports[`correctly applies paddingLeft from contentStyleProp 1`] = ` Object { "margin": 0, }, - Object { - "paddingLeft": 16, - "paddingRight": 16, - }, Object { "height": 56, }, @@ -1000,6 +990,8 @@ exports[`correctly applies paddingLeft from contentStyleProp 1`] = ` "fontWeight": undefined, "letterSpacing": 0.15, "lineHeight": undefined, + "paddingLeft": 16, + "paddingRight": 16, "textAlign": "left", "textAlignVertical": "center", }, @@ -1178,10 +1170,6 @@ exports[`correctly applies textAlign center 1`] = ` Object { "margin": 0, }, - Object { - "paddingLeft": 16, - "paddingRight": 16, - }, Object { "height": 56, }, @@ -1196,6 +1184,8 @@ exports[`correctly applies textAlign center 1`] = ` "fontWeight": undefined, "letterSpacing": 0.15, "lineHeight": undefined, + "paddingLeft": 16, + "paddingRight": 16, "textAlign": "center", "textAlignVertical": "center", }, @@ -1372,10 +1362,6 @@ exports[`correctly renders left-side affix adornment, and right-side icon adornm Object { "margin": 0, }, - Object { - "paddingLeft": 16, - "paddingRight": 56, - }, Object { "height": 56, }, @@ -1390,6 +1376,8 @@ exports[`correctly renders left-side affix adornment, and right-side icon adornm "fontWeight": undefined, "letterSpacing": 0.15, "lineHeight": undefined, + "paddingLeft": 16, + "paddingRight": 56, "textAlign": "left", "textAlignVertical": "center", }, @@ -1756,10 +1744,6 @@ exports[`correctly renders left-side icon adornment, and right-side affix adornm Object { "margin": 0, }, - Object { - "paddingLeft": 56, - "paddingRight": 56, - }, Object { "height": 56, }, @@ -1774,6 +1758,8 @@ exports[`correctly renders left-side icon adornment, and right-side affix adornm "fontWeight": undefined, "letterSpacing": 0.15, "lineHeight": undefined, + "paddingLeft": 56, + "paddingRight": 56, "textAlign": "left", "textAlignVertical": "center", },