Skip to content

Commit 4308e45

Browse files
Merge branch 'textfields' into 'main'
Textfields See merge request react-native/react-native-material-components!21
2 parents 056a566 + 1f9478f commit 4308e45

14 files changed

+627
-0
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,45 @@ export const MyScreen = () => {
11661166
![snackbar with icon](https://ik.imagekit.io/Computools/rn-material-components/snackbar-with-icon.png?updatedAt=1704887400512)
11671167
![snackbar gif](https://ik.imagekit.io/Computools/rn-material-components/snackbar-gif.gif?updatedAt=1704887530020)
11681168
</details>
1169+
<details><summary>Text Inputs</summary>
1170+
<br />
1171+
1172+
**Properties**
1173+
1174+
| name | description | type | default |
1175+
| ------ | ------ | ------ | ---- |
1176+
| label | Required | string | - |
1177+
| disabled | - | boolean | - |
1178+
| errorText | - | string | - |
1179+
| suportingText | - | string | - |
1180+
| leadingIcon | - | React.FC<T> | - |
1181+
| trailingIcon | - | React.FC<T> | - |
1182+
| leadingIconProps | - | T | - |
1183+
| trailingIconProps | - | T | - |
1184+
| leadingComponent | - | ReactNode | - |
1185+
| trailingComponent | - | ReactNode | - |
1186+
| labelStyle | - | ViewStyle | - |
1187+
| supportingTextStyle | - | TextStyle | - |
1188+
| innerContainerStyle | - | ViewStyle | - |
1189+
| outerContainerStyle | - | ViewStyle | - |
1190+
| activeIndicatorStyle | - | ViewStyle | - |
1191+
| onOuterContainerLayout | - | (e: LayoutChangeEvent) => void | - |
1192+
1193+
<details><summary>Filled Input</summary>
1194+
<br />
1195+
1196+
![filled text input](https://ik.imagekit.io/Computools/rn-material-components/filled_text_input.png?updatedAt=1736357640156)
1197+
![filled text input animation](https://ik.imagekit.io/Computools/rn-material-components/filled_text_input.gif?updatedAt=1736357640313)
1198+
1199+
</deatils>
1200+
<details><summary>Outlined Input</summary>
1201+
<br />
1202+
1203+
![outlined text input](https://ik.imagekit.io/Computools/rn-material-components/outlined_text_input.png?updatedAt=1736357640133)
1204+
![outlined text input animation](https://ik.imagekit.io/Computools/rn-material-components/outlined-text-input.gif?updatedAt=1736357640468)
1205+
1206+
</deatils>
1207+
</deatils>
11691208
</deatils>
11701209

11711210
## Contributing
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import Svg, {Path} from 'react-native-svg';
3+
4+
import {path} from './path.json';
5+
import type {IconProps} from '../icon-props';
6+
7+
const DEFAULT_SIZE = 24;
8+
const DEFAULT_COLOR = '#000';
9+
10+
export const ErrorIcon: React.FC<IconProps> = ({color = DEFAULT_COLOR, size = DEFAULT_SIZE, ...props}) => (
11+
<Svg viewBox="0 0 24 24" width={size} height={size} fill={'none'} {...props}>
12+
<Path fill={color} d={path} />
13+
</Svg>
14+
);

src/icons/error-icon/path.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"path": "M12 17c0.283 0 0.521-0.096 0.712-0.288s0.288-0.429 0.288-0.712c0-0.283-0.096-0.521-0.288-0.713s-0.429-0.288-0.712-0.288-0.521 0.096-0.712 0.288c-0.192 0.192-0.288 0.429-0.288 0.713s0.096 0.521 0.288 0.712c0.192 0.192 0.429 0.288 0.712 0.288zM11 13h2v-6h-2v6zM12 22c-1.383 0-2.683-0.262-3.9-0.787s-2.275-1.238-3.175-2.138c-0.9-0.9-1.612-1.958-2.137-3.175s-0.788-2.517-0.788-3.9c0-1.383 0.262-2.683 0.787-3.9s1.238-2.275 2.137-3.175c0.9-0.9 1.958-1.612 3.175-2.137s2.517-0.788 3.9-0.788c1.383 0 2.683 0.262 3.9 0.787s2.275 1.237 3.175 2.138c0.9 0.9 1.613 1.958 2.138 3.175s0.788 2.517 0.788 3.9c0 1.383-0.263 2.683-0.788 3.9s-1.237 2.275-2.138 3.175-1.958 1.613-3.175 2.138c-1.217 0.525-2.517 0.787-3.9 0.787z"
3+
}

src/icons/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export {PlusIcon} from './plus-icon/PlusIcon.component';
44
export {PhotoIcon} from './photo-icon/PhotoIcon.component';
55
export {TodayIcon} from './today-icon/TodayIcon.component';
66
export {CloseIcon} from './close-icon/CloseIcon.component';
7+
export {ErrorIcon} from './error-icon/ErrorIcon.component';
78
export {DeleteIcon} from './delete-icon/DeleteIcon.component';
89
export {SearchIcon} from './search-icon/SearchIcon.component';
910
export {CommuteIcon} from './commute-icon/CommuteIcon.component';

src/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export {SuggestionChip, type SuggestionChipProps} from './chips/suggestion-chip/
1616
export {PrimaryTabs, type PrimaryTabsProps} from './navigation/tabs/primary-tabs/PrimaryTabs.component';
1717
export {SecondaryTabs, type SecondaryTabsProps} from './navigation/tabs/secondary-tabs/SecondaryTabs.component';
1818

19+
export {FilledTextInput, type FilledTextInputProps} from './text-inputs/filled-text-input/FilledTextInput.component';
20+
export {OutlinedTextInput, type OutlinedTextInputProps} from './text-inputs/outlined-text-input/OutlinedTextInput.component';
21+
1922
export {
2023
CenterAlignedTopAppBar,
2124
type CenterAlignedTopAppBarProps,
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import React, {forwardRef, useRef} from 'react';
2+
import Animated, {useAnimatedStyle, interpolateColor, interpolate} from 'react-native-reanimated';
3+
import {Text, View, TextInput, TouchableWithoutFeedback, type StyleProp, type ViewStyle} from 'react-native';
4+
5+
import {ErrorIcon} from '../../icons';
6+
import {styles} from './filled-text-input.styles';
7+
import {type IconProps} from '../../icons/icon-props';
8+
import type {TextInputProps} from '../text-input.types';
9+
import {useTextInputColors} from '../use-text-input-colors.hook';
10+
import {useTextInputFocus} from '../use-text-input-focus-anim.hook';
11+
import {useTypography} from '../../typography/useTypography.component';
12+
13+
const UNFOCUSED_LABEL_TOP_PLACEMENT = 8;
14+
const DEFAULT_LABEL_SMALL_FONT_SIZE = 12;
15+
const DEFAULT_LABEL_LARGE_FONT_SIZE = 16;
16+
const ACTIVE_INDICATOR_FOCUSED_HEIGHT = 3;
17+
const ACTIVE_INDICATOR_UNFOCUSED_HEIGHT = 1;
18+
19+
export interface FilledTextInputProps<T> extends TextInputProps<T> {
20+
activeIndicatorStyle?: StyleProp<ViewStyle>;
21+
}
22+
23+
export const FilledTextInput = forwardRef(
24+
<T extends IconProps>(
25+
{
26+
label,
27+
placeholder,
28+
29+
errorText,
30+
suportingText,
31+
disabled = false,
32+
33+
leadingIcon,
34+
trailingIcon,
35+
leadingIconProps = {} as T,
36+
trailingIconProps = {} as T,
37+
38+
leadingComponent,
39+
trailingComponent,
40+
41+
style,
42+
labelStyle,
43+
outerContainerStyle,
44+
innerContainerStyle,
45+
supportingTextStyle,
46+
activeIndicatorStyle,
47+
48+
onFocus,
49+
onBlur,
50+
onOuterContainerLayout,
51+
...props
52+
}: FilledTextInputProps<T>,
53+
ref: React.Ref<TextInput>
54+
) => {
55+
const {bodyLarge, bodySmall} = useTypography();
56+
// eslint-disable-next-line react-hooks/rules-of-hooks
57+
const inputRef = (ref as React.MutableRefObject<TextInput>) || useRef<TextInput>(null);
58+
const {
59+
valueColor,
60+
containerColor,
61+
selectionColor,
62+
placeholderColor,
63+
labelFocusedColor,
64+
trailingIconColor,
65+
leadingIconColor,
66+
labelUnfocusedColor,
67+
supportingTextColor,
68+
activeIndicatorFocusedColor,
69+
activeIndicatorUnfocusedColor,
70+
} = useTextInputColors({
71+
disabled,
72+
isError: (errorText?.length ?? 0) > 0,
73+
});
74+
const {focusAnim, focusAnimRange, focusInput, onInputBlur, onInputFocus} = useTextInputFocus({inputRef, onBlur, onFocus});
75+
76+
const LeadingIcon = leadingIcon;
77+
const TrailingIcon = errorText ? ErrorIcon : trailingIcon;
78+
79+
const [smallLabelFontSize, largeLabelFontSize] = [
80+
bodySmall.fontSize ?? DEFAULT_LABEL_SMALL_FONT_SIZE,
81+
bodyLarge.fontSize ?? DEFAULT_LABEL_LARGE_FONT_SIZE,
82+
];
83+
const [unfocusedLabelFontSize, unfocusedLabelTopPlacement] = placeholder
84+
? [smallLabelFontSize, 0]
85+
: [largeLabelFontSize, UNFOCUSED_LABEL_TOP_PLACEMENT];
86+
87+
const labelAnimatedStyles = useAnimatedStyle(
88+
() => ({
89+
top: interpolate(focusAnim.value, focusAnimRange, [unfocusedLabelTopPlacement, 0]),
90+
color: interpolateColor(focusAnim.value, focusAnimRange, [labelUnfocusedColor, labelFocusedColor] as string[]),
91+
fontSize: interpolate(focusAnim.value, focusAnimRange, [unfocusedLabelFontSize, smallLabelFontSize]),
92+
}),
93+
[labelUnfocusedColor, labelFocusedColor, unfocusedLabelTopPlacement, unfocusedLabelFontSize, smallLabelFontSize]
94+
);
95+
96+
const activeIndicatorAnimatedStyles = useAnimatedStyle(
97+
() => ({
98+
backgroundColor: interpolateColor(focusAnim.value, focusAnimRange, [activeIndicatorUnfocusedColor, activeIndicatorFocusedColor] as string[]),
99+
height: interpolate(focusAnim.value, focusAnimRange, [ACTIVE_INDICATOR_UNFOCUSED_HEIGHT, ACTIVE_INDICATOR_FOCUSED_HEIGHT]),
100+
}),
101+
[activeIndicatorUnfocusedColor, activeIndicatorFocusedColor]
102+
);
103+
104+
return (
105+
<View style={outerContainerStyle} onLayout={onOuterContainerLayout}>
106+
<TouchableWithoutFeedback onPress={focusInput}>
107+
<Animated.View style={[styles.container, {backgroundColor: containerColor}, innerContainerStyle]}>
108+
{leadingComponent}
109+
{LeadingIcon ? <LeadingIcon color={leadingIconColor} style={styles.leadingIcon} {...leadingIconProps} /> : null}
110+
<View style={[styles.inputWithLabelContainer]}>
111+
<Animated.Text style={[styles.label, bodyLarge, labelAnimatedStyles, labelStyle]}>{label}</Animated.Text>
112+
<TextInput
113+
ref={inputRef}
114+
onBlur={onInputBlur}
115+
onFocus={onInputFocus}
116+
editable={!disabled}
117+
placeholder={placeholder}
118+
selectionColor={selectionColor}
119+
placeholderTextColor={placeholderColor}
120+
style={[styles.input, bodyLarge, {color: valueColor}, style]}
121+
{...props}
122+
/>
123+
</View>
124+
{TrailingIcon ? <TrailingIcon color={trailingIconColor} style={styles.trailingIcon} {...trailingIconProps} /> : null}
125+
{trailingComponent}
126+
<Animated.View style={[styles.activeIndicator, activeIndicatorAnimatedStyles, activeIndicatorStyle]} />
127+
</Animated.View>
128+
</TouchableWithoutFeedback>
129+
{suportingText || errorText ? (
130+
<Text style={[bodySmall, styles.supportingText, {color: supportingTextColor}, supportingTextStyle]}>{errorText ?? suportingText}</Text>
131+
) : null}
132+
</View>
133+
);
134+
}
135+
);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {StyleSheet} from 'react-native';
2+
3+
export const styles = StyleSheet.create({
4+
container: {
5+
flexDirection: 'row',
6+
alignItems: 'center',
7+
8+
height: 56,
9+
paddingHorizontal: 16,
10+
paddingVertical: 8,
11+
borderTopStartRadius: 4,
12+
borderTopEndRadius: 4,
13+
width: '100%',
14+
},
15+
inputWithLabelContainer: {
16+
flex: 1,
17+
18+
height: '100%',
19+
},
20+
input: {
21+
marginTop: 'auto',
22+
paddingBottom: 0,
23+
paddingStart: 0,
24+
},
25+
label: {
26+
position: 'absolute',
27+
},
28+
activeIndicator: {
29+
position: 'absolute',
30+
zIndex: 1,
31+
bottom: 0,
32+
left: 0,
33+
right: 0,
34+
},
35+
supportingText: {
36+
alignSelf: 'flex-start',
37+
38+
marginHorizontal: 16,
39+
marginTop: 4,
40+
},
41+
leadingIcon: {
42+
marginEnd: 16,
43+
},
44+
trailingIcon: {
45+
marginStart: 16,
46+
},
47+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {type ColorValue} from 'react-native';
2+
3+
import {type Theme} from '../theme/theme.types';
4+
import {TextInputColors} from './text-input.types';
5+
6+
export const getTextInputActiveColors = (theme: Theme, primaryColorBasedOnError: ColorValue, surfaceVariantColorBasedOnError: ColorValue) => ({
7+
[TextInputColors.VALUE_COLOR]: theme.surface.text,
8+
[TextInputColors.CONTAINER_COLOR]: theme.surfaceContainer.backgroundHighest,
9+
[TextInputColors.SELECTION_COLOR]: primaryColorBasedOnError,
10+
[TextInputColors.PLACEHOLDER_COLOR]: theme.surface.textVariant,
11+
[TextInputColors.LABEL_FOCUSED_COLOR]: primaryColorBasedOnError,
12+
[TextInputColors.TRAILING_ICON_COLOR]: surfaceVariantColorBasedOnError,
13+
[TextInputColors.LEADING_ICON_COLOR]: theme.surface.textVariant,
14+
[TextInputColors.LABEL_UNFOCUSED_COLOR]: surfaceVariantColorBasedOnError,
15+
[TextInputColors.SUPPORING_TEXT_COLOR]: surfaceVariantColorBasedOnError,
16+
[TextInputColors.ACTIVE_INDICATOR_FOCUSED_COLOR]: primaryColorBasedOnError,
17+
[TextInputColors.ACTIVE_INDICATOR_UNFOCUSED_COLOR]: surfaceVariantColorBasedOnError,
18+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {type ColorValue} from 'react-native';
2+
3+
import {TextInputColors} from './text-input.types';
4+
5+
export const getTextInputDisabledColors = (disabledOnContainerColor: ColorValue, disabledContaienerColor: ColorValue) => {
6+
const colors = Object.values(TextInputColors).reduce((acc, key) => {
7+
acc[key] = disabledOnContainerColor;
8+
9+
return acc;
10+
}, {} as Record<TextInputColors, ColorValue>);
11+
12+
colors[TextInputColors.CONTAINER_COLOR] = disabledContaienerColor;
13+
14+
return colors;
15+
};

0 commit comments

Comments
 (0)