From 881a965d2aa9b87dd383c785b9e5f17602af127a Mon Sep 17 00:00:00 2001 From: Michal Lul Date: Tue, 12 May 2026 09:34:19 +0200 Subject: [PATCH 1/2] feat: removal of text input and helper text --- docs/docs/guides/12-migration.md | 136 + docs/docusaurus.config.js | 20 +- docs/src/components/BannerExample.tsx | 8 +- docs/src/data/screenshots.js | 9 - docs/src/data/themeColors.js | 11 - example/src/ExampleList.tsx | 2 - example/src/Examples/TextInputExample.tsx | 899 ------ src/components/HelperText/HelperText.tsx | 175 -- src/components/HelperText/utils.ts | 28 - src/components/TextInput/Addons/Outline.tsx | 60 - src/components/TextInput/Addons/Underline.tsx | 75 - .../Adornment/TextInputAdornment.tsx | 201 -- .../TextInput/Adornment/TextInputAffix.tsx | 219 -- .../TextInput/Adornment/TextInputIcon.tsx | 185 -- src/components/TextInput/Adornment/enums.tsx | 12 - src/components/TextInput/Adornment/types.tsx | 11 - src/components/TextInput/Adornment/utils.ts | 36 - src/components/TextInput/Label/InputLabel.tsx | 224 -- .../TextInput/Label/LabelBackground.tsx | 100 - src/components/TextInput/TextInput.tsx | 577 ---- src/components/TextInput/TextInputFlat.tsx | 476 --- .../TextInput/TextInputOutlined.tsx | 449 --- src/components/TextInput/constants.tsx | 39 - src/components/TextInput/helpers.tsx | 501 ---- src/components/TextInput/types.tsx | 155 - src/components/__tests__/HelperText.test.tsx | 32 - src/components/__tests__/TextInput.test.tsx | 988 ------- .../__snapshots__/TextInput.test.tsx.snap | 2613 ----------------- src/index.tsx | 6 - 29 files changed, 142 insertions(+), 8105 deletions(-) create mode 100644 docs/docs/guides/12-migration.md delete mode 100644 example/src/Examples/TextInputExample.tsx delete mode 100644 src/components/HelperText/HelperText.tsx delete mode 100644 src/components/HelperText/utils.ts delete mode 100644 src/components/TextInput/Addons/Outline.tsx delete mode 100644 src/components/TextInput/Addons/Underline.tsx delete mode 100644 src/components/TextInput/Adornment/TextInputAdornment.tsx delete mode 100644 src/components/TextInput/Adornment/TextInputAffix.tsx delete mode 100644 src/components/TextInput/Adornment/TextInputIcon.tsx delete mode 100644 src/components/TextInput/Adornment/enums.tsx delete mode 100644 src/components/TextInput/Adornment/types.tsx delete mode 100644 src/components/TextInput/Adornment/utils.ts delete mode 100644 src/components/TextInput/Label/InputLabel.tsx delete mode 100644 src/components/TextInput/Label/LabelBackground.tsx delete mode 100644 src/components/TextInput/TextInput.tsx delete mode 100644 src/components/TextInput/TextInputFlat.tsx delete mode 100644 src/components/TextInput/TextInputOutlined.tsx delete mode 100644 src/components/TextInput/constants.tsx delete mode 100644 src/components/TextInput/helpers.tsx delete mode 100644 src/components/TextInput/types.tsx delete mode 100644 src/components/__tests__/HelperText.test.tsx delete mode 100644 src/components/__tests__/TextInput.test.tsx delete mode 100644 src/components/__tests__/__snapshots__/TextInput.test.tsx.snap diff --git a/docs/docs/guides/12-migration.md b/docs/docs/guides/12-migration.md new file mode 100644 index 0000000000..5f32d6d2d7 --- /dev/null +++ b/docs/docs/guides/12-migration.md @@ -0,0 +1,136 @@ +--- +title: Migration from Paper 5.x to 6.x +--- + +TBC + +## Component and types + +The Paper text field is renamed. Use **`TextField`** and the **`TextFieldProps`** type instead of **`TextInput`** / **`TextInputProps`**. + +```tsx +import { TextField, type TextFieldProps } from 'react-native-paper'; +``` + +## Visual / variant + +- **`mode="flat"`** → **`variant="filled"`** +- **`mode="outlined"`** → **`variant="outlined"`** + +```tsx +// Before (v5) + + + +// After (v6) + + +``` + +## Icons and adornments + +- **`left` / `right`** → **`StartAccessory` / `EndAccessory`** +- **`TextInput.Icon`** → **`TextField.Icon`** +- **`TextInput.Affix`** → **`prefix` / `suffix`**, or **`TextInput.Icon`**, or **`StartAccessory` / `EndAccessory`** + +```tsx +// Before (v5) +} + right={} +/> + +// After (v6) + } + EndAccessory={(p) => } + maxLength={100} + prefix={"$"} + suffix={"/100"} + counter +/> +``` + +## Label, helper, error, disabled + +- **`label: React.Element | string`** → **`string`** +- **`error` / `disabled`** → **`status="error"` / `status="disabled"`** or **`status={['error','disabled']}`** when both apply. +- **`HelperText`** was removed; use **`supportingText`**. + +```tsx +// Before (v5) +<> + + + Enter a valid email + + + +// After (v6) + +``` + +## Styling / behavior removed + +No direct `TextField` equivalents for: + +- **`dense`**, **`contentStyle`**, **`outlineStyle`**, **`underlineStyle`** +- **`underlineColor`**, **`activeUnderlineColor`**, **`outlineColor`**, **`activeOutlineColor`**, **`textColor`** +- **`render`** + +Prefer **`fieldStyle`**, **`containerStyle`**, **`pressableStyle`**, **`style`** on the inner input, and the theme. + +```tsx +import { MD3LightTheme, TextField, TextInput } from 'react-native-paper'; + +const theme = { + ...MD3LightTheme, + colors: { + ...MD3LightTheme.colors, + outline: '#79747E', + primary: '#6750A4', + }, +}; + +// Before (v5) + + +// After (v6) + +``` diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index ed833ce99e..11d45f9bea 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -126,7 +126,6 @@ const config = { AnimatedFAB: 'FAB/AnimatedFAB', FABGroup: 'FAB/FABGroup', }, - HelperText: { HelperText: 'HelperText/HelperText' }, IconButton: { IconButton: 'IconButton/IconButton', }, @@ -165,11 +164,6 @@ const config = { Switch: { Switch: 'Switch/Switch', }, - TextInput: { - TextInput: 'TextInput/TextInput', - TextInputAffix: 'TextInput/Adornment/TextInputAffix', - TextInputIcon: 'TextInput/Adornment/TextInputIcon', - }, TextField: { TextField: 'TextField/TextField', TextFieldIcon: 'TextField/TextFieldIcon', @@ -210,11 +204,8 @@ const config = { } const customUrls = { - TextInputAffix: - 'src/components/TextInput/Adornment/TextInputAffix.tsx', - TextInputIcon: - 'src/components/TextInput/Adornment/TextInputIcon.tsx', TextField: 'src/components/TextField/TextField.tsx', + TextFieldIcon: 'src/components/TextField/TextFieldIcon.tsx', Text: 'src/components/Typography/Text.tsx', showcase: 'docs/src/components/Showcase.tsx', @@ -334,14 +325,7 @@ const config = { 'https://snack.expo.dev/@react-native-paper/more-examples---snackbar-rendered-regardless-of-the-parent-positioning', }, }, - knownIssues: { - TextInput: { - 'Outline overlaps label': - 'https://github.com/callstack/react-native-paper/issues/3759#issuecomment-1601235262', - 'Long text wraps to a second line': - 'https://github.com/callstack/react-native-paper/issues/2581#issuecomment-790251987', - }, - }, + themeColors, screenshots, extendedExamples, diff --git a/docs/src/components/BannerExample.tsx b/docs/src/components/BannerExample.tsx index 7f4acdc7bc..56be90b3e4 100644 --- a/docs/src/components/BannerExample.tsx +++ b/docs/src/components/BannerExample.tsx @@ -16,7 +16,7 @@ import { RadioButton, Switch, Text, - TextInput, + TextField, useTheme, } from 'react-native-paper'; @@ -106,14 +106,14 @@ const BannerExample = () => { - setText(text)} /> - setText(text)} /> diff --git a/docs/src/data/screenshots.js b/docs/src/data/screenshots.js index ed4f3f26c1..dbba754655 100644 --- a/docs/src/data/screenshots.js +++ b/docs/src/data/screenshots.js @@ -79,7 +79,6 @@ const screenshots = { }, AnimatedFAB: 'screenshots/animated-fab.gif', 'FAB.Group': 'screenshots/fab-group.gif', - HelperText: 'screenshots/helper-text.gif', Icon: 'screenshots/icon.png', IconButton: { default: 'screenshots/icon-button-1.png', @@ -146,14 +145,6 @@ const screenshots = { 'iOS (disabled)': 'screenshots/switch-disabled.ios.png', }, Text: 'screenshots/typography.png', - TextInput: { - 'flat (focused)': 'screenshots/textinput-flat.focused.png', - 'flat (disabled)': 'screenshots/textinput-flat.disabled.png', - 'outlined (focused)': 'screenshots/textinput-outlined.focused.png', - 'outlined (disabled)': 'screenshots/textinput-outlined.disabled.png', - }, - 'TextInput.Affix': 'screenshots/textinput-outline.affix.png', - 'TextInput.Icon': 'screenshots/textinput-flat.icon.png', TextField: { filled: 'screenshots/text-field-filled.png', outlined: 'screenshots/text-field-outlined.png', diff --git a/docs/src/data/themeColors.js b/docs/src/data/themeColors.js index f12b955341..2e97dd1bce 100644 --- a/docs/src/data/themeColors.js +++ b/docs/src/data/themeColors.js @@ -190,17 +190,6 @@ const themeColors = { 'textColor/iconColor': 'theme.colors.primary', }, }, - HelperText: { - disabled: { - textColor: 'theme.colors.onSurfaceDisabled', - }, - default: { - textColor: 'theme.colors.onSurfaceVariant', - }, - error: { - textColor: 'theme.colors.error', - }, - }, IconButton: { selected: { default: { diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index 2dc53f93d4..99c21ede18 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -44,7 +44,6 @@ import TeamDetails from './Examples/TeamDetails'; import TeamsList from './Examples/TeamsList'; import TextExample from './Examples/TextExample'; import TextFieldExample from './Examples/TextFieldExample'; -import TextInputExample from './Examples/TextInputExample'; import ThemeExample from './Examples/ThemeExample'; import ThemingWithReactNavigation from './Examples/ThemingWithReactNavigation'; import ToggleButtonExample from './Examples/ToggleButtonExample'; @@ -90,7 +89,6 @@ export const mainExamples: Record< surface: SurfaceExample, switch: SwitchExample, text: TextExample, - textInput: TextInputExample, textField: TextFieldExample, toggleButton: ToggleButtonExample, tooltipExample: TooltipExample, diff --git a/example/src/Examples/TextInputExample.tsx b/example/src/Examples/TextInputExample.tsx deleted file mode 100644 index caecdc49b8..0000000000 --- a/example/src/Examples/TextInputExample.tsx +++ /dev/null @@ -1,899 +0,0 @@ -import * as React from 'react'; -import { - KeyboardAvoidingView, - Platform, - StyleSheet, - Text, - View, -} from 'react-native'; - -import Icon from '@expo/vector-icons/MaterialCommunityIcons'; -import { useFonts } from 'expo-font'; -import { - configureFonts, - HelperText, - List, - MD3Colors, - TextInput, -} from 'react-native-paper'; - -import { inputReducer, State } from '../../utils'; -import { useExampleTheme } from '../hooks/useExampleTheme'; -import ScreenWrapper from '../ScreenWrapper'; - -const MAX_LENGTH = 20; - -const initialState: State = { - text: '', - customIconText: '', - name: '', - outlinedText: '', - largeText: '', - flatTextPassword: 'Password', - flatLongText: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vitae odio quis dolor tempor mattis at non sem. Suspendisse et sem tincidunt, accumsan massa eleifend, euismod dui. Praesent eget urna lectus.', - outlinedLargeText: '', - outlinedCustomLabel: '', - outlinedTextPassword: '', - outlinedLongText: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vitae odio quis dolor tempor mattis at non sem. Suspendisse et sem tincidunt, accumsan massa eleifend, euismod dui. Praesent eget urna lectus.', - nameNoPadding: '', - customStyleText: '', - nameRequired: '', - flatDenseText: '', - flatDense: '', - outlinedDenseText: '', - outlinedDense: '', - flatMultiline: '', - flatTextArea: '', - flatUnderlineColors: '', - outlinedMultiline: '', - outlinedTextArea: '', - outlinedColors: '', - outlinedLongLabel: '', - maxLengthName: '', - flatTextSecureEntry: true, - outlineTextSecureEntry: true, - iconsColor: { - flatLeftIcon: undefined, - flatRightIcon: undefined, - outlineLeftIcon: undefined, - outlineRightIcon: undefined, - customIcon: undefined, - }, -}; - -type AvoidingViewProps = { - children: React.ReactNode; -}; - -type ExpandedId = string | number | undefined; - -const TextInputAvoidingView = ({ children }: AvoidingViewProps) => { - return Platform.OS === 'ios' ? ( - - {children} - - ) : ( - <>{children} - ); -}; - -const TextInputExample = () => { - const [state, dispatch] = React.useReducer(inputReducer, initialState); - const { - text, - customIconText, - name, - outlinedText, - largeText, - flatTextPassword, - flatLongText, - outlinedLargeText, - outlinedCustomLabel, - outlinedTextPassword, - outlinedLongText, - nameNoPadding, - customStyleText, - nameRequired, - flatDenseText, - flatDense, - outlinedDenseText, - outlinedDense, - flatMultiline, - flatTextArea, - flatUnderlineColors, - outlinedMultiline, - outlinedTextArea, - outlinedColors, - maxLengthName, - flatTextSecureEntry, - outlineTextSecureEntry, - iconsColor: { - flatLeftIcon, - flatRightIcon, - outlineLeftIcon, - outlineRightIcon, - customIcon, - }, - } = state; - - const _isUsernameValid = (name: string) => /^[a-zA-Z]*$/.test(name); - - const theme = useExampleTheme(); - - const inputActionHandler = (type: keyof State, payload: string) => - dispatch({ - type: type, - payload: payload, - }); - - const changeIconColor = (name: keyof State['iconsColor']) => { - const color = state.iconsColor[name]; - - const newColors = { - ...state.iconsColor, - [name]: !color ? theme.colors.primary : undefined, - }; - - dispatch({ - type: 'iconsColor', - payload: newColors, - }); - }; - - const [fontsLoaded] = useFonts({ - Abel: require('../../assets/fonts/Abel-Regular.ttf'), - }); - - const [expandedId, setExpandedId] = React.useState('flat'); - - const onAccordionPress = (id: string | number) => - setExpandedId(expandedId === id ? undefined : id); - - return ( - - - - - 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('flatLongText', flatLongText) - } - /> - - - - inputActionHandler('outlinedText', outlinedText) - } - left={ - { - changeIconColor('outlineLeftIcon'); - }} - /> - } - maxLength={100} - right={} - /> - - inputActionHandler('outlinedLargeText', outlinedLargeText) - } - left={} - right={ - { - changeIconColor('outlineRightIcon'); - }} - /> - } - /> - Custom label} - placeholder="Type something" - value={outlinedCustomLabel} - onChangeText={(outlinedCustomLabel) => - inputActionHandler('outlinedCustomLabel', outlinedCustomLabel) - } - /> - - inputActionHandler('outlinedTextPassword', outlinedTextPassword) - } - secureTextEntry={outlineTextSecureEntry} - right={ - - dispatch({ - type: 'outlineTextSecureEntry', - payload: !outlineTextSecureEntry, - }) - } - /> - } - /> - - inputActionHandler('outlinedLongText', outlinedLongText) - } - /> - - - - - { - 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('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={MD3Colors.primary70} - activeUnderlineColor={MD3Colors.tertiary50} - /> - - inputActionHandler('outlinedColors', outlinedColors) - } - outlineColor={MD3Colors.primary70} - activeOutlineColor={MD3Colors.tertiary50} - /> - - inputActionHandler('outlinedLongLabel', outlinedLongLabel) - } - /> - - - inputActionHandler('customStyleText', customStyleText) - } - contentStyle={styles.inputContentStyle} - /> - - - - inputActionHandler('nameNoPadding', nameNoPadding) - } - /> - - Error: Only letters are allowed - - - - - - - - - - - - - - - - - - - - - - - - - - - - {fontsLoaded ? ( - - - - ) : null} - - - - - - - - - - - - - - - - - - - - - - } - /> - - - - - - - - } - /> - - - - - - - ); -}; - -TextInputExample.title = 'TextInput'; - -const styles = StyleSheet.create({ - helpersWrapper: { - flexDirection: 'row', - justifyContent: 'space-between', - }, - wrapper: { - flex: 1, - }, - helper: { - flexShrink: 1, - }, - counterHelper: { - textAlign: 'right', - }, - inputContainerStyle: { - margin: 8, - }, - inputContentStyle: { - paddingLeft: 50, - fontWeight: 'bold', - fontStyle: 'italic', - }, - fontSize: { - fontSize: 32, - }, - textArea: { - height: 80, - }, - // eslint-disable-next-line react-native/no-color-literals - noPaddingInput: { - backgroundColor: 'transparent', - paddingHorizontal: 0, - }, - centeredText: { - textAlign: 'center', - }, - fixedHeight: { - height: 100, - }, - row: { - margin: 8, - justifyContent: 'space-between', - flexDirection: 'row', - }, - month: { - flex: 1, - marginRight: 4, - }, - year: { - flex: 1, - marginLeft: 4, - }, - inputLabelText: { - color: MD3Colors.tertiary70, - }, - left: { - width: '30%', - }, - right: { - width: '70%', - }, - autoText: { - textAlign: 'auto', - }, -}); - -export default TextInputExample; diff --git a/src/components/HelperText/HelperText.tsx b/src/components/HelperText/HelperText.tsx deleted file mode 100644 index 6fc44d58a5..0000000000 --- a/src/components/HelperText/HelperText.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import * as React from 'react'; -import { - Animated, - LayoutChangeEvent, - StyleProp, - StyleSheet, - TextStyle, - View, -} from 'react-native'; - -import { getTextColor } from './utils'; -import { useInternalTheme } from '../../core/theming'; -import type { $Omit, ThemeProp } from '../../types'; -import AnimatedText from '../Typography/AnimatedText'; - -export type Props = $Omit< - $Omit, 'padding'>, - 'type' -> & { - /** - * Type of the helper text. - */ - type: 'error' | 'info'; - /** - * Text content of the HelperText. - */ - children: React.ReactNode; - /** - * Whether to display the helper text. - */ - visible?: boolean; - /** - * Whether to apply padding to the helper text. - */ - padding?: 'none' | 'normal'; - /** - * Whether the text input tied with helper text is disabled. - */ - disabled?: boolean; - style?: StyleProp; - /** - * @optional - */ - theme?: ThemeProp; - /** - * TestID used for testing purposes - */ - testID?: string; -}; - -/** - * Helper text is used in conjuction with input elements to provide additional hints for the user. - * - * ## Usage - * ```js - * import * as React from 'react'; - * import { View } from 'react-native'; - * import { HelperText, TextInput } from 'react-native-paper'; - * - * const MyComponent = () => { - * const [text, setText] = React.useState(''); - * - * const onChangeText = text => setText(text); - * - * const hasErrors = () => { - * return !text.includes('@'); - * }; - * - * return ( - * - * - * - * Email address is invalid! - * - * - * ); - * }; - * - * export default MyComponent; - * ``` - */ -const HelperText = ({ - style, - type = 'info', - visible = true, - theme: themeOverrides, - onLayout, - padding = 'normal', - disabled, - ...rest -}: Props) => { - const theme = useInternalTheme(themeOverrides); - const { current: shown } = React.useRef( - new Animated.Value(visible ? 1 : 0) - ); - - let { current: textHeight } = React.useRef(0); - - const { scale } = theme.animation; - - const { maxFontSizeMultiplier = 1.5 } = rest; - - React.useEffect(() => { - if (visible) { - // show text - Animated.timing(shown, { - toValue: 1, - duration: 150 * scale, - useNativeDriver: true, - }).start(); - } else { - // hide text - Animated.timing(shown, { - toValue: 0, - duration: 180 * scale, - useNativeDriver: true, - }).start(); - } - }, [visible, scale, shown]); - - const handleTextLayout = (e: LayoutChangeEvent) => { - onLayout?.(e); - textHeight = e.nativeEvent.layout.height; - }; - - const { color: textColor, opacity: textOpacity } = getTextColor({ - theme, - disabled, - type, - }); - - return ( - - - {rest.children} - - - ); -}; - -const styles = StyleSheet.create({ - text: { - fontSize: 12, - paddingVertical: 4, - }, - padding: { - paddingHorizontal: 12, - }, -}); - -export default HelperText; diff --git a/src/components/HelperText/utils.ts b/src/components/HelperText/utils.ts deleted file mode 100644 index 767cb74ef0..0000000000 --- a/src/components/HelperText/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { tokens } from '../../styles/themes/v3/tokens'; -import type { InternalTheme } from '../../types'; - -const { stateOpacity } = tokens.md.ref; - -type BaseProps = { - theme: InternalTheme; - disabled?: boolean; - type?: 'error' | 'info'; -}; - -export function getTextColor({ theme, disabled, type }: BaseProps) { - if (type === 'error') { - return { color: theme.colors.error, opacity: stateOpacity.enabled }; - } - - if (disabled) { - return { - color: theme.colors.onSurfaceVariant, - opacity: stateOpacity.disabled, - }; - } - - return { - color: theme.colors.onSurfaceVariant, - opacity: stateOpacity.enabled, - }; -} diff --git a/src/components/TextInput/Addons/Outline.tsx b/src/components/TextInput/Addons/Outline.tsx deleted file mode 100644 index 39ffa48b56..0000000000 --- a/src/components/TextInput/Addons/Outline.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; -import { - StyleSheet, - ColorValue, - StyleProp, - View, - ViewStyle, -} from 'react-native'; - -import { TextInputLabelProp } from '../types'; - -type OutlineProps = { - activeColor: string; - backgroundColor: ColorValue; - hasActiveOutline?: boolean; - outlineColor?: string; - roundness?: number; - label?: TextInputLabelProp; - style?: StyleProp; -}; - -export const Outline = ({ - label, - activeColor, - backgroundColor, - hasActiveOutline, - outlineColor, - roundness, - style, -}: OutlineProps) => ( - -); - -const styles = StyleSheet.create({ - outline: { - position: 'absolute', - left: 0, - right: 0, - top: 6, - bottom: 0, - }, - noLabelOutline: { - top: 0, - }, -}); diff --git a/src/components/TextInput/Addons/Underline.tsx b/src/components/TextInput/Addons/Underline.tsx deleted file mode 100644 index 22a8e419cc..0000000000 --- a/src/components/TextInput/Addons/Underline.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react'; -import { Animated, StyleSheet, StyleProp, ViewStyle } from 'react-native'; - -import type { ThemeProp } from 'src/types'; - -type UnderlineProps = { - parentState: { - focused: boolean; - }; - error?: boolean; - colors?: { - error?: string; - }; - activeColor: string; - underlineColorCustom?: string; - hasActiveOutline?: boolean; - disabledOpacity?: number; - style?: StyleProp; - theme?: ThemeProp; -}; - -export const Underline = ({ - parentState, - error, - colors, - activeColor, - underlineColorCustom, - hasActiveOutline, - disabledOpacity, - style, - theme: _themeOverrides, -}: UnderlineProps) => { - let backgroundColor = parentState.focused - ? activeColor - : underlineColorCustom; - - if (error) backgroundColor = colors?.error; - - const activeScale = 2; - - 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/Adornment/TextInputAdornment.tsx b/src/components/TextInput/Adornment/TextInputAdornment.tsx deleted file mode 100644 index 6295908025..0000000000 --- a/src/components/TextInput/Adornment/TextInputAdornment.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import React from 'react'; -import type { - LayoutChangeEvent, - TextStyle, - StyleProp, - Animated, - DimensionValue, -} from 'react-native'; - -import { AdornmentSide, AdornmentType, InputMode } from './enums'; -import TextInputAffix, { AffixAdornment } from './TextInputAffix'; -import TextInputIcon, { IconAdornment } from './TextInputIcon'; -import type { - AdornmentConfig, - AdornmentStyleAdjustmentForNativeInput, -} from './types'; -import { getConstants } from '../helpers'; - -export function getAdornmentConfig({ - left, - right, -}: { - left?: React.ReactNode; - right?: React.ReactNode; -}): Array { - let adornmentConfig: any[] = []; - if (left || right) { - [ - { side: AdornmentSide.Left, adornment: left }, - { side: AdornmentSide.Right, adornment: right }, - ].forEach(({ side, adornment }) => { - if (adornment && React.isValidElement(adornment)) { - let type; - if (adornment.type === TextInputAffix) { - type = AdornmentType.Affix; - } else if (adornment.type === TextInputIcon) { - type = AdornmentType.Icon; - } - adornmentConfig.push({ - side, - type, - }); - } - }); - } - - return adornmentConfig; -} - -export function getAdornmentStyleAdjustmentForNativeInput({ - adornmentConfig, - leftAffixWidth, - rightAffixWidth, - paddingHorizontal, - inputOffset = 0, - mode, -}: { - inputOffset?: number; - adornmentConfig: AdornmentConfig[]; - leftAffixWidth: number; - rightAffixWidth: number; - mode?: 'outlined' | 'flat'; - paddingHorizontal?: DimensionValue; -}): AdornmentStyleAdjustmentForNativeInput | {} { - const { OUTLINED_INPUT_OFFSET, ADORNMENT_OFFSET } = getConstants(); - - if (adornmentConfig.length) { - const adornmentStyleAdjustmentForNativeInput = adornmentConfig.map( - ({ type, side }: AdornmentConfig) => { - const isLeftSide = side === AdornmentSide.Left; - const inputModeAdornemntOffset = - mode === InputMode.Outlined - ? ADORNMENT_OFFSET + OUTLINED_INPUT_OFFSET - : ADORNMENT_OFFSET; - const paddingKey = `padding${captalize(side)}`; - const affixWidth = isLeftSide ? leftAffixWidth : rightAffixWidth; - const padding = - typeof paddingHorizontal === 'number' - ? paddingHorizontal - : inputModeAdornemntOffset; - const offset = affixWidth + padding; - - const isAffix = type === AdornmentType.Affix; - const marginKey = `margin${captalize(side)}`; - - return { - [marginKey]: isAffix ? 0 : offset, - [paddingKey]: isAffix ? offset : inputOffset, - }; - } - ); - const allStyleAdjustmentsMerged = - adornmentStyleAdjustmentForNativeInput.reduce( - (mergedStyles, currentStyle) => { - return { - ...mergedStyles, - ...currentStyle, - }; - }, - {} - ); - return allStyleAdjustmentsMerged; - } else { - return [{}]; - } -} - -const captalize = (text: string) => - text.charAt(0).toUpperCase() + text.slice(1); - -export interface TextInputAdornmentProps { - forceFocus: () => void; - adornmentConfig: AdornmentConfig[]; - topPosition: { - [AdornmentType.Affix]: { - [AdornmentSide.Left]: number | null; - [AdornmentSide.Right]: number | null; - }; - [AdornmentType.Icon]: number; - }; - onAffixChange: { - [AdornmentSide.Left]: (event: LayoutChangeEvent) => void; - [AdornmentSide.Right]: (event: LayoutChangeEvent) => void; - }; - left?: React.ReactNode; - right?: React.ReactNode; - textStyle?: StyleProp; - visible?: Animated.Value; - isTextInputFocused: boolean; - paddingHorizontal?: DimensionValue; - maxFontSizeMultiplier?: number | undefined | null; - disabled?: boolean; -} - -const TextInputAdornment: React.FunctionComponent = ({ - adornmentConfig, - left, - right, - onAffixChange, - textStyle, - visible, - topPosition, - isTextInputFocused, - forceFocus, - paddingHorizontal, - maxFontSizeMultiplier, - disabled, -}) => { - if (adornmentConfig.length) { - return ( - <> - {adornmentConfig.map(({ type, side }: AdornmentConfig) => { - let inputAdornmentComponent; - if (side === AdornmentSide.Left) { - inputAdornmentComponent = left; - } else if (side === AdornmentSide.Right) { - inputAdornmentComponent = right; - } - - const commonProps = { - side: side, - testID: `${side}-${type}-adornment`, - isTextInputFocused, - paddingHorizontal, - disabled, - }; - if (type === AdornmentType.Icon) { - return ( - - ); - } else if (type === AdornmentType.Affix) { - return ( - - ); - } else { - return null; - } - })} - - ); - } else { - return null; - } -}; - -export default TextInputAdornment; diff --git a/src/components/TextInput/Adornment/TextInputAffix.tsx b/src/components/TextInput/Adornment/TextInputAffix.tsx deleted file mode 100644 index 94a0c08662..0000000000 --- a/src/components/TextInput/Adornment/TextInputAffix.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import React from 'react'; -import { - Animated, - DimensionValue, - GestureResponderEvent, - LayoutChangeEvent, - Pressable, - StyleProp, - StyleSheet, - Text, - TextStyle, - ViewStyle, -} from 'react-native'; - -import { AdornmentSide } from './enums'; -import { getTextColor } from './utils'; -import { useInternalTheme } from '../../../core/theming'; -import type { ThemeProp } from '../../../types'; -import { getConstants } from '../helpers'; - -export type Props = { - /** - * Text to show. - */ - text: string; - onLayout?: (event: LayoutChangeEvent) => void; - /** - * Function to execute on press. - */ - onPress?: (e: GestureResponderEvent) => void; - /** - * Accessibility label for the affix. This is read by the screen reader when the user taps the affix. - */ - accessibilityLabel?: string; - /** - * Style that is passed to the Text element. - */ - textStyle?: StyleProp; - /** - * @optional - */ - theme?: ThemeProp; -}; - -type ContextState = { - topPosition: number | null; - onLayout?: (event: LayoutChangeEvent) => void; - visible?: Animated.Value; - textStyle?: StyleProp; - side: AdornmentSide; - paddingHorizontal?: DimensionValue; - maxFontSizeMultiplier?: number | undefined | null; - testID?: string; - disabled?: boolean; -}; - -const AffixContext = React.createContext({ - textStyle: { fontFamily: '', color: '' }, - topPosition: null, - side: AdornmentSide.Left, -}); - -const AffixAdornment: React.FunctionComponent< - { - affix: React.ReactNode; - testID: string; - } & ContextState -> = ({ - affix, - side, - textStyle, - topPosition, - onLayout, - visible, - paddingHorizontal, - maxFontSizeMultiplier, - testID, - disabled, -}) => { - return ( - - {affix} - - ); -}; - -/** - * A component to render a leading / trailing text in the TextInput - * - * ## Usage - * ```js - * import * as React from 'react'; - * import { TextInput } from 'react-native-paper'; - * - * const MyComponent = () => { - * const [text, setText] = React.useState(''); - * - * return ( - * } - * /> - * ); - * }; - * - * export default MyComponent; - * ``` - */ - -const TextInputAffix = ({ - text, - textStyle: labelStyle, - theme: themeOverrides, - onLayout: onTextLayout, - onPress, - accessibilityLabel = text, -}: Props) => { - const theme = useInternalTheme(themeOverrides); - const { AFFIX_OFFSET } = getConstants(); - - const { - textStyle, - onLayout, - topPosition, - side, - visible, - paddingHorizontal, - maxFontSizeMultiplier, - testID, - disabled, - } = React.useContext(AffixContext); - - const offset = - typeof paddingHorizontal === 'number' ? paddingHorizontal : AFFIX_OFFSET; - - const style = { - top: topPosition, - [side]: offset, - } as ViewStyle; - - const { color: textColor, opacity: textOpacity } = getTextColor({ - theme, - disabled, - }); - - const content = ( - - {text} - - ); - - return ( - - {onPress ? ( - - {content} - - ) : ( - content - )} - - ); -}; - -TextInputAffix.displayName = 'TextInput.Affix'; - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - justifyContent: 'center', - alignItems: 'center', - }, -}); - -export default TextInputAffix; - -// @component-docs ignore-next-line -export { TextInputAffix, AffixAdornment }; diff --git a/src/components/TextInput/Adornment/TextInputIcon.tsx b/src/components/TextInput/Adornment/TextInputIcon.tsx deleted file mode 100644 index 0fc2c7c906..0000000000 --- a/src/components/TextInput/Adornment/TextInputIcon.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import React from 'react'; -import { - GestureResponderEvent, - StyleProp, - StyleSheet, - View, - ViewStyle, -} from 'react-native'; - -import { getIconColor } from './utils'; -import { useInternalTheme } from '../../../core/theming'; -import type { $Omit, ThemeProp } from '../../../types'; -import type { IconSource } from '../../Icon'; -import IconButton from '../../IconButton/IconButton'; -import { ICON_SIZE } from '../constants'; -import { getConstants } from '../helpers'; - -export type Props = $Omit< - React.ComponentProps, - 'icon' | 'theme' | 'color' | 'iconColor' -> & { - /** - * @renamed Renamed from 'name' to 'icon` in v5.x - * Icon to show. - */ - icon: IconSource; - /** - * Function to execute on press. - */ - onPress?: (e: GestureResponderEvent) => void; - /** - * Whether the TextInput will focus after onPress. - */ - forceTextInputFocus?: boolean; - /** - * Color of the icon or a function receiving a boolean indicating whether the TextInput is focused and returning the color. - */ - color?: ((isTextInputFocused: boolean) => string | undefined) | string; - style?: StyleProp; - /** - * @optional - */ - theme?: ThemeProp; -}; - -type StyleContextType = { - style: StyleProp; - isTextInputFocused: boolean; - forceFocus: () => void; - testID: string; - disabled?: boolean; -}; - -const StyleContext = React.createContext({ - style: {}, - isTextInputFocused: false, - forceFocus: () => {}, - testID: '', -}); - -const IconAdornment: React.FunctionComponent< - { - testID: string; - icon: React.ReactNode; - topPosition: number; - side: 'left' | 'right'; - disabled?: boolean; - } & Omit -> = ({ - icon, - topPosition, - side, - isTextInputFocused, - forceFocus, - testID, - disabled, -}) => { - const { ICON_OFFSET } = getConstants(); - - const style = { - top: topPosition, - [side]: ICON_OFFSET, - }; - const contextState = { - style, - isTextInputFocused, - forceFocus, - testID, - disabled, - }; - - return ( - {icon} - ); -}; - -/** - * A component to render a leading / trailing icon in the TextInput - * - * ## Usage - * ```js - * import * as React from 'react'; - * import { TextInput } from 'react-native-paper'; - * - * const MyComponent = () => { - * const [text, setText] = React.useState(''); - * - * return ( - * } - * /> - * ); - * }; - * - * export default MyComponent; - * ``` - */ - -const TextInputIcon = ({ - icon, - onPress, - forceTextInputFocus = true, - color: customColor, - theme: themeOverrides, - ...rest -}: Props) => { - const { style, isTextInputFocused, forceFocus, testID, disabled } = - React.useContext(StyleContext); - - const onPressWithFocusControl = React.useCallback( - (e: GestureResponderEvent) => { - if (forceTextInputFocus && !isTextInputFocused) { - forceFocus(); - } - - onPress?.(e); - }, - [forceTextInputFocus, forceFocus, isTextInputFocused, onPress] - ); - - const theme = useInternalTheme(themeOverrides); - - const { color: iconColor, opacity: iconOpacity } = getIconColor({ - theme, - disabled, - isTextInputFocused, - customColor, - }); - - return ( - - - - ); -}; -TextInputIcon.displayName = 'TextInput.Icon'; - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - width: ICON_SIZE, - height: ICON_SIZE, - justifyContent: 'center', - alignItems: 'center', - }, - iconButton: { - margin: 0, - }, -}); - -export default TextInputIcon; - -// @component-docs ignore-next-line -export { IconAdornment }; diff --git a/src/components/TextInput/Adornment/enums.tsx b/src/components/TextInput/Adornment/enums.tsx deleted file mode 100644 index 9a364f7215..0000000000 --- a/src/components/TextInput/Adornment/enums.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export enum AdornmentType { - Icon = 'icon', - Affix = 'affix', -} -export enum AdornmentSide { - Right = 'right', - Left = 'left', -} -export enum InputMode { - Outlined = 'outlined', - Flat = 'flat', -} diff --git a/src/components/TextInput/Adornment/types.tsx b/src/components/TextInput/Adornment/types.tsx deleted file mode 100644 index fbd81c936a..0000000000 --- a/src/components/TextInput/Adornment/types.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { AdornmentSide, AdornmentType } from './enums'; - -export type AdornmentConfig = { - side: AdornmentSide; - type: AdornmentType; -}; -export type AdornmentStyleAdjustmentForNativeInput = { - adornmentStyleAdjustmentForNativeInput: Array< - { paddingRight: number; paddingLeft: number } | {} - >; -}; diff --git a/src/components/TextInput/Adornment/utils.ts b/src/components/TextInput/Adornment/utils.ts deleted file mode 100644 index e7d57985d6..0000000000 --- a/src/components/TextInput/Adornment/utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { tokens } from '../../../styles/themes/v3/tokens'; -import type { InternalTheme } from '../../../types'; - -const { stateOpacity } = tokens.md.ref; - -type BaseProps = { - theme: InternalTheme; - disabled?: boolean; -}; - -export function getTextColor({ theme, disabled }: BaseProps) { - return { - color: theme.colors.onSurfaceVariant, - opacity: disabled ? stateOpacity.disabled : stateOpacity.enabled, - }; -} - -export function getIconColor({ - theme, - isTextInputFocused, - disabled, - customColor, -}: BaseProps & { - isTextInputFocused: boolean; - customColor?: ((isTextInputFocused: boolean) => string | undefined) | string; -}) { - const color = - typeof customColor === 'function' - ? customColor(isTextInputFocused) - : customColor ?? theme.colors.onSurfaceVariant; - - const opacity = - disabled && !customColor ? stateOpacity.disabled : stateOpacity.enabled; - - return { color, opacity }; -} diff --git a/src/components/TextInput/Label/InputLabel.tsx b/src/components/TextInput/Label/InputLabel.tsx deleted file mode 100644 index 6fd869a1c3..0000000000 --- a/src/components/TextInput/Label/InputLabel.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React from 'react'; -import { - Animated, - ColorValue, - Platform, - StyleSheet, - useWindowDimensions, - View, -} from 'react-native'; - -import AnimatedText from '../../Typography/AnimatedText'; -import { getConstants } from '../helpers'; -import type { InputLabelProps } from '../types'; - -const InputLabel = (props: InputLabelProps) => { - const { - labeled, - wiggle, - error, - focused, - labelLayoutWidth, - labelLayoutHeight, - labelBackground, - label, - labelError, - onLayoutAnimatedText, - onLabelTextLayout, - hasActiveOutline, - activeColor, - placeholderStyle, - baseLabelTranslateX, - baseLabelTranslateY, - font, - fontSize, - lineHeight, - fontWeight, - placeholderOpacity, - wiggleOffsetX, - labelScale, - topPosition, - paddingLeft, - paddingRight, - backgroundColor, - roundness, - placeholderColor, - disabledOpacity, - opacity, - errorColor, - labelTranslationXOffset, - maxFontSizeMultiplier, - testID, - inputContainerLayout, - scaledLabel, - } = props; - - const { INPUT_PADDING_HORIZONTAL } = getConstants(); - const { width } = useWindowDimensions(); - - const isWeb = Platform.OS === 'web'; - - const paddingOffset = - paddingLeft && paddingRight ? { paddingLeft, paddingRight } : {}; - - const labelTranslationX = { - transform: [ - { - // Offset label scale since RN doesn't support transform origin - translateX: labeled.interpolate({ - inputRange: [0, 1], - outputRange: [baseLabelTranslateX, labelTranslationXOffset || 0], - }), - }, - ], - }; - - const labelStyle = { - ...font, - fontSize, - lineHeight, - fontWeight, - opacity: labeled.interpolate({ - inputRange: [0, 1], - outputRange: [hasActiveOutline ? 1 : 0, 0], - }), - transform: [ - { - // Wiggle the label when there's an error - translateX: wiggle - ? error.interpolate({ - inputRange: [0, 0.5, 1], - outputRange: [0, wiggleOffsetX, 0], - }) - : 0, - }, - { - // Move label to top - translateY: - baseLabelTranslateY !== 0 - ? labeled.interpolate({ - inputRange: [0, 1], - outputRange: [baseLabelTranslateY, 0], - }) - : 0, - }, - { - // Make label smaller - scale: - labelScale !== 0 - ? labeled.interpolate({ - inputRange: [0, 1], - outputRange: [labelScale, 1], - }) - : labeled, - }, - ], - }; - - const labelWidth = - (inputContainerLayout.width + INPUT_PADDING_HORIZONTAL / 2) / - (scaledLabel ? labelScale : 1); - - const commonStyles = [ - placeholderStyle, - { - top: topPosition, - }, - { - maxWidth: labelWidth, - }, - labelStyle, - paddingOffset || {}, - ]; - - const textColor = ( - labelError && errorColor ? errorColor : placeholderColor - ) as ColorValue; - - return ( - // Position colored placeholder and gray placeholder on top of each other and crossfade them - // This gives the effect of animating the color, but allows us to use native driver - - - - {labelBackground?.({ - labeled, - labelLayoutWidth, - labelLayoutHeight, - labelStyle, - placeholderStyle, - baseLabelTranslateX, - topPosition, - label, - backgroundColor, - roundness, - maxFontSizeMultiplier: maxFontSizeMultiplier, - testID, - })} - - {label} - - - {label} - - - - - ); -}; - -const styles = StyleSheet.create({ - overflow: { - overflow: 'hidden', - }, - labelContainer: { - zIndex: 3, - }, -}); - -export default React.memo(InputLabel); diff --git a/src/components/TextInput/Label/LabelBackground.tsx b/src/components/TextInput/Label/LabelBackground.tsx deleted file mode 100644 index 409606d208..0000000000 --- a/src/components/TextInput/Label/LabelBackground.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import * as React from 'react'; -import { Animated, StyleSheet } from 'react-native'; - -import AnimatedText from '../../Typography/AnimatedText'; -import type { LabelBackgroundProps } from '../types'; - -const LabelBackground = ({ - labeled, - labelLayoutWidth, - labelLayoutHeight, - placeholderStyle, - baseLabelTranslateX, - topPosition, - backgroundColor, - roundness, - labelStyle, - maxFontSizeMultiplier, - testID, -}: LabelBackgroundProps) => { - const opacity = labeled.interpolate({ - inputRange: [0, 0.6], - outputRange: [1, 0], - }); - - const labelTranslationX = { - translateX: labeled.interpolate({ - inputRange: [0, 1], - outputRange: [-baseLabelTranslateX, 0], - }), - }; - - const labelTextScaleY = { - scaleY: labeled.interpolate({ - inputRange: [0, 1], - outputRange: [0.2, 1], - }), - }; - - const labelTextTransform = [...labelStyle.transform, labelTextScaleY]; - - const isRounded = roundness > 6; - const roundedEdgeCover = isRounded ? ( - - ) : null; - - return [ - roundedEdgeCover, - , - ]; -}; - -export default LabelBackground; - -const styles = StyleSheet.create({ - view: { - position: 'absolute', - top: 6, - left: 10, - width: 12, - }, - // eslint-disable-next-line react-native/no-color-literals - outlinedLabel: { - position: 'absolute', - left: 8, - paddingHorizontal: 0, - color: 'transparent', - }, -}); diff --git a/src/components/TextInput/TextInput.tsx b/src/components/TextInput/TextInput.tsx deleted file mode 100644 index 7c289d9152..0000000000 --- a/src/components/TextInput/TextInput.tsx +++ /dev/null @@ -1,577 +0,0 @@ -import * as React from 'react'; -import { - Animated, - LayoutChangeEvent, - StyleProp, - TextInput as NativeTextInput, - TextStyle, - ViewStyle, - NativeSyntheticEvent, - TextLayoutEventData, -} from 'react-native'; - -import TextInputAffix, { - Props as TextInputAffixProps, -} from './Adornment/TextInputAffix'; -import TextInputIcon, { - Props as TextInputIconProps, -} from './Adornment/TextInputIcon'; -import TextInputFlat from './TextInputFlat'; -import TextInputOutlined from './TextInputOutlined'; -import type { RenderProps, TextInputLabelProp } from './types'; -import { useInternalTheme } from '../../core/theming'; -import type { ThemeProp } from '../../types'; -import { forwardRef } from '../../utils/forwardRef'; -import { roundLayoutSize } from '../../utils/roundLayoutSize'; - -const BLUR_ANIMATION_DURATION = 180; -const FOCUS_ANIMATION_DURATION = 150; - -export type Props = React.ComponentPropsWithRef & { - /** - * Mode of the TextInput. - * - `flat` - flat input with an underline. - * - `outlined` - input with an outline. - * - * In `outlined` mode, the background color of the label is derived from `colors?.background` in theme or the `backgroundColor` style. - * This component render TextInputOutlined or TextInputFlat based on that props - */ - mode?: 'flat' | 'outlined'; - /** - * The adornment placed on the left side of the input. It can be either `TextInput.Icon` or `TextInput.Affix`. - */ - left?: React.ReactNode; - /** - * The adornment placed on the right side of the input. It can be either `TextInput.Icon` or `TextInput.Affix`. - */ - right?: React.ReactNode; - /** - * If true, user won't be able to interact with the component. - */ - disabled?: boolean; - /** - * The text or component to use for the floating label. - */ - label?: TextInputLabelProp; - /** - * Placeholder for the input. - */ - placeholder?: string; - /** - * Whether to style the TextInput with error style. - */ - error?: boolean; - /** - * Callback that is called when the text input's text changes. Changed text is passed as an argument to the callback handler. - */ - onChangeText?: Function; - /** - * Selection color of the input. On iOS, it sets both the selection color and cursor color. - * On Android, it sets only the selection color. - */ - selectionColor?: string; - /** - * @platform Android only - * Cursor (or "caret") color of the input on Android. - * This property has no effect on iOS. - */ - cursorColor?: string; - /** - * Inactive underline color of the input. - */ - underlineColor?: string; - /** - * Active underline color of the input. - */ - activeUnderlineColor?: string; - /** - * Inactive outline color of the input. - */ - outlineColor?: string; - /** - * Active outline color of the input. - */ - activeOutlineColor?: string; - /** - * Color of the text in the input. - */ - textColor?: string; - /** - * Sets min height with densed layout. For `TextInput` in `flat` mode - * height is `64dp` or in dense layout - `52dp` with label or `40dp` without label. - * For `TextInput` in `outlined` mode - * height is `56dp` or in dense layout - `40dp` regardless of label. - * When you apply `height` prop in style the `dense` prop affects only `paddingVertical` inside `TextInput` - */ - dense?: boolean; - /** - * Whether the input can have multiple lines. - */ - multiline?: boolean; - /** - * @platform Android only - * The number of lines to show in the input (Android only). - */ - numberOfLines?: number; - /** - * Callback that is called when the text input is focused. - */ - onFocus?: (args: any) => void; - /** - * Callback that is called when the text input is blurred. - */ - onBlur?: (args: any) => void; - /** - * - * Callback to render a custom input component such as `react-native-text-input-mask` - * instead of the default `TextInput` component from `react-native`. - * - * Example: - * ```js - * - * - * } - * /> - * ``` - */ - render?: (props: RenderProps) => React.ReactNode; - /** - * Value of the text input. - */ - value?: string; - /** - * Pass `fontSize` prop to modify the font size inside `TextInput`. - * Pass `height` prop to set `TextInput` height. When `height` is passed, - * `dense` prop will affect only input's `paddingVertical`. - * Pass `paddingHorizontal` to modify horizontal padding. - * This can be used to get MD Guidelines v1 TextInput look. - */ - style?: StyleProp; - /** - * @optional - */ - theme?: ThemeProp; - /** - * testID to be used on tests. - */ - testID?: string; - /** - * Pass custom style directly to the input itself. - * Overrides input style - * Example: `paddingLeft`, `backgroundColor` - */ - contentStyle?: StyleProp; - /** - * Pass style to override the default style of outlined wrapper. - * Overrides style when mode is set to `outlined` - * Example: `borderRadius`, `borderColor` - */ - outlineStyle?: StyleProp; - /** - * Pass style to override the default style of underlined wrapper. - * Overrides style when mode is set to `flat` - * Example: `borderRadius`, `borderColor` - */ - underlineStyle?: StyleProp; -}; - -interface CompoundedComponent - extends React.ForwardRefExoticComponent< - Props & React.RefAttributes - > { - Icon: React.FunctionComponent; - Affix: React.FunctionComponent>; -} - -type TextInputHandles = Pick< - NativeTextInput, - 'focus' | 'clear' | 'blur' | 'isFocused' | 'setNativeProps' | 'setSelection' ->; - -const DefaultRenderer = (props: RenderProps) => ; - -/** - * A component to allow users to input text. - * - * ## Usage - * ```js - * import * as React from 'react'; - * import { TextInput } from 'react-native-paper'; - * - * const MyComponent = () => { - * const [text, setText] = React.useState(""); - * - * return ( - * setText(text)} - * /> - * ); - * }; - * - * export default MyComponent; - * ``` - * - * @extends TextInput props https://reactnative.dev/docs/textinput#props - */ -const TextInput = forwardRef( - ( - { - mode = 'flat', - dense = false, - disabled = false, - error: errorProp = false, - multiline = false, - editable = true, - contentStyle, - render = DefaultRenderer, - theme: themeOverrides, - ...rest - }: Props, - 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< - string | undefined - >(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: boolean; - width: number; - height: number; - }>({ - measured: false, - width: 0, - height: 0, - }); - const [leftLayout, setLeftLayout] = React.useState<{ - height: number | null; - width: number | null; - }>({ - width: null, - height: null, - }); - const [rightLayout, setRightLayout] = React.useState<{ - height: number | null; - width: number | null; - }>({ - 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: Object) => root.current?.setNativeProps(args), - isFocused: () => root.current?.isFocused() || false, - blur: () => root.current?.blur(), - forceFocus: () => root.current?.focus(), - setSelection: (start: number, end: number) => - 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 - ) as unknown as NodeJS.Timeout; - } - } 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: LayoutChangeEvent) => { - 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: LayoutChangeEvent) => { - 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: any) => { - if (disabled || !editable) { - return; - } - - setFocused(true); - - rest.onFocus?.(args); - }; - - const handleBlur = (args: Object) => { - if (!editable) { - return; - } - - setFocused(false); - rest.onBlur?.(args); - }; - - const handleChangeText = (value: string) => { - 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: LayoutChangeEvent) => { - 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 }: NativeSyntheticEvent) => { - setLabelTextLayout({ - width: nativeEvent.lines.reduce( - (acc, line) => acc + Math.ceil(line.width), - 0 - ), - }); - }, - [] - ); - - const handleInputContainerLayout = React.useCallback( - ({ nativeEvent: { layout } }: LayoutChangeEvent) => { - setInputContainerLayout({ - width: layout.width, - }); - }, - [] - ); - - const forceFocus = React.useCallback(() => root.current?.focus(), []); - - const { maxFontSizeMultiplier = 1.5 } = rest; - - const scaledLabel = !!(value || focused); - - if (mode === 'outlined') { - return ( - { - 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 ( - { - 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} - /> - ); - } -) as CompoundedComponent; -// @component ./Adornment/TextInputIcon.tsx -TextInput.Icon = TextInputIcon; - -// @component ./Adornment/TextInputAffix.tsx -// @ts-ignore Types of property 'theme' are incompatible. -TextInput.Affix = TextInputAffix; - -export default TextInput; diff --git a/src/components/TextInput/TextInputFlat.tsx b/src/components/TextInput/TextInputFlat.tsx deleted file mode 100644 index e1f4c2a6c3..0000000000 --- a/src/components/TextInput/TextInputFlat.tsx +++ /dev/null @@ -1,476 +0,0 @@ -import * as React from 'react'; -import { - I18nManager, - Platform, - StyleSheet, - TextInput as NativeTextInput, - TextStyle, - View, - Animated, -} from 'react-native'; - -import { Underline } from './Addons/Underline'; -import { AdornmentSide, AdornmentType, InputMode } from './Adornment/enums'; -import TextInputAdornment, { - TextInputAdornmentProps, -} from './Adornment/TextInputAdornment'; -import { - getAdornmentConfig, - getAdornmentStyleAdjustmentForNativeInput, -} from './Adornment/TextInputAdornment'; -import { - ADORNMENT_SIZE, - LABEL_PADDING_TOP_DENSE, - LABEL_WIGGLE_X_OFFSET, - MAXIMIZED_LABEL_FONT_SIZE, - MINIMIZED_LABEL_FONT_SIZE, - MINIMIZED_LABEL_Y_OFFSET, - MIN_DENSE_HEIGHT, - MIN_DENSE_HEIGHT_WL, -} from './constants'; -import { - adjustPaddingFlat, - calculateFlatAffixTopPosition, - calculateFlatInputHorizontalPadding, - calculateInputHeight, - calculateLabelTopPosition, - calculatePadding, - getConstants, - getFlatInputColors, - Padding, -} from './helpers'; -import InputLabel from './Label/InputLabel'; -import type { ChildTextInputProps, RenderProps } from './types'; - -const TextInputFlat = ({ - disabled = false, - editable = true, - label, - error = false, - selectionColor: customSelectionColor, - cursorColor, - underlineColor, - underlineStyle, - activeUnderlineColor, - textColor, - dense, - style, - theme, - render = (props: RenderProps) => , - multiline = false, - parentState, - innerRef, - onFocus, - forceFocus, - onBlur, - onChangeText, - onLayoutAnimatedText, - onLabelTextLayout, - onLeftAffixLayoutChange, - onRightAffixLayoutChange, - onInputLayout, - left, - right, - placeholderTextColor, - testID = 'text-input-flat', - contentStyle, - scaledLabel, - ...rest -}: ChildTextInputProps) => { - const isAndroid = Platform.OS === 'android'; - const { colors, roundness } = theme; - const font = theme.fonts.bodyLarge; - const hasActiveOutline = parentState.focused || error; - - const { LABEL_PADDING_TOP, FLAT_INPUT_OFFSET, MIN_HEIGHT, MIN_WIDTH } = - getConstants(); - - const { - fontSize: fontSizeStyle, - lineHeight: lineHeightStyle, - fontWeight, - height, - paddingHorizontal, - textAlign, - ...viewStyle - } = (StyleSheet.flatten(style) || {}) as TextStyle; - const fontSize = fontSizeStyle || MAXIMIZED_LABEL_FONT_SIZE; - const lineHeight = - lineHeightStyle || (Platform.OS === 'web' ? fontSize * 1.2 : undefined); - - const isPaddingHorizontalPassed = - paddingHorizontal !== undefined && typeof paddingHorizontal === 'number'; - - const adornmentConfig = getAdornmentConfig({ - left, - right, - }); - - let { paddingLeft, paddingRight } = calculateFlatInputHorizontalPadding({ - adornmentConfig, - }); - - if (isPaddingHorizontalPassed) { - paddingLeft = paddingHorizontal as number; - paddingRight = paddingHorizontal as number; - } - - const { leftLayout, rightLayout } = parentState; - - const rightAffixWidth = right - ? rightLayout.width || ADORNMENT_SIZE - : ADORNMENT_SIZE; - - const leftAffixWidth = left - ? leftLayout.width || ADORNMENT_SIZE - : ADORNMENT_SIZE; - - const adornmentStyleAdjustmentForNativeInput = - getAdornmentStyleAdjustmentForNativeInput({ - adornmentConfig, - rightAffixWidth, - leftAffixWidth, - paddingHorizontal, - inputOffset: FLAT_INPUT_OFFSET, - mode: InputMode.Flat, - }); - - const { - inputTextColor, - activeColor, - disabledOpacity, - underlineColorCustom, - placeholderColor, - errorColor, - backgroundColor, - selectionColor, - } = getFlatInputColors({ - underlineColor, - activeUnderlineColor, - customSelectionColor, - textColor, - disabled, - error, - theme, - }); - - const containerStyle = { - backgroundColor, - borderTopLeftRadius: theme.roundness, - borderTopRightRadius: theme.roundness, - }; - - 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) + - (1 - labelScale) * - (I18nManager.getConstants().isRTL ? -1 : 1) * - paddingLeft; - - const minInputHeight = dense - ? (label ? MIN_DENSE_HEIGHT_WL : MIN_DENSE_HEIGHT) - LABEL_PADDING_TOP_DENSE - : MIN_HEIGHT - LABEL_PADDING_TOP; - - const inputHeight = calculateInputHeight(labelHeight, height, minInputHeight); - - const topPosition = calculateLabelTopPosition( - labelHeight, - inputHeight, - multiline && height ? 0 : !height ? minInputHeight / 2 : 0 - ); - - 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: FLAT_INPUT_OFFSET, - multiline: multiline ? multiline : null, - dense: dense ? dense : null, - topPosition, - fontSize, - lineHeight, - label, - scale: fontScale, - isAndroid, - styles: StyleSheet.flatten( - dense ? styles.inputFlatDense : styles.inputFlat - ) as Padding, - }; - - const pad = calculatePadding(paddingSettings); - - const paddingFlat = adjustPaddingFlat({ - ...paddingSettings, - pad, - }); - - const baseLabelTranslateY = - -labelHalfHeight - (topPosition + MINIMIZED_LABEL_Y_OFFSET); - - const { current: placeholderOpacityAnims } = React.useRef([ - new Animated.Value(0), - new Animated.Value(1), - ]); - - const placeholderOpacity = hasActiveOutline - ? parentState.labeled - : placeholderOpacityAnims[parentState.labelLayout.measured ? 1 : 0]; - - // We don't want to show placeholder if label is displayed, because they overlap. - // Before it was done by setting placeholder's value to " ", but inputs have the same props - // what causes broken styles due to: https://github.com/facebook/react-native/issues/48249 - const placeholderTextColorBasedOnState = parentState.displayPlaceholder - ? placeholderTextColor ?? placeholderColor - : 'transparent'; - - const minHeight = - height || - (dense ? (label ? MIN_DENSE_HEIGHT_WL : MIN_DENSE_HEIGHT) : MIN_HEIGHT); - - const flatHeight = - inputHeight + - (!height ? (dense ? LABEL_PADDING_TOP_DENSE : LABEL_PADDING_TOP) : 0); - - const iconTopPosition = (flatHeight - ADORNMENT_SIZE) / 2; - - const leftAffixTopPosition = leftLayout.height - ? calculateFlatAffixTopPosition({ - height: flatHeight, - ...paddingFlat, - affixHeight: leftLayout.height, - }) - : null; - - const rightAffixTopPosition = rightLayout.height - ? calculateFlatAffixTopPosition({ - height: flatHeight, - ...paddingFlat, - affixHeight: rightLayout.height, - }) - : null; - - const labelProps = { - label, - onLayoutAnimatedText, - onLabelTextLayout, - placeholderOpacity, - labelError: error, - placeholderStyle: styles.placeholder, - baseLabelTranslateY, - baseLabelTranslateX, - font, - fontSize, - lineHeight, - fontWeight, - labelScale, - wiggleOffsetX: LABEL_WIGGLE_X_OFFSET, - topPosition, - paddingLeft: isAndroid - ? I18nManager.isRTL - ? paddingRight - : paddingLeft - : paddingLeft, - paddingRight: isAndroid - ? I18nManager.isRTL - ? paddingLeft - : paddingRight - : paddingRight, - hasActiveOutline, - activeColor, - placeholderColor, - disabledOpacity, - errorColor, - roundness, - maxFontSizeMultiplier: rest.maxFontSizeMultiplier, - testID, - contentStyle, - inputContainerLayout: parentState.inputContainerLayout, - labelTextLayout: parentState.labelTextLayout, - opacity: - parentState.value || parentState.focused - ? parentState.labelLayout.measured - ? 1 - : 0 - : 1, - }; - - const affixTopPosition = { - [AdornmentSide.Left]: leftAffixTopPosition, - [AdornmentSide.Right]: rightAffixTopPosition, - }; - const onAffixChange = { - [AdornmentSide.Left]: onLeftAffixLayoutChange, - [AdornmentSide.Right]: onRightAffixLayoutChange, - }; - - let adornmentProps: TextInputAdornmentProps = { - paddingHorizontal, - adornmentConfig, - forceFocus, - topPosition: { - [AdornmentType.Affix]: affixTopPosition, - [AdornmentType.Icon]: iconTopPosition, - }, - onAffixChange, - isTextInputFocused: parentState.focused, - maxFontSizeMultiplier: rest.maxFontSizeMultiplier, - disabled, - }; - if (adornmentConfig.length) { - adornmentProps = { - ...adornmentProps, - left, - right, - textStyle: { ...font, fontSize, lineHeight, fontWeight }, - visible: parentState.labeled, - }; - } - - return ( - - - - {!isAndroid && multiline && !!label && !disabled && ( - // Workaround for: https://github.com/callstack/react-native-paper/issues/2799 - // Patch for a multiline TextInput with fixed height, which allow to avoid covering input label with its value. - - )} - {label ? ( - - ) : null} - {render?.({ - ...rest, - ref: innerRef, - onChangeText, - placeholder: rest.placeholder, - editable: !disabled && editable, - selectionColor, - cursorColor: - typeof cursorColor === 'undefined' ? activeColor : cursorColor, - placeholderTextColor: placeholderTextColorBasedOnState, - onFocus, - onBlur, - underlineColorAndroid: 'transparent', - multiline, - style: [ - styles.input, - multiline && height ? { height: flatHeight } : {}, - paddingFlat, - { - paddingLeft, - paddingRight, - ...font, - fontSize, - lineHeight, - fontWeight, - color: inputTextColor, - opacity: disabledOpacity, - textAlignVertical: multiline ? 'top' : 'center', - textAlign: textAlign - ? textAlign - : I18nManager.getConstants().isRTL - ? 'right' - : 'left', - minWidth: Math.min( - parentState.labelTextLayout.width + 2 * FLAT_INPUT_OFFSET, - MIN_WIDTH - ), - }, - Platform.OS === 'web' ? { outline: 'none' } : undefined, - adornmentStyleAdjustmentForNativeInput, - contentStyle, - ], - testID, - })} - - - - ); -}; - -export default TextInputFlat; - -const styles = StyleSheet.create({ - placeholder: { - position: 'absolute', - left: 0, - }, - labelContainer: { - paddingTop: 0, - paddingBottom: 0, - flexGrow: 1, - }, - input: { - margin: 0, - flexGrow: 1, - }, - inputFlat: { - paddingTop: 24, - paddingBottom: 4, - }, - inputFlatDense: { - paddingTop: 22, - paddingBottom: 2, - }, - patchContainer: { - height: 24, - zIndex: 2, - }, - densePatchContainer: { - height: 22, - zIndex: 2, - }, -}); diff --git a/src/components/TextInput/TextInputOutlined.tsx b/src/components/TextInput/TextInputOutlined.tsx deleted file mode 100644 index 63efb7f14b..0000000000 --- a/src/components/TextInput/TextInputOutlined.tsx +++ /dev/null @@ -1,449 +0,0 @@ -import * as React from 'react'; -import { - Animated, - View, - TextInput as NativeTextInput, - StyleSheet, - I18nManager, - Platform, - TextStyle, - ColorValue, - LayoutChangeEvent, -} from 'react-native'; - -import { Outline } from './Addons/Outline'; -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: customSelectionColor, - cursorColor, - underlineColor: _underlineColor, - outlineColor: customOutlineColor, - activeOutlineColor, - outlineStyle, - textColor, - dense, - style, - theme, - render = (props: RenderProps) => , - multiline = false, - parentState, - innerRef, - onFocus, - forceFocus, - onBlur, - onChangeText, - onLayoutAnimatedText, - onLabelTextLayout, - onLeftAffixLayoutChange, - onRightAffixLayoutChange, - onInputLayout, - onLayout, - left, - right, - placeholderTextColor, - testID = 'text-input-outlined', - contentStyle, - scaledLabel, - ...rest -}: ChildTextInputProps) => { - const adornmentConfig = getAdornmentConfig({ left, right }); - - const { colors, roundness } = theme; - const font = theme.fonts.bodyLarge; - const hasActiveOutline = parentState.focused || error; - - const { INPUT_PADDING_HORIZONTAL, MIN_HEIGHT, ADORNMENT_OFFSET, MIN_WIDTH } = - getConstants(); - - const { - fontSize: fontSizeStyle, - fontWeight, - lineHeight: lineHeightStyle, - height, - backgroundColor = colors?.background, - textAlign, - ...viewStyle - } = (StyleSheet.flatten(style) || {}) as TextStyle; - const fontSize = fontSizeStyle || MAXIMIZED_LABEL_FONT_SIZE; - const lineHeight = - lineHeightStyle || (Platform.OS === 'web' ? fontSize * 1.2 : undefined); - - const { - inputTextColor, - activeColor, - disabledOpacity, - outlineColor, - placeholderColor, - errorColor, - selectionColor, - } = getOutlinedInputColors({ - activeOutlineColor, - customOutlineColor, - customSelectionColor, - textColor, - disabled, - error, - theme, - }); - - const densePaddingTop = label ? LABEL_PADDING_TOP_DENSE : 0; - const paddingTop = label ? LABEL_PADDING_TOP : 0; - const yOffset = label ? OUTLINE_MINIMIZED_LABEL_Y_OFFSET : 0; - - 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 - ); - const isAdornmentRightIcon = adornmentConfig.some( - ({ side, type }) => - side === AdornmentSide.Right && type === AdornmentType.Icon - ); - - if (isAdornmentLeftIcon) { - labelTranslationXOffset = - (I18nManager.getConstants().isRTL ? -1 : 1) * ADORNMENT_SIZE + - ADORNMENT_OFFSET; - } - - const minInputHeight = - (dense ? MIN_DENSE_HEIGHT_OUTLINED : MIN_HEIGHT) - paddingTop; - - const inputHeight = calculateInputHeight(labelHeight, height, minInputHeight); - - const topPosition = calculateLabelTopPosition( - labelHeight, - inputHeight, - paddingTop - ); - - 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: paddingTop, - 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 + yOffset); - - const { current: placeholderOpacityAnims } = React.useRef([ - new Animated.Value(0), - new Animated.Value(1), - ]); - - const placeholderOpacity = hasActiveOutline - ? parentState.labeled - : placeholderOpacityAnims[parentState.labelLayout.measured ? 1 : 0]; - - const placeholderStyle = { - position: 'absolute', - left: 0, - paddingHorizontal: INPUT_PADDING_HORIZONTAL, - }; - - const placeholderTextColorBasedOnState = parentState.displayPlaceholder - ? placeholderTextColor ?? placeholderColor - : 'transparent'; - - const labelBackgroundColor: ColorValue = - backgroundColor === 'transparent' - ? theme.colors.background - : backgroundColor; - - const labelProps = { - label, - onLayoutAnimatedText, - onLabelTextLayout, - placeholderOpacity, - labelError: error, - placeholderStyle, - baseLabelTranslateY, - baseLabelTranslateX, - font, - fontSize, - lineHeight, - fontWeight, - labelScale, - wiggleOffsetX: LABEL_WIGGLE_X_OFFSET, - topPosition, - hasActiveOutline, - activeColor, - placeholderColor, - disabledOpacity, - backgroundColor: labelBackgroundColor, - errorColor, - labelTranslationXOffset, - roundness, - maxFontSizeMultiplier: rest.maxFontSizeMultiplier, - testID, - contentStyle, - inputContainerLayout: { - width: - parentState.inputContainerLayout.width + - (isAdornmentRightIcon || isAdornmentLeftIcon - ? INPUT_PADDING_HORIZONTAL - : 0), - }, - opacity: - parentState.value || parentState.focused - ? parentState.labelLayout.measured - ? 1 - : 0 - : 1, - }; - - const onLayoutChange = React.useCallback( - (e: LayoutChangeEvent) => { - onInputLayout(e); - onLayout?.(e); - }, - [onLayout, onInputLayout] - ); - - const minHeight = (height || - (dense ? MIN_DENSE_HEIGHT_OUTLINED : MIN_HEIGHT)) as number; - - const outlinedHeight = - inputHeight + (dense ? densePaddingTop / 2 : paddingTop); - const { leftLayout, rightLayout } = parentState; - - const leftAffixTopPosition = calculateOutlinedIconAndAffixTopPosition({ - height: outlinedHeight, - affixHeight: leftLayout.height || 0, - labelYOffset: -yOffset, - }); - - const rightAffixTopPosition = calculateOutlinedIconAndAffixTopPosition({ - height: outlinedHeight, - affixHeight: rightLayout.height || 0, - labelYOffset: -yOffset, - }); - const iconTopPosition = calculateOutlinedIconAndAffixTopPosition({ - height: outlinedHeight, - affixHeight: ADORNMENT_SIZE, - labelYOffset: -yOffset, - }); - - 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', - }); - 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?.({ - ...rest, - ref: innerRef, - onLayout: onLayoutChange, - onChangeText, - placeholder: rest.placeholder, - editable: !disabled && editable, - selectionColor, - cursorColor: - typeof cursorColor === 'undefined' ? activeColor : cursorColor, - placeholderTextColor: placeholderTextColorBasedOnState, - onFocus, - onBlur, - underlineColorAndroid: 'transparent', - multiline, - style: [ - styles.input, - !multiline || (multiline && height) ? { height: inputHeight } : {}, - paddingOut, - { - ...font, - fontSize, - lineHeight, - fontWeight, - color: inputTextColor, - opacity: disabledOpacity, - textAlignVertical: multiline ? 'top' : 'center', - textAlign: textAlign - ? textAlign - : I18nManager.getConstants().isRTL - ? 'right' - : 'left', - paddingHorizontal: INPUT_PADDING_HORIZONTAL, - minWidth: Math.min( - parentState.labelTextLayout.width + - 2 * INPUT_PADDING_HORIZONTAL, - MIN_WIDTH - ), - }, - Platform.OS === 'web' ? { outline: 'none' } : undefined, - adornmentStyleAdjustmentForNativeInput, - contentStyle, - ], - testID, - } as RenderProps)} - - - - ); -}; - -export default TextInputOutlined; - -const styles = StyleSheet.create({ - labelContainer: { - paddingBottom: 0, - flexGrow: 1, - }, - input: { - margin: 0, - flexGrow: 1, - }, - inputOutlined: { - paddingTop: 8, - paddingBottom: 8, - }, - inputOutlinedDense: { - paddingTop: 4, - paddingBottom: 4, - }, -}); diff --git a/src/components/TextInput/constants.tsx b/src/components/TextInput/constants.tsx deleted file mode 100644 index d85d28e542..0000000000 --- a/src/components/TextInput/constants.tsx +++ /dev/null @@ -1,39 +0,0 @@ -export const MAXIMIZED_LABEL_FONT_SIZE = 16; -export const MINIMIZED_LABEL_FONT_SIZE = 12; -export const LABEL_WIGGLE_X_OFFSET = 4; - -export const ADORNMENT_SIZE = 24; -export const MIN_WIDTH = 100; - -//Text input affix offset -export const MD3_AFFIX_OFFSET = 16; - -// Text input icon -export const ICON_SIZE = 24; -export const MD3_ICON_OFFSET = 16; - -// Text input common -export const MD3_MIN_HEIGHT = 56; -export const MD3_ADORNMENT_OFFSET = 16; -export const LABEL_PADDING_TOP_DENSE = 24; -export const LABEL_PADDING_TOP = 8; - -// Text input flat -export const MD3_LABEL_PADDING_TOP = 26; - -export const MD3_LABEL_PADDING_HORIZONTAL = 16; - -export const MD3_FLAT_INPUT_OFFSET = 16; - -export const MINIMIZED_LABEL_Y_OFFSET = -18; -export const MIN_DENSE_HEIGHT_WL = 52; -export const MIN_DENSE_HEIGHT = 40; - -// Text input outlined -export const MD3_INPUT_PADDING_HORIZONTAL = 16; - -// extra space to avoid overlapping input's text and icon -export const MD3_OUTLINED_INPUT_OFFSET = 16; - -export const OUTLINE_MINIMIZED_LABEL_Y_OFFSET = -6; -export const MIN_DENSE_HEIGHT_OUTLINED = 48; diff --git a/src/components/TextInput/helpers.tsx b/src/components/TextInput/helpers.tsx deleted file mode 100644 index b1111b2a1a..0000000000 --- a/src/components/TextInput/helpers.tsx +++ /dev/null @@ -1,501 +0,0 @@ -import { AdornmentSide, AdornmentType } from './Adornment/enums'; -import type { AdornmentConfig } from './Adornment/types'; -import { - MIN_WIDTH, - ADORNMENT_SIZE, - MD3_ADORNMENT_OFFSET, - MD3_AFFIX_OFFSET, - MD3_FLAT_INPUT_OFFSET, - MD3_ICON_OFFSET, - MD3_INPUT_PADDING_HORIZONTAL, - MD3_LABEL_PADDING_HORIZONTAL, - MD3_LABEL_PADDING_TOP, - MD3_MIN_HEIGHT, - MD3_OUTLINED_INPUT_OFFSET, -} from './constants'; -import type { TextInputLabelProp } from './types'; -import { tokens } from '../../styles/themes/v3/tokens'; -import type { InternalTheme } from '../../types'; - -const { stateOpacity } = tokens.md.ref; - -type PaddingProps = { - height: number | null; - labelHalfHeight: number; - multiline: boolean | null; - dense: boolean | null; - topPosition: number; - fontSize: number; - lineHeight?: number; - label?: TextInputLabelProp | null; - scale: number; - offset: number; - isAndroid: boolean; - styles: { paddingTop: number; paddingBottom: number }; -}; - -type AdjProps = PaddingProps & { - pad: number; -}; - -export type Padding = { paddingTop: number; paddingBottom: number }; - -export const calculateLabelTopPosition = ( - labelHeight: number, - height: number = 0, - optionalPadding: number = 0 -): number => { - const customHeight = height > 0 ? height : 0; - - return Math.floor((customHeight - labelHeight) / 2 + optionalPadding); -}; - -export const calculateInputHeight = ( - labelHeight: number, - height: any = 0, - minHeight: number -): number => { - const finalHeight = height > 0 ? height : labelHeight; - - if (height > 0) return height; - return finalHeight < minHeight ? minHeight : finalHeight; -}; - -export const calculatePadding = (props: PaddingProps): number => { - const { height, multiline = false } = props; - - let result = 0; - - if (multiline) { - if (height && multiline) { - result = calculateTextAreaPadding(props); - } else { - result = calculateInputPadding(props); - } - } - - return Math.max(0, result); -}; - -const calculateTextAreaPadding = (props: PaddingProps) => { - const { dense } = props; - - return dense ? 10 : 20; -}; - -const calculateInputPadding = ({ - topPosition, - fontSize, - multiline, - scale, - dense, - offset, - isAndroid, -}: PaddingProps): number => { - const refFontSize = scale * fontSize; - let result = Math.floor(topPosition / 2); - - result = - result + - Math.floor((refFontSize - fontSize) / 2) - - (scale < 1 ? offset / 2 : 0); - - if (multiline && isAndroid) - result = Math.min(dense ? offset / 2 : offset, result); - - return result; -}; - -export const adjustPaddingOut = ({ - pad, - multiline, - label, - scale, - height, - fontSize, - lineHeight, - dense, - offset, - isAndroid, -}: AdjProps): Padding => { - const fontHeight = lineHeight ?? fontSize; - const refFontHeight = scale * fontSize; - let result = pad; - - if (!isAndroid && height && !multiline) { - return { - paddingTop: Math.max(0, (height - fontHeight) / 2), - paddingBottom: Math.max(0, (height - fontHeight) / 2), - }; - } - if (!isAndroid && multiline) { - if (dense) { - if (label) { - result += scale < 1 ? Math.min(offset, (refFontHeight / 2) * scale) : 0; - } else { - result += 0; - } - } - if (!dense) { - if (label) { - result += - scale < 1 - ? Math.min(offset, refFontHeight * scale) - : Math.min(offset / 2, refFontHeight * scale); - } else { - result += scale < 1 ? Math.min(offset / 2, refFontHeight * scale) : 0; - } - } - result = Math.floor(result); - } - return { paddingTop: result, paddingBottom: result }; -}; - -export const adjustPaddingFlat = ({ - pad, - scale, - multiline, - label, - height, - offset, - dense, - fontSize, - isAndroid, - styles, -}: AdjProps): Padding => { - let result = pad; - let topResult = result; - let bottomResult = result; - const { paddingTop, paddingBottom } = styles; - const refFontSize = scale * fontSize; - - if (!multiline) { - // do not modify padding if input is not multiline - if (label) { - // return const style for flat input with label - return { paddingTop, paddingBottom }; - } - // return pad for flat input without label - return { paddingTop: result, paddingBottom: result }; - } - - if (label) { - // add paddings passed from styles - topResult = paddingTop; - bottomResult = paddingBottom; - - // adjust top padding for iOS - if (!isAndroid) { - if (dense) { - topResult += - scale < 1 - ? Math.min(result, refFontSize * scale) - result / 2 - : Math.min(result, refFontSize * scale) - result / 2; - } - if (!dense) { - topResult += - scale < 1 - ? Math.min(offset / 2, refFontSize * scale) - : Math.min(result, refFontSize * scale) - offset / 2; - } - } - topResult = Math.floor(topResult); - } else { - if (height) { - // center text when height is passed - return { - paddingTop: Math.max(0, (height - fontSize) / 2), - paddingBottom: Math.max(0, (height - fontSize) / 2), - }; - } - // adjust paddings for iOS if no label - if (!isAndroid) { - if (dense) { - result += - scale < 1 - ? Math.min(offset / 2, (fontSize / 2) * scale) - : Math.min(offset / 2, scale); - } - if (!dense) { - result += - scale < 1 - ? Math.min(offset, fontSize * scale) - : Math.min(fontSize, (offset / 2) * scale); - } - - result = Math.floor(result); - topResult = result; - bottomResult = result; - } - } - - return { - paddingTop: Math.max(0, topResult), - paddingBottom: Math.max(0, bottomResult), - }; -}; - -export function calculateFlatAffixTopPosition({ - height, - paddingTop, - paddingBottom, - affixHeight, -}: { - height: number; - paddingTop: number; - paddingBottom: number; - affixHeight: number; -}): number { - const inputHeightWithoutPadding = height - paddingTop - paddingBottom; - - const halfOfTheInputHeightDecreasedByAffixHeight = - (inputHeightWithoutPadding - affixHeight) / 2; - - return paddingTop + halfOfTheInputHeightDecreasedByAffixHeight; -} - -export function calculateOutlinedIconAndAffixTopPosition({ - height, - affixHeight, - labelYOffset, -}: { - height: number; - affixHeight: number; - labelYOffset: number; -}): number { - return (height - affixHeight + labelYOffset) / 2; -} - -export const calculateFlatInputHorizontalPadding = ({ - adornmentConfig, -}: { - adornmentConfig: AdornmentConfig[]; -}) => { - const { LABEL_PADDING_HORIZONTAL, ADORNMENT_OFFSET, FLAT_INPUT_OFFSET } = - getConstants(); - - let paddingLeft = LABEL_PADDING_HORIZONTAL; - let paddingRight = LABEL_PADDING_HORIZONTAL; - - adornmentConfig.forEach(({ type, side }) => { - if (type === AdornmentType.Icon && side === AdornmentSide.Left) { - paddingLeft = ADORNMENT_SIZE + ADORNMENT_OFFSET + FLAT_INPUT_OFFSET; - } else if (side === AdornmentSide.Right) { - if (type === AdornmentType.Affix) { - paddingRight = ADORNMENT_SIZE + ADORNMENT_OFFSET + FLAT_INPUT_OFFSET; - } else if (type === AdornmentType.Icon) { - paddingRight = ADORNMENT_SIZE + ADORNMENT_OFFSET + FLAT_INPUT_OFFSET; - } - } - }); - - return { paddingLeft, paddingRight }; -}; - -type BaseProps = { - theme: InternalTheme; - disabled?: boolean; -}; - -type Mode = 'flat' | 'outlined'; - -const getInputTextColor = ({ - theme, - textColor, -}: BaseProps & { textColor?: string }) => { - if (textColor) { - return textColor; - } - - return theme.colors.onSurface; -}; - -const getActiveColor = ({ - theme, - error, - activeUnderlineColor, - activeOutlineColor, - mode, -}: BaseProps & { - error?: boolean; - activeUnderlineColor?: string; - activeOutlineColor?: string; - mode?: Mode; -}) => { - const isFlat = mode === 'flat'; - const modeColor = isFlat ? activeUnderlineColor : activeOutlineColor; - - if (error) { - return theme.colors.error; - } - - if (modeColor) { - return modeColor; - } - - return theme.colors.primary; -}; - -const getPlaceholderColor = ({ theme }: BaseProps) => { - return theme.colors.onSurfaceVariant; -}; - -const getSelectionColor = ({ - activeColor, - customSelectionColor, -}: { - activeColor: string; - customSelectionColor?: string; -}) => { - if (typeof customSelectionColor !== 'undefined') { - return customSelectionColor; - } - return activeColor; -}; - -const getFlatBackgroundColor = ({ theme, disabled }: BaseProps) => { - if (disabled) { - return theme.colors.surfaceContainerHighest; - } - - return theme.colors.surfaceVariant; -}; - -const getFlatUnderlineColor = ({ - theme, - disabled, - underlineColor, -}: BaseProps & { underlineColor?: string }) => { - if (!disabled && underlineColor) { - return underlineColor; - } - - return theme.colors.onSurfaceVariant; -}; - -const getOutlinedOutlineInputColor = ({ - theme, - disabled, - customOutlineColor, -}: BaseProps & { customOutlineColor?: string }) => { - if (!disabled && customOutlineColor) { - return customOutlineColor; - } - - if (disabled) { - if (theme.dark) { - return 'transparent'; - } - return theme.colors.outlineVariant; - } - - return theme.colors.outline; -}; - -export const getFlatInputColors = ({ - underlineColor, - activeUnderlineColor, - customSelectionColor, - textColor, - disabled, - error, - theme, -}: { - 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', - }); - - const disabledOpacity = disabled - ? stateOpacity.disabled - : stateOpacity.enabled; - - return { - inputTextColor: getInputTextColor({ - ...baseFlatColorProps, - textColor, - }), - activeColor, - disabledOpacity, - underlineColorCustom: getFlatUnderlineColor({ - ...baseFlatColorProps, - underlineColor, - }), - placeholderColor: getPlaceholderColor(baseFlatColorProps), - selectionColor: getSelectionColor({ activeColor, customSelectionColor }), - errorColor: theme.colors.error, - backgroundColor: getFlatBackgroundColor(baseFlatColorProps), - }; -}; - -export const getOutlinedInputColors = ({ - activeOutlineColor, - customOutlineColor, - customSelectionColor, - textColor, - disabled, - error, - theme, -}: { - 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', - }); - - const disabledOpacity = disabled - ? stateOpacity.disabled - : stateOpacity.enabled; - - return { - inputTextColor: getInputTextColor({ - ...baseOutlinedColorProps, - textColor, - }), - activeColor, - disabledOpacity, - outlineColor: getOutlinedOutlineInputColor({ - ...baseOutlinedColorProps, - customOutlineColor, - }), - placeholderColor: getPlaceholderColor(baseOutlinedColorProps), - selectionColor: getSelectionColor({ activeColor, customSelectionColor }), - errorColor: theme.colors.error, - }; -}; - -export const getConstants = () => { - return { - AFFIX_OFFSET: MD3_AFFIX_OFFSET, - ICON_OFFSET: MD3_ICON_OFFSET, - LABEL_PADDING_TOP: MD3_LABEL_PADDING_TOP, - LABEL_PADDING_HORIZONTAL: MD3_LABEL_PADDING_HORIZONTAL, - FLAT_INPUT_OFFSET: MD3_FLAT_INPUT_OFFSET, - MIN_HEIGHT: MD3_MIN_HEIGHT, - INPUT_PADDING_HORIZONTAL: MD3_INPUT_PADDING_HORIZONTAL, - ADORNMENT_OFFSET: MD3_ADORNMENT_OFFSET, - OUTLINED_INPUT_OFFSET: MD3_OUTLINED_INPUT_OFFSET, - MIN_WIDTH, - }; -}; diff --git a/src/components/TextInput/types.tsx b/src/components/TextInput/types.tsx deleted file mode 100644 index 49bc46e121..0000000000 --- a/src/components/TextInput/types.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import * as React from 'react'; -import type { - TextInput as NativeTextInput, - Animated, - TextStyle, - LayoutChangeEvent, - ColorValue, - StyleProp, - ViewProps, - ViewStyle, - NativeSyntheticEvent, - TextLayoutEventData, -} from 'react-native'; - -import type { $Omit, InternalTheme, ThemeProp } from './../../types'; - -export type TextInputLabelProp = string | React.ReactElement; - -type TextInputProps = React.ComponentPropsWithRef & { - mode?: 'flat' | 'outlined'; - left?: React.ReactNode; - right?: React.ReactNode; - disabled?: boolean; - label?: TextInputLabelProp; - placeholder?: string; - error?: boolean; - onChangeText?: Function; - selectionColor?: string; - cursorColor?: string; - underlineColor?: string; - activeUnderlineColor?: string; - outlineColor?: string; - activeOutlineColor?: string; - textColor?: string; - dense?: boolean; - multiline?: boolean; - numberOfLines?: number; - onFocus?: (args: any) => void; - onBlur?: (args: any) => void; - render?: (props: RenderProps) => React.ReactNode; - value?: string; - style?: StyleProp; - theme?: ThemeProp; - testID?: string; - contentStyle?: StyleProp; - outlineStyle?: StyleProp; - underlineStyle?: StyleProp; - scaledLabel?: boolean; -}; - -export type RenderProps = { - ref: (a?: NativeTextInput | null) => void; - onChangeText?: (a: string) => void; - placeholder?: string; - placeholderTextColor?: ColorValue; - editable?: boolean; - selectionColor?: string; - cursorColor?: string; - onFocus?: (args: any) => void; - onBlur?: (args: any) => void; - underlineColorAndroid?: string; - onLayout?: (args: any) => void; - style: any; - multiline?: boolean; - numberOfLines?: number; - value?: string; - adjustsFontSizeToFit?: boolean; - testID?: string; -}; -type TextInputTypesWithoutMode = $Omit; -export type State = { - labeled: Animated.Value; - error: Animated.Value; - focused: boolean; - displayPlaceholder: boolean; - value?: string; - labelTextLayout: { width: number }; - labelLayout: { measured: boolean; width: number; height: number }; - leftLayout: { height: number | null; width: number | null }; - rightLayout: { height: number | null; width: number | null }; - inputContainerLayout: { width: number }; - contentStyle?: StyleProp; -}; -export type ChildTextInputProps = { - parentState: State; - innerRef: (ref?: NativeTextInput | null) => void; - onFocus?: (args: any) => void; - onBlur?: (args: any) => void; - forceFocus: () => void; - onChangeText?: (value: string) => void; - onInputLayout: (event: LayoutChangeEvent) => void; - onLayoutAnimatedText: (args: any) => void; - onLabelTextLayout: (event: NativeSyntheticEvent) => void; - onLeftAffixLayoutChange: (event: LayoutChangeEvent) => void; - onRightAffixLayoutChange: (event: LayoutChangeEvent) => void; -} & $Omit & { theme: InternalTheme }; - -export type LabelProps = { - mode?: 'flat' | 'outlined'; - placeholderStyle: any; - placeholderOpacity: - | number - | Animated.Value - | Animated.AnimatedInterpolation; - baseLabelTranslateX: number; - baseLabelTranslateY: number; - wiggleOffsetX: number; - labelScale: number; - fontSize: number; - lineHeight?: number | undefined; - fontWeight: TextStyle['fontWeight']; - font: any; - topPosition: number; - paddingLeft?: number; - paddingRight?: number; - labelTranslationXOffset?: number; - placeholderColor: string | null; - disabledOpacity?: number; - backgroundColor?: ColorValue; - label?: TextInputLabelProp | null; - hasActiveOutline?: boolean | null; - activeColor: string; - errorColor?: string; - labelError?: boolean | null; - onLayoutAnimatedText: (args: any) => void; - onLabelTextLayout: (event: NativeSyntheticEvent) => void; - roundness: number; - maxFontSizeMultiplier?: number | undefined | null; - testID?: string; - contentStyle?: StyleProp; - theme?: ThemeProp; -}; -export type InputLabelProps = { - labeled: Animated.Value; - error: Animated.Value; - focused: boolean; - wiggle: boolean; - opacity: number; - labelLayoutMeasured: boolean; - labelLayoutWidth: number; - labelLayoutHeight: number; - inputContainerLayout: { width: number }; - labelBackground?: any; - maxFontSizeMultiplier?: number | undefined | null; - scaledLabel?: boolean; -} & LabelProps; - -export type LabelBackgroundProps = { - labelStyle: any; - labeled: Animated.Value; - labelLayoutWidth: number; - labelLayoutHeight: number; - maxFontSizeMultiplier?: number | undefined | null; - theme?: ThemeProp; -} & LabelProps; diff --git a/src/components/__tests__/HelperText.test.tsx b/src/components/__tests__/HelperText.test.tsx deleted file mode 100644 index ce16526776..0000000000 --- a/src/components/__tests__/HelperText.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { render } from '@testing-library/react-native'; - -import { getTheme } from '../../core/theming'; -import HelperText from '../HelperText/HelperText'; - -describe('HelperText', () => { - it('should have correct text color for info type', () => { - const { getByTestId } = render( - - Info: Maximum length is 100 characters - - ); - - expect(getByTestId('helper-text')).toHaveStyle({ - color: getTheme().colors.onSurfaceVariant, - }); - }); - - it('should have correct text color for error type', () => { - const { getByTestId } = render( - - Error: Only letters are allowed - - ); - - expect(getByTestId('helper-text')).toHaveStyle({ - color: getTheme().colors.error, - }); - }); -}); diff --git a/src/components/__tests__/TextInput.test.tsx b/src/components/__tests__/TextInput.test.tsx deleted file mode 100644 index 2fbebb326f..0000000000 --- a/src/components/__tests__/TextInput.test.tsx +++ /dev/null @@ -1,988 +0,0 @@ -/* eslint-disable react-native/no-inline-styles */ -import * as React from 'react'; -import { I18nManager, Platform, StyleSheet, Text, View } from 'react-native'; - -import { fireEvent, render } from '@testing-library/react-native'; - -import { DefaultTheme, getTheme, ThemeProvider } from '../../core/theming'; -import { red500 } from '../../styles/themes/v2/colors'; -import { tokens } from '../../styles/themes/v3/tokens'; -import { - getFlatInputColors, - getOutlinedInputColors, -} from '../TextInput/helpers'; -import TextInput, { Props } from '../TextInput/TextInput'; - -const { stateOpacity } = tokens.md.ref; - -const style = StyleSheet.create({ - inputStyle: { - color: red500, - }, - centered: { - textAlign: 'center', - }, - height: { - height: 100, - }, - lineHeight: { - lineHeight: 22, - }, - contentStyle: { - paddingLeft: 20, - }, -}); - -// Revert changes to Platform.OS automatically -const defaultPlatform = Platform.OS; -beforeEach(() => { - Platform.OS = defaultPlatform; -}); - -const affixTextValue = '/100'; -it('correctly renders left-side icon adornment, and right-side affix adornment', () => { - const { getByText, getByTestId, toJSON } = render( - { - console.log('!@# press left'); - }} - /> - } - right={ - - } - /> - ); - expect(getByText(affixTextValue)).toBeTruthy(); - expect(getByTestId('left-icon-adornment')).toBeTruthy(); - expect(getByTestId('right-affix-adornment')).toBeTruthy(); - expect(toJSON()).toMatchSnapshot(); -}); - -it('correctly renders left-side affix adornment, and right-side icon adornment', () => { - const { getByText, getByTestId, toJSON } = render( - - } - right={ - { - console.log('!@# press left'); - }} - /> - } - /> - ); - expect(getByText(affixTextValue)).toBeTruthy(); - expect(getByTestId('right-icon-adornment')).toBeTruthy(); - expect(getByTestId('left-affix-adornment')).toBeTruthy(); - expect(toJSON()).toMatchSnapshot(); -}); - -it('correctly applies default textAlign based on default RTL', () => { - const { toJSON } = render( - - ); - - expect(toJSON()).toMatchSnapshot(); -}); - -it('correctly applies textAlign center', () => { - const { toJSON } = render( - - ); - - expect(toJSON()).toMatchSnapshot(); -}); - -it('correctly applies cursorColor prop', () => { - const { toJSON } = render( - - ); - - expect(toJSON()).toMatchSnapshot(); -}); - -it('correctly applies height to multiline Outline TextInput', () => { - const { toJSON } = render( - - ); - - expect(toJSON()).toMatchSnapshot(); -}); - -it('correctly applies error state Outline TextInput', () => { - const { getByTestId } = render( - - ); - - const outline = getByTestId('text-input-outline'); - expect(outline).toHaveStyle({ borderWidth: 2 }); -}); - -it('correctly applies focused state Outline TextInput', () => { - const { getByTestId } = render( - - ); - - const outline = getByTestId('text-input-outline'); - expect(outline).toHaveStyle({ borderWidth: 2 }); - - fireEvent(getByTestId('text-input-outlined'), 'focus'); - - expect(outline).toHaveStyle({ borderWidth: 2 }); -}); - -it('contains patch spacing for flat input when ios, multiline and disabled', () => { - Platform.OS = 'ios'; - const { getByTestId } = render( - - ); - expect(getByTestId('patch-container')).toBeTruthy(); -}); - -it('correctly applies a component as the text label', () => { - const { toJSON } = render( - Flat input} - placeholder="Type something" - value={'Some test value'} - /> - ); - - expect(toJSON()).toMatchSnapshot(); -}); - -it('correctly applies paddingLeft from contentStyleProp', () => { - const { toJSON } = render( - - ); - - expect(toJSON()).toMatchSnapshot(); -}); - -it('renders label with correct color when active', () => { - const { getByTestId } = render( - - ); - - fireEvent(getByTestId('text-input-flat'), 'focus'); - - expect(getByTestId('text-input-flat-label-active')).toHaveStyle({ - color: getTheme().colors.primary, - }); -}); - -it('renders label with correct color when inactive', () => { - const { getByTestId } = render( - - ); - - expect(getByTestId('text-input-label-inactive')).toHaveStyle({ - color: getTheme().colors.onSurfaceVariant, - }); -}); - -it('renders input placeholder initially with transparent placeholderTextColor', () => { - const { getByTestId } = render( - - ); - - expect(getByTestId('text-input').props.placeholderTextColor).toBe( - 'transparent' - ); -}); - -it('correctly applies padding offset to input label on Android when RTL', () => { - Platform.OS = 'android'; - I18nManager.isRTL = true; - - const { getByTestId } = render( - - } - right={ - - } - /> - ); - - expect(getByTestId('text-input-flat-label-active')).toHaveStyle({ - paddingLeft: 56, - paddingRight: 16, - }); - - I18nManager.isRTL = false; -}); - -it('correctly applies padding offset to input label on Android when LTR', () => { - Platform.OS = 'android'; - - const { getByTestId } = render( - - } - right={ - - } - /> - ); - - expect(getByTestId('text-input-flat-label-active')).toHaveStyle({ - paddingLeft: 16, - paddingRight: 56, - }); -}); - -it('calls onLayout on right-side affix adornment', () => { - const onLayoutMock = jest.fn(); - const nativeEventMock = { - nativeEvent: { layout: { height: 100 } }, - }; - - const { getByTestId } = render( - } - /> - ); - fireEvent( - getByTestId('right-affix-adornment-text'), - 'onLayout', - nativeEventMock - ); - expect(onLayoutMock).toHaveBeenCalledWith(nativeEventMock); -}); - -(['outlined', 'flat'] as const).forEach((mode) => - it(`renders ${mode} input with correct line height`, () => { - const input = render( - - ); - - expect(input.getByTestId(`text-input-${mode}`)).toHaveStyle({ - lineHeight: 22, - }); - }) -); - -(['outlined', 'flat'] as const).forEach((mode) => - it(`renders ${mode} input with passed textColor`, () => { - const input = render( - - ); - - expect(input.getByTestId(`text-input-${mode}`)).toHaveStyle({ - color: 'purple', - }); - }) -); - -it("correctly applies theme background to label when input's background is transparent", () => { - const backgroundColor = 'transparent'; - const theme = { - ...DefaultTheme, - colors: { - ...DefaultTheme.colors, - background: 'pink', - }, - }; - - const { getByTestId } = render( - - - - ); - - expect(getByTestId('transparent-example-label-background')).toHaveStyle({ - backgroundColor: 'pink', - }); -}); - -it('always applies line height for web, even if not specified', () => { - Platform.OS = 'web'; - const { getByTestId } = render( - - - - - - - - - ); - - expect(getByTestId('default-font')).toHaveStyle({ lineHeight: 16 * 1.2 }); - expect(getByTestId('default-font-flat')).toHaveStyle({ - lineHeight: 16 * 1.2, - }); - - expect(getByTestId('large-font')).toHaveStyle({ lineHeight: 30 * 1.2 }); - expect(getByTestId('large-font-flat')).toHaveStyle({ lineHeight: 30 * 1.2 }); - - expect(getByTestId('custom-line-height')).toHaveStyle({ - lineHeight: 29, - }); - expect(getByTestId('custom-line-height-flat')).toHaveStyle({ - lineHeight: 29, - }); -}); - -it('call onPress when affix adornment pressed', () => { - const affixOnPress = jest.fn(); - const affixTextValue = '+39'; - const { getByText, toJSON } = render( - } - /> - ); - - fireEvent.press(getByText(affixTextValue)); - - expect(getByText(affixTextValue)).toBeTruthy(); - expect(toJSON()).toMatchSnapshot(); - expect(affixOnPress).toHaveBeenCalledTimes(1); -}); - -describe('maxFontSizeMultiplier', () => { - const createInput = ( - type: Exclude, - maxFontSizeMultiplier?: Props['maxFontSizeMultiplier'] - ) => { - return ( - - ); - }; - - it('should have default value in flat input', () => { - const { getByTestId } = render(createInput('flat')); - - expect(getByTestId('text-input-flat').props.maxFontSizeMultiplier).toBe( - 1.5 - ); - }); - - it('should have default value in outlined input', () => { - const { getByTestId } = render(createInput('outlined')); - - expect(getByTestId('text-input-outlined').props.maxFontSizeMultiplier).toBe( - 1.5 - ); - }); - - it('should have correct passed value in flat input', () => { - const { getByTestId } = render(createInput('flat', 2)); - - expect(getByTestId('text-input-flat').props.maxFontSizeMultiplier).toBe(2); - }); - - it('should have correct passed value in outlined input', () => { - const { getByTestId } = render(createInput('outlined', 2)); - - expect(getByTestId('text-input-outlined').props.maxFontSizeMultiplier).toBe( - 2 - ); - }); - - it('should have passed null value in flat input', () => { - const { getByTestId } = render(createInput('flat', null)); - - expect(getByTestId('text-input-flat').props.maxFontSizeMultiplier).toBe( - null - ); - }); - - it('should have passed null value in outlined input', () => { - const { getByTestId } = render(createInput('outlined', null)); - - expect(getByTestId('text-input-outlined').props.maxFontSizeMultiplier).toBe( - null - ); - }); -}); - -describe('getFlatInputColor - underline color', () => { - it('should return correct disabled color, for theme version 3', () => { - expect( - getFlatInputColors({ - disabled: true, - theme: getTheme(), - }) - ).toMatchObject({ - underlineColorCustom: getTheme().colors.onSurfaceVariant, - }); - }); - - it('should return correct theme color, for theme version 3', () => { - expect( - getFlatInputColors({ - theme: getTheme(), - }) - ).toMatchObject({ - underlineColorCustom: getTheme().colors.onSurfaceVariant, - }); - }); - - it('should return custom color, no matter what the theme is', () => { - expect( - getFlatInputColors({ - underlineColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - underlineColorCustom: 'beige', - }); - - expect( - getFlatInputColors({ - underlineColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - underlineColorCustom: 'beige', - }); - }); -}); - -describe('getFlatInputColor - input text color', () => { - it('should return custom color, if not disabled, no matter what the theme is', () => { - expect( - getOutlinedInputColors({ - textColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - inputTextColor: 'beige', - }); - - expect( - getOutlinedInputColors({ - textColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - inputTextColor: 'beige', - }); - }); - - it('should return correct disabled color, for theme version 3', () => { - expect( - getFlatInputColors({ - disabled: true, - theme: getTheme(), - }) - ).toMatchObject({ - inputTextColor: getTheme().colors.onSurface, - disabledOpacity: stateOpacity.disabled, - }); - }); - - it('should return correct theme color, for theme version 3', () => { - expect( - getFlatInputColors({ - theme: getTheme(), - }) - ).toMatchObject({ - inputTextColor: getTheme().colors.onSurface, - }); - }); -}); - -describe('getFlatInputColor - placeholder color', () => { - it('should return correct disabled color', () => { - expect( - getFlatInputColors({ - disabled: true, - theme: getTheme(), - }) - ).toMatchObject({ - placeholderColor: getTheme().colors.onSurfaceVariant, - disabledOpacity: stateOpacity.disabled, - }); - }); - - it('should return correct theme color', () => { - expect( - getFlatInputColors({ - theme: getTheme(), - }) - ).toMatchObject({ - placeholderColor: getTheme().colors.onSurfaceVariant, - }); - }); -}); - -describe('getFlatInputColor - background color', () => { - it('should return correct disabled color, for theme version 3', () => { - expect( - getFlatInputColors({ - disabled: true, - theme: getTheme(), - }) - ).toMatchObject({ - backgroundColor: getTheme().colors.surfaceContainerHighest, - }); - expect( - getFlatInputColors({ - disabled: true, - theme: getTheme(true), - }) - ).toMatchObject({ - backgroundColor: getTheme(true).colors.surfaceContainerHighest, - }); - }); - - it('should return correct theme color, for theme version 3', () => { - expect( - getFlatInputColors({ - theme: getTheme(), - }) - ).toMatchObject({ - backgroundColor: getTheme().colors.surfaceVariant, - }); - }); -}); - -describe('getFlatInputColor - error color', () => { - it('should return correct error color, no matter what the theme is', () => { - expect( - getFlatInputColors({ - error: true, - theme: getTheme(), - }) - ).toMatchObject({ - errorColor: getTheme().colors.error, - }); - - expect( - getFlatInputColors({ - error: true, - theme: getTheme(), - }) - ).toMatchObject({ - errorColor: getTheme().colors.error, - }); - }); -}); - -describe('getFlatInputColor - active color', () => { - it('should return disabled color, for theme version 3', () => { - expect( - getFlatInputColors({ - disabled: true, - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: getTheme().colors.primary, - disabledOpacity: stateOpacity.disabled, - }); - }); - - it('should return correct active color, if error, no matter what the theme is', () => { - expect( - getFlatInputColors({ - error: true, - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: getTheme().colors.error, - }); - - expect( - getFlatInputColors({ - error: true, - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: getTheme().colors.error, - }); - }); - - it('should return custom active color, no matter what the theme is', () => { - expect( - getFlatInputColors({ - activeUnderlineColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: 'beige', - }); - - expect( - getFlatInputColors({ - activeUnderlineColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: 'beige', - }); - }); - - it('should return theme active color, for theme version 3', () => { - expect( - getFlatInputColors({ - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: getTheme().colors.primary, - }); - }); -}); - -describe('getOutlinedInputColors - outline color', () => { - it('should return correct disabled color, for theme version 3, light theme', () => { - expect( - getOutlinedInputColors({ - disabled: true, - theme: getTheme(), - }) - ).toMatchObject({ - outlineColor: getTheme().colors.outlineVariant, - }); - }); - - it('should return correct disabled color, for theme version 3, dark theme', () => { - expect( - getOutlinedInputColors({ - disabled: true, - theme: getTheme(true), - }) - ).toMatchObject({ - outlineColor: 'transparent', - }); - }); - - it('should return custom color, if not disabled, no matter what the theme is', () => { - expect( - getOutlinedInputColors({ - customOutlineColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - outlineColor: 'beige', - }); - - expect( - getOutlinedInputColors({ - customOutlineColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - outlineColor: 'beige', - }); - }); - - it('should return theme color, for theme version 3', () => { - expect( - getOutlinedInputColors({ - theme: getTheme(), - }) - ).toMatchObject({ - outlineColor: getTheme().colors.outline, - }); - }); -}); - -describe('getOutlinedInputColors - input text color', () => { - it('should return correct disabled color, for theme version 3', () => { - expect( - getOutlinedInputColors({ - disabled: true, - theme: getTheme(), - }) - ).toMatchObject({ - inputTextColor: getTheme().colors.onSurface, - disabledOpacity: stateOpacity.disabled, - }); - }); - - it('should return correct theme color, for theme version 3', () => { - expect( - getOutlinedInputColors({ - theme: getTheme(), - }) - ).toMatchObject({ - inputTextColor: getTheme().colors.onSurface, - }); - }); -}); - -describe('getOutlinedInputColors - placeholder color', () => { - it('should return correct disabled color, for theme version 3', () => { - expect( - getOutlinedInputColors({ - disabled: true, - theme: getTheme(), - }) - ).toMatchObject({ - placeholderColor: getTheme().colors.onSurfaceVariant, - disabledOpacity: stateOpacity.disabled, - }); - }); - - it('should return correct theme color, for theme version 3', () => { - expect( - getOutlinedInputColors({ - theme: getTheme(), - }) - ).toMatchObject({ - placeholderColor: getTheme().colors.onSurfaceVariant, - }); - }); -}); - -describe('getOutlinedInputColors - error color', () => { - it('should return correct error color, no matter what the theme is', () => { - expect( - getOutlinedInputColors({ - error: true, - theme: getTheme(), - }) - ).toMatchObject({ - errorColor: getTheme().colors.error, - }); - - expect( - getOutlinedInputColors({ - error: true, - theme: getTheme(), - }) - ).toMatchObject({ - errorColor: getTheme().colors.error, - }); - }); -}); - -describe('getOutlinedInputColors - active color', () => { - it('should return disabled color, for theme version 3', () => { - expect( - getOutlinedInputColors({ - disabled: true, - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: getTheme().colors.primary, - disabledOpacity: stateOpacity.disabled, - }); - }); - - it('should return correct active color, if error, no matter what the theme is', () => { - expect( - getOutlinedInputColors({ - error: true, - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: getTheme().colors.error, - }); - - expect( - getOutlinedInputColors({ - error: true, - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: getTheme().colors.error, - }); - }); - - it('should return custom active color, no matter what the theme is', () => { - expect( - getOutlinedInputColors({ - activeOutlineColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: 'beige', - }); - - expect( - getOutlinedInputColors({ - activeOutlineColor: 'beige', - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: 'beige', - }); - }); - - it('should return theme active color, for theme version 3', () => { - expect( - getOutlinedInputColors({ - theme: getTheme(), - }) - ).toMatchObject({ - activeColor: getTheme().colors.primary, - }); - }); -}); - -describe('outlineStyle - underlineStyle', () => { - it('correctly applies outline style', () => { - const { getByTestId } = render( - - ); - - expect(getByTestId('text-input-outline')).toHaveStyle({ - borderRadius: 16, - borderWidth: 6, - }); - }); - - it('correctly applies underline style', () => { - const { getByTestId } = render( - - ); - - expect(getByTestId('text-input-underline')).toHaveStyle({ - borderRadius: 16, - borderWidth: 6, - }); - }); -}); diff --git a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap deleted file mode 100644 index 0a7334fa12..0000000000 --- a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap +++ /dev/null @@ -1,2613 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`call onPress when affix adornment pressed 1`] = ` - - - - - - - - Flat input - - - Flat input - - - - - - - - - - +39 - - - - -`; - -exports[`correctly applies a component as the text label 1`] = ` - - - - - - - - - Flat input - - - - - Flat input - - - - - - - - -`; - -exports[`correctly applies cursorColor prop 1`] = ` - - - - - - - - Flat input - - - Flat input - - - - - - - -`; - -exports[`correctly applies default textAlign based on default RTL 1`] = ` - - - - - - - - Flat input - - - Flat input - - - - - - - -`; - -exports[`correctly applies height to multiline Outline TextInput 1`] = ` - - - - - - - - - Outline Input - - - Outline Input - - - - - - - -`; - -exports[`correctly applies paddingLeft from contentStyleProp 1`] = ` - - - - - - - - With padding - - - With padding - - - - - - - -`; - -exports[`correctly applies textAlign center 1`] = ` - - - - - - - - Flat input - - - Flat input - - - - - - - -`; - -exports[`correctly renders left-side affix adornment, and right-side icon adornment 1`] = ` - - - - - - - - Flat input - - - Flat input - - - - - - - - - /100 - - - - - - - - - heart - - - - - - - -`; - -exports[`correctly renders left-side icon adornment, and right-side affix adornment 1`] = ` - - - - - - - - Flat input - - - Flat input - - - - - - - - - - - - - heart - - - - - - - - - /100 - - - -`; diff --git a/src/index.tsx b/src/index.tsx index b17bb919ca..e48809cd3c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -37,7 +37,6 @@ export { default as Dialog } from './components/Dialog/Dialog'; export { default as Divider } from './components/Divider'; export { default as FAB } from './components/FAB'; export { default as AnimatedFAB } from './components/FAB/AnimatedFAB'; -export { default as HelperText } from './components/HelperText/HelperText'; export { default as Icon } from './components/Icon'; export { default as IconButton } from './components/IconButton/IconButton'; export { default as Menu } from './components/Menu/Menu'; @@ -51,7 +50,6 @@ export { default as Surface } from './components/Surface'; export { default as Switch } from './components/Switch/Switch'; export { default as Appbar } from './components/Appbar'; export { default as TouchableRipple } from './components/TouchableRipple/TouchableRipple'; -export { default as TextInput } from './components/TextInput/TextInput'; export { default as TextField } from './components/TextField'; export { default as ToggleButton } from './components/ToggleButton'; export { default as SegmentedButtons } from './components/SegmentedButtons/SegmentedButtons'; @@ -105,7 +103,6 @@ export type { Props as DrawerItemProps } from './components/Drawer/DrawerItem'; export type { Props as DrawerSectionProps } from './components/Drawer/DrawerSection'; export type { Props as FABProps } from './components/FAB/FAB'; export type { Props as FABGroupProps } from './components/FAB/FABGroup'; -export type { Props as HelperTextProps } from './components/HelperText/HelperText'; export type { Props as IconButtonProps } from './components/IconButton/IconButton'; export type { Props as ListAccordionProps } from './components/List/ListAccordion'; export type { Props as ListAccordionGroupProps } from './components/List/ListAccordionGroup'; @@ -129,9 +126,6 @@ export type { Props as SearchbarProps } from './components/Searchbar'; export type { Props as SnackbarProps } from './components/Snackbar'; export type { Props as SurfaceProps } from './components/Surface'; export type { Props as SwitchProps } from './components/Switch/Switch'; -export type { Props as TextInputProps } from './components/TextInput/TextInput'; -export type { Props as TextInputAffixProps } from './components/TextInput/Adornment/TextInputAffix'; -export type { Props as TextInputIconProps } from './components/TextInput/Adornment/TextInputIcon'; export type { TextFieldProps, TextFieldAccessoryProps, From 0d166920b8ca5ed2b20cb2728ba1f8dfe2aba6c2 Mon Sep 17 00:00:00 2001 From: Michal Lul Date: Tue, 12 May 2026 13:05:29 +0200 Subject: [PATCH 2/2] chore: migration guide update --- docs/docs/guides/12-migration.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/docs/guides/12-migration.md b/docs/docs/guides/12-migration.md index 5f32d6d2d7..5fae83d943 100644 --- a/docs/docs/guides/12-migration.md +++ b/docs/docs/guides/12-migration.md @@ -31,7 +31,7 @@ import { TextField, type TextFieldProps } from 'react-native-paper'; - **`left` / `right`** → **`StartAccessory` / `EndAccessory`** - **`TextInput.Icon`** → **`TextField.Icon`** -- **`TextInput.Affix`** → **`prefix` / `suffix`**, or **`TextInput.Icon`**, or **`StartAccessory` / `EndAccessory`** +- **`TextInput.Affix`** → **`prefix` / `suffix`**, or **`TextField.Icon`**, or **`StartAccessory` / `EndAccessory`** ```tsx // Before (v5) @@ -54,7 +54,7 @@ import { TextField, type TextFieldProps } from 'react-native-paper'; ## Label, helper, error, disabled - **`label: React.Element | string`** → **`string`** -- **`error` / `disabled`** → **`status="error"` / `status="disabled"`** or **`status={['error','disabled']}`** when both apply. +- **`error` / `disabled`** → **`error`** and **`editable={false}`** - **`HelperText`** was removed; use **`supportingText`**. ```tsx @@ -74,7 +74,8 @@ import { TextField, type TextFieldProps } from 'react-native-paper'; ``` @@ -83,14 +84,14 @@ import { TextField, type TextFieldProps } from 'react-native-paper'; No direct `TextField` equivalents for: -- **`dense`**, **`contentStyle`**, **`outlineStyle`**, **`underlineStyle`** +- **`dense`**, **`contentStyle`**, **`underlineStyle`** - **`underlineColor`**, **`activeUnderlineColor`**, **`outlineColor`**, **`activeOutlineColor`**, **`textColor`** - **`render`** -Prefer **`fieldStyle`**, **`containerStyle`**, **`pressableStyle`**, **`style`** on the inner input, and the theme. +Prefer **`fieldStyle`**, **`containerStyle`**, **`pressableStyle`**, **`style`** on the inner input, and the theme for the rest. Use **`outlineStyle`** for the indicator outline. ```tsx -import { MD3LightTheme, TextField, TextInput } from 'react-native-paper'; +import { MD3LightTheme, TextField } from 'react-native-paper'; const theme = { ...MD3LightTheme, @@ -127,7 +128,7 @@ const theme = { paddingTop: 8, paddingBottom: 8, }} - fieldStyle={{ + outlineStyle={{ borderRadius: 12, borderWidth: 2, }}