Skip to content

Commit bc95ae1

Browse files
authored
feat: add support for affix and icon in textinput (#1651)
1 parent cc14479 commit bc95ae1

15 files changed

+1503
-54
lines changed

example/src/Examples/TextInputExample.tsx

+120-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type State = {
1818
name: string;
1919
outlinedText: string;
2020
largeText: string;
21+
flatTextPassword: string;
2122
outlinedLargeText: string;
2223
nameNoPadding: string;
2324
flatDenseText: string;
@@ -29,6 +30,11 @@ type State = {
2930
outlinedMultiline: string;
3031
outlinedTextArea: string;
3132
maxLengthName: string;
33+
flatTextSecureEntry: boolean;
34+
outlineTextSecureEntry: boolean;
35+
iconsColor: {
36+
[key: string]: string | undefined;
37+
};
3238
};
3339

3440
class TextInputExample extends React.Component<Props, State> {
@@ -39,7 +45,9 @@ class TextInputExample extends React.Component<Props, State> {
3945
name: '',
4046
outlinedText: '',
4147
largeText: '',
48+
flatTextPassword: 'Password',
4249
outlinedLargeText: '',
50+
outlinedTextPassword: 'Password',
4351
nameNoPadding: '',
4452
flatDenseText: '',
4553
flatDense: '',
@@ -50,10 +58,37 @@ class TextInputExample extends React.Component<Props, State> {
5058
outlinedMultiline: '',
5159
outlinedTextArea: '',
5260
maxLengthName: '',
61+
flatTextSecureEntry: true,
62+
outlineTextSecureEntry: true,
63+
iconsColor: {
64+
flatLeftIcon: undefined,
65+
flatRightIcon: undefined,
66+
outlineLeftIcon: undefined,
67+
outlineRightIcon: undefined,
68+
},
5369
};
5470

5571
_isUsernameValid = (name: string) => /^[a-zA-Z]*$/.test(name);
5672

73+
_changeIconColor = (name: string) => {
74+
const {
75+
theme: {
76+
colors: { accent },
77+
},
78+
} = this.props;
79+
80+
const { iconsColor: currentColors } = this.state;
81+
82+
const color = (currentColors as State['iconsColor'])[name];
83+
84+
const iconsColor = {
85+
...currentColors,
86+
[name]: !color ? accent : undefined,
87+
};
88+
89+
this.setState({ iconsColor });
90+
};
91+
5792
render() {
5893
const {
5994
theme: {
@@ -78,13 +113,53 @@ class TextInputExample extends React.Component<Props, State> {
78113
placeholder="Type something"
79114
value={this.state.text}
80115
onChangeText={text => this.setState({ text })}
116+
left={
117+
<TextInput.Icon
118+
name="heart"
119+
color={this.state.iconsColor['flatLeftIcon']}
120+
onPress={() => {
121+
this._changeIconColor('flatLeftIcon');
122+
}}
123+
/>
124+
}
125+
right={<TextInput.Affix text="/100" />}
81126
/>
82127
<TextInput
83128
style={[styles.inputContainerStyle, styles.fontSize]}
84129
label="Flat input large font"
85130
placeholder="Type something"
86131
value={this.state.largeText}
87132
onChangeText={largeText => this.setState({ largeText })}
133+
left={<TextInput.Affix text="#" />}
134+
right={
135+
<TextInput.Icon
136+
name="heart"
137+
color={this.state.iconsColor['flatRightIcon']}
138+
onPress={() => {
139+
this._changeIconColor('flatRightIcon');
140+
}}
141+
/>
142+
}
143+
/>
144+
<TextInput
145+
style={[styles.inputContainerStyle, styles.fontSize]}
146+
label="Flat input large font"
147+
placeholder="Type something"
148+
value={this.state.flatTextPassword}
149+
onChangeText={flatTextPassword =>
150+
this.setState({ flatTextPassword })
151+
}
152+
secureTextEntry={this.state.flatTextSecureEntry}
153+
right={
154+
<TextInput.Icon
155+
name={this.state.flatTextSecureEntry ? 'eye' : 'eye-off'}
156+
onPress={() =>
157+
this.setState({
158+
flatTextSecureEntry: !this.state.flatTextSecureEntry,
159+
})
160+
}
161+
/>
162+
}
88163
/>
89164
<TextInput
90165
style={styles.inputContainerStyle}
@@ -93,6 +168,7 @@ class TextInputExample extends React.Component<Props, State> {
93168
placeholder="Type something"
94169
value={this.state.flatDenseText}
95170
onChangeText={flatDenseText => this.setState({ flatDenseText })}
171+
left={<TextInput.Affix text="#" />}
96172
/>
97173
<TextInput
98174
style={styles.inputContainerStyle}
@@ -129,6 +205,16 @@ class TextInputExample extends React.Component<Props, State> {
129205
placeholder="Type something"
130206
value={this.state.outlinedText}
131207
onChangeText={outlinedText => this.setState({ outlinedText })}
208+
left={
209+
<TextInput.Icon
210+
name="heart"
211+
color={this.state.iconsColor['outlineLeftIcon']}
212+
onPress={() => {
213+
this._changeIconColor('outlineLeftIcon');
214+
}}
215+
/>
216+
}
217+
right={<TextInput.Affix text="/100" />}
132218
/>
133219
<TextInput
134220
mode="outlined"
@@ -139,6 +225,37 @@ class TextInputExample extends React.Component<Props, State> {
139225
onChangeText={outlinedLargeText =>
140226
this.setState({ outlinedLargeText })
141227
}
228+
left={<TextInput.Affix text="$" />}
229+
right={
230+
<TextInput.Icon
231+
name="heart"
232+
color={this.state.iconsColor['outlineRightIcon']}
233+
onPress={() => {
234+
this._changeIconColor('outlineRightIcon');
235+
}}
236+
/>
237+
}
238+
/>
239+
<TextInput
240+
mode="outlined"
241+
style={[styles.inputContainerStyle, styles.fontSize]}
242+
label="Outlined large font"
243+
placeholder="Type something"
244+
value={this.state.outlinedTextPassword}
245+
onChangeText={outlinedLargeText =>
246+
this.setState({ outlinedLargeText })
247+
}
248+
secureTextEntry={this.state.outlineTextSecureEntry}
249+
right={
250+
<TextInput.Icon
251+
name={this.state.outlineTextSecureEntry ? 'eye' : 'eye-off'}
252+
onPress={() =>
253+
this.setState({
254+
outlineTextSecureEntry: !this.state.outlineTextSecureEntry,
255+
})
256+
}
257+
/>
258+
}
142259
/>
143260
<TextInput
144261
mode="outlined"
@@ -150,6 +267,7 @@ class TextInputExample extends React.Component<Props, State> {
150267
onChangeText={outlinedDenseText =>
151268
this.setState({ outlinedDenseText })
152269
}
270+
left={<TextInput.Affix text="$" />}
153271
/>
154272
<TextInput
155273
mode="outlined"
@@ -196,7 +314,7 @@ class TextInputExample extends React.Component<Props, State> {
196314
onChangeText={name => this.setState({ name })}
197315
/>
198316
<HelperText
199-
type="error"
317+
type="info"
200318
visible={!this._isUsernameValid(this.state.name)}
201319
>
202320
Error: Only letters are allowed
@@ -269,7 +387,7 @@ const styles = StyleSheet.create({
269387
margin: 8,
270388
},
271389
fontSize: {
272-
fontSize: 24,
390+
fontSize: 32,
273391
},
274392
textArea: {
275393
height: 80,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import React from 'react';
2+
import color from 'color';
3+
import {
4+
Text,
5+
StyleSheet,
6+
StyleProp,
7+
TextStyle,
8+
LayoutChangeEvent,
9+
Animated,
10+
} from 'react-native';
11+
12+
import { withTheme } from '../../../core/theming';
13+
import { Theme } from '../../../types';
14+
import { AdornmentSide } from './enums';
15+
16+
const AFFIX_OFFSET = 12;
17+
18+
type Props = {
19+
text: string;
20+
onLayout?: (event: LayoutChangeEvent) => void;
21+
/**
22+
* @optional
23+
*/
24+
theme: Theme;
25+
};
26+
27+
type ContextState = {
28+
affixTopPosition: number | null;
29+
onLayout?: (event: LayoutChangeEvent) => void;
30+
visible?: Animated.Value;
31+
textStyle?: StyleProp<TextStyle>;
32+
side: AdornmentSide;
33+
};
34+
35+
const AffixContext = React.createContext<ContextState>({
36+
textStyle: { fontFamily: '', color: '' },
37+
affixTopPosition: null,
38+
side: AdornmentSide.Left,
39+
});
40+
41+
export const AffixAdornment: React.FunctionComponent<{
42+
affix: React.ReactNode;
43+
testID: string;
44+
} & ContextState> = ({
45+
affix,
46+
side,
47+
textStyle,
48+
affixTopPosition,
49+
onLayout,
50+
visible,
51+
}) => {
52+
return (
53+
<AffixContext.Provider
54+
value={{
55+
side,
56+
textStyle,
57+
affixTopPosition,
58+
onLayout,
59+
visible,
60+
}}
61+
>
62+
{affix}
63+
</AffixContext.Provider>
64+
);
65+
};
66+
67+
const TextInputAffix = ({ text, theme }: Props) => {
68+
const {
69+
textStyle,
70+
onLayout,
71+
affixTopPosition,
72+
side,
73+
visible,
74+
} = React.useContext(AffixContext);
75+
const textColor = color(theme.colors.text)
76+
.alpha(theme.dark ? 0.7 : 0.54)
77+
.rgb()
78+
.string();
79+
80+
const style = {
81+
top: affixTopPosition,
82+
[side]: AFFIX_OFFSET,
83+
};
84+
85+
return (
86+
<Animated.View
87+
style={[
88+
styles.container,
89+
style,
90+
{
91+
opacity:
92+
visible?.interpolate({
93+
inputRange: [0, 1],
94+
outputRange: [1, 0],
95+
}) || 1,
96+
},
97+
]}
98+
onLayout={onLayout}
99+
>
100+
<Text style={[{ color: textColor }, textStyle]}>{text}</Text>
101+
</Animated.View>
102+
);
103+
};
104+
TextInputAffix.displayName = 'TextInput.Affix';
105+
106+
const styles = StyleSheet.create({
107+
container: {
108+
position: 'absolute',
109+
justifyContent: 'center',
110+
alignItems: 'center',
111+
},
112+
});
113+
114+
export default withTheme(TextInputAffix);
115+
116+
// @component-docs ignore-next-line
117+
export { TextInputAffix };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react';
2+
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
3+
4+
import IconButton from '../../IconButton';
5+
import { $Omit, Theme } from '../../../../src/types';
6+
7+
type Props = $Omit<
8+
React.ComponentProps<typeof IconButton>,
9+
'icon' | 'theme'
10+
> & {
11+
name: string;
12+
onPress?: () => void;
13+
style?: StyleProp<ViewStyle>;
14+
theme?: Theme;
15+
};
16+
17+
export const ICON_SIZE = 24;
18+
const ICON_OFFSET = 12;
19+
20+
const StyleContext = React.createContext<{ style?: StyleProp<ViewStyle> }>({
21+
style: {},
22+
});
23+
24+
export const IconAdornment: React.FunctionComponent<{
25+
testID: string;
26+
icon: React.ReactNode;
27+
iconTopPosition: number;
28+
side: 'left' | 'right';
29+
}> = ({ icon, iconTopPosition, side }) => {
30+
const style = {
31+
top: iconTopPosition,
32+
[side]: ICON_OFFSET,
33+
};
34+
35+
return (
36+
<StyleContext.Provider value={{ style }}>{icon}</StyleContext.Provider>
37+
);
38+
};
39+
40+
const TextInputIcon = ({ name, onPress, ...rest }: Props) => {
41+
const { style } = React.useContext(StyleContext);
42+
return (
43+
<View style={[styles.container, style]}>
44+
<IconButton
45+
icon={name}
46+
style={styles.iconButton}
47+
size={ICON_SIZE}
48+
onPress={onPress}
49+
{...rest}
50+
/>
51+
</View>
52+
);
53+
};
54+
TextInputIcon.displayName = 'TextInput.Icon';
55+
56+
const styles = StyleSheet.create({
57+
container: {
58+
position: 'absolute',
59+
width: ICON_SIZE,
60+
height: ICON_SIZE,
61+
justifyContent: 'center',
62+
alignItems: 'center',
63+
},
64+
iconButton: {
65+
margin: 0,
66+
},
67+
});
68+
69+
export default TextInputIcon;

0 commit comments

Comments
 (0)