Skip to content

Commit 85fddb6

Browse files
committed
feat: placeholder with animation in progress
1 parent 3de6eaf commit 85fddb6

11 files changed

Lines changed: 195 additions & 53 deletions

File tree

example/src/Examples/TextFieldExample.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const TextFieldExample = () => {
7777
LeftAccessory={SearchLeadingAccessory}
7878
RightAccessory={ClearFilledSearchAccessory}
7979
pressableStyle={styles.field}
80+
placeholder="Search"
8081
/>
8182
<TextField
8283
variant="filled"
@@ -87,6 +88,7 @@ const TextFieldExample = () => {
8788
autoCapitalize="none"
8889
autoCorrect={false}
8990
pressableStyle={styles.field}
91+
placeholder="Email"
9092
/>
9193
<TextField
9294
variant="filled"
@@ -140,6 +142,7 @@ const TextFieldExample = () => {
140142
LeftAccessory={SearchLeadingAccessory}
141143
RightAccessory={ClearOutlinedSearchAccessory}
142144
pressableStyle={styles.field}
145+
placeholder="Search"
143146
/>
144147
<TextField
145148
variant="outlined"

src/components/TextField/TextField.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type TextFieldSharedApi = {
3333
hasError: boolean;
3434
$animatedLabelWrapperStyle: Animated.WithAnimatedObject<ViewStyle>;
3535
$animatedLabelTextStyle: Animated.WithAnimatedObject<TextStyle>;
36+
$animatedPlaceholderStyle: Animated.WithAnimatedObject<TextStyle>;
3637
};
3738

3839
export interface TextFieldProps extends TextInputProps {
@@ -174,7 +175,7 @@ function TextField(props: TextFieldProps) {
174175
$helperStyles,
175176
$selectionColor,
176177
$cursorColor,
177-
$placeholderTextColor,
178+
$animatedPlaceholderStyles,
178179
LeadingAccessory,
179180
TrailingAccessory,
180181
focusInput,
@@ -214,6 +215,16 @@ function TextField(props: TextFieldProps) {
214215
)}
215216

216217
<View style={$containerStyles}>
218+
{!!textInputProps.placeholder && !textInputProps.value && (
219+
<Animated.Text
220+
aria-hidden
221+
pointerEvents="none"
222+
style={$animatedPlaceholderStyles}
223+
>
224+
{textInputProps.placeholder}
225+
</Animated.Text>
226+
)}
227+
217228
<TextInput
218229
aria-label={label}
219230
aria-disabled={disabled}
@@ -224,8 +235,8 @@ function TextField(props: TextFieldProps) {
224235
onBlur={onBlurHandler}
225236
selectionColor={$selectionColor}
226237
cursorColor={$cursorColor}
227-
placeholderTextColor={$placeholderTextColor}
228238
{...textInputProps}
239+
placeholder={undefined}
229240
style={$inputStyles}
230241
/>
231242
</View>

src/components/TextField/constants.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,6 @@ export const INACTIVE_INDICATOR_SIZE = 1;
5656
// ============
5757
export const TEXT_FIELD_BORDER_RADIUS = 4;
5858

59-
// ============
60-
// MULTILINE
61-
// ============
62-
export const MULTILINE_PADDING_TOP =
63-
ACTIVE_LABEL_FONT_SIZE + TEXT_FIELD_PADDING_VERTICAL;
64-
6559
// ============
6660
// OPACITY
6761
// ============

src/components/TextField/filled/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
ACCESSORY_SIZE,
3+
ACTIVE_LABEL_FONT_SIZE,
34
TEXT_FIELD_ACCESSORY_MARGIN_HORIZONTAL,
45
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL,
56
TEXT_FIELD_PADDING_VERTICAL,
@@ -18,3 +19,9 @@ export const LABEL_LEFT_OFFSET_WITHOUT_ACCESSORY =
1819
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL;
1920

2021
export const ACTIVE_LABEL_TOP_POSITION = TEXT_FIELD_PADDING_VERTICAL;
22+
23+
// ==================
24+
// PLACEHOLDER & MULTILINE POSITIONING
25+
// ==================
26+
27+
export const PADDING_TOP = ACTIVE_LABEL_FONT_SIZE + TEXT_FIELD_PADDING_VERTICAL;

src/components/TextField/filled/logic.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
INACTIVE_INDICATOR_SIZE,
1212
INPUT_FONT_SIZE,
1313
isWeb,
14-
MULTILINE_PADDING_TOP,
1514
} from '../constants';
1615
import {
1716
$disabledStyle,
@@ -29,6 +28,7 @@ import {
2928
import {
3029
LABEL_LEFT_OFFSET_WITH_ACCESSORY,
3130
LABEL_LEFT_OFFSET_WITHOUT_ACCESSORY,
31+
PADDING_TOP,
3232
} from './constants';
3333
import {
3434
$containerStyle,
@@ -61,6 +61,7 @@ export const getFilledTextFieldData = (
6161
hasError,
6262
$animatedLabelWrapperStyle,
6363
$animatedLabelTextStyle,
64+
$animatedPlaceholderStyle,
6465
} = api;
6566

6667
// =======================
@@ -161,7 +162,7 @@ export const getFilledTextFieldData = (
161162
},
162163
textInputProps.multiline && {
163164
height: 'auto' as TextStyle['height'],
164-
paddingTop: MULTILINE_PADDING_TOP,
165+
paddingTop: PADDING_TOP,
165166
},
166167
isWeb && {
167168
outlineStyle: 'none' as TextStyle['outlineStyle'],
@@ -180,12 +181,33 @@ export const getFilledTextFieldData = (
180181
disabled && $disabledStyle,
181182
];
182183

184+
const $animatedPlaceholderStyles: StyleProp<
185+
Animated.WithAnimatedObject<TextStyle> | TextStyle
186+
> = [
187+
$inputStyle,
188+
{
189+
position: 'absolute',
190+
top: PADDING_TOP,
191+
left: hasAccessory
192+
? LABEL_LEFT_OFFSET_WITH_ACCESSORY
193+
: LABEL_LEFT_OFFSET_WITHOUT_ACCESSORY,
194+
fontSize: INPUT_FONT_SIZE,
195+
color:
196+
textInputProps.placeholderTextColor ?? theme.colors.onSurfaceVariant,
197+
textAlign: isRTL ? 'right' : 'left',
198+
writingDirection: isRTL ? 'rtl' : 'ltr',
199+
},
200+
disabled && $disabledStyle,
201+
$animatedPlaceholderStyle,
202+
];
203+
183204
return {
184205
input,
185206
disabled,
186207
hasError,
187208
$animatedLabelWrapperStyles,
188209
$animatedLabelTextStyles,
210+
$animatedPlaceholderStyles,
189211
$fieldStyles,
190212
$outlineStyles,
191213
$containerStyles,

src/components/TextField/logic.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const useTextField = (props: TextFieldProps) => {
6262

6363
const { isRTL } = I18nManager;
6464
const disabled = props.editable === false || props.status === 'disabled';
65-
const isFloating = isFocused || !!props.value || !!props.placeholder;
65+
const isFloating = isFocused || !!props.value;
6666
const hasAccessory = isRTL ? !!props.RightAccessory : !!props.LeftAccessory;
6767
const hasError = props.status === 'error';
6868

@@ -73,18 +73,19 @@ export const useTextField = (props: TextFieldProps) => {
7373
const { selectionColor: $selectionColor, cursorColor: $cursorColor } =
7474
getAccentColors({ theme, hasError });
7575

76-
const $placeholderTextColor = theme.colors.onSurfaceVariant;
77-
7876
// =======================
7977
// LABEL ANIMATION
8078
// =======================
8179

82-
const { $animatedLabelWrapperStyle, $animatedLabelTextStyle } =
83-
useTextFieldLabelAnimation({
84-
variant,
85-
isFloating,
86-
hasAccessory,
87-
});
80+
const {
81+
$animatedLabelWrapperStyle,
82+
$animatedLabelTextStyle,
83+
$animatedPlaceholderStyle,
84+
} = useTextFieldAnimation({
85+
variant,
86+
isFloating,
87+
hasAccessory,
88+
});
8889

8990
// =======================
9091
// HANDLERS
@@ -118,6 +119,7 @@ export const useTextField = (props: TextFieldProps) => {
118119
hasError,
119120
$animatedLabelWrapperStyle,
120121
$animatedLabelTextStyle,
122+
$animatedPlaceholderStyle,
121123
};
122124

123125
const LeadingAccessory = isRTL ? props.RightAccessory : props.LeftAccessory;
@@ -130,10 +132,10 @@ export const useTextField = (props: TextFieldProps) => {
130132
const $pressableStyles = [$pressableStyle, $pressableStyleOverride];
131133

132134
const data = {
135+
isFocused,
133136
$pressableStyles,
134137
$selectionColor,
135138
$cursorColor,
136-
$placeholderTextColor,
137139
LeadingAccessory,
138140
TrailingAccessory,
139141
onFocusHandler,
@@ -154,7 +156,7 @@ export const useTextField = (props: TextFieldProps) => {
154156
};
155157
};
156158

157-
const useTextFieldLabelAnimation = ({
159+
const useTextFieldAnimation = ({
158160
variant,
159161
isFloating,
160162
hasAccessory,
@@ -165,6 +167,7 @@ const useTextFieldLabelAnimation = ({
165167
}): {
166168
$animatedLabelWrapperStyle: Animated.WithAnimatedObject<ViewStyle>;
167169
$animatedLabelTextStyle: Animated.WithAnimatedObject<TextStyle>;
170+
$animatedPlaceholderStyle: Animated.WithAnimatedObject<TextStyle>;
168171
} => {
169172
const progress = useRef(new Animated.Value(isFloating ? 1 : 0)).current;
170173

@@ -192,10 +195,16 @@ const useTextFieldLabelAnimation = ({
192195
outputRange: [INACTIVE_LABEL_TOP_POSITION, activeTop],
193196
});
194197

198+
const opacity = progress.interpolate({
199+
inputRange: [0, 1],
200+
outputRange: [0, 1],
201+
});
202+
195203
if (variant === 'filled') {
196204
return {
197205
$animatedLabelWrapperStyle: { top },
198206
$animatedLabelTextStyle: { fontSize },
207+
$animatedPlaceholderStyle: { opacity },
199208
};
200209
}
201210

@@ -216,6 +225,7 @@ const useTextFieldLabelAnimation = ({
216225
],
217226
},
218227
$animatedLabelTextStyle: { fontSize },
228+
$animatedPlaceholderStyle: { opacity },
219229
};
220230
}, [variant, hasAccessory, progress]);
221231
};

src/components/TextField/outlined/constants.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,20 @@ export const LABEL_LEFT_OFFSET_WITH_ACCESSORY =
2727
LABEL_PADDING_HORIZONTAL;
2828

2929
export const LABEL_LEFT_OFFSET_WITHOUT_ACCESSORY =
30-
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL - LABEL_PADDING_HORIZONTAL;
30+
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL;
3131

3232
export const ACTIVE_LABEL_TOP_POSITION =
3333
-TEXT_FIELD_PADDING_VERTICAL + LINE_HEIGHT_DELTA;
3434

3535
export const LABEL_TRANSLATE_X_WITH_ACCESSORY =
3636
-layoutSupportMultiplier *
37-
(ACCESSORY_SIZE +
38-
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL -
39-
LABEL_PADDING_HORIZONTAL);
37+
(ACCESSORY_SIZE + TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL);
4038

41-
export const LABEL_TRANSLATE_X_WITHOUT_ACCESSORY =
42-
layoutSupportMultiplier * LABEL_PADDING_HORIZONTAL;
39+
export const LABEL_TRANSLATE_X_WITHOUT_ACCESSORY = -LABEL_PADDING_HORIZONTAL;
40+
41+
// ==================
42+
// PLACEHOLDER POSITIONING
43+
// ==================
44+
45+
export const PLACEHOLDER_TOP_POSITION =
46+
TEXT_FIELD_PADDING_VERTICAL + LINE_HEIGHT_DELTA;

src/components/TextField/outlined/logic.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,28 @@ import {
77
} from 'react-native';
88

99
import { INPUT_FONT_SIZE, isWeb } from '../constants';
10+
import {
11+
$disabledStyle,
12+
$helperStyle,
13+
$inputStyle,
14+
$leadingAccessoryStyle,
15+
$trailingAccessoryStyle,
16+
} from '../styles';
1017
import type { TextFieldProps, TextFieldSharedApi } from '../TextField';
1118
import { getHelperColor, getLabelColor } from '../utils';
1219
import {
1320
LABEL_LEFT_OFFSET_WITH_ACCESSORY,
1421
LABEL_LEFT_OFFSET_WITHOUT_ACCESSORY,
22+
PLACEHOLDER_TOP_POSITION,
1523
} from './constants';
1624
import {
17-
$fieldStyle,
1825
$containerStyle,
19-
$labelWrapperStyle,
26+
$fieldStyle,
2027
$labelTextStyle,
28+
$labelWrapperStyle,
2129
$outlineStyle,
2230
} from './styles';
23-
import { getOutlinedFieldColors } from './utils';
24-
import {
25-
$disabledStyle,
26-
$helperStyle,
27-
$inputStyle,
28-
$leadingAccessoryStyle,
29-
$trailingAccessoryStyle,
30-
} from '../styles';
31+
import { getOutlineColor } from './utils';
3132

3233
export const getOutlinedTextFieldData = (
3334
api: TextFieldSharedApi,
@@ -51,6 +52,7 @@ export const getOutlinedTextFieldData = (
5152
hasError,
5253
$animatedLabelWrapperStyle,
5354
$animatedLabelTextStyle,
55+
$animatedPlaceholderStyle,
5456
} = api;
5557

5658
// =======================
@@ -74,9 +76,10 @@ export const getOutlinedTextFieldData = (
7476
disabled,
7577
});
7678

77-
const { inactiveOutlineColor, activeOutlineColor } = getOutlinedFieldColors({
79+
const outlineColor = getOutlineColor({
7880
theme,
7981
disabled,
82+
isFocused,
8083
hasError,
8184
});
8285

@@ -92,7 +95,7 @@ export const getOutlinedTextFieldData = (
9295
$outlineStyle,
9396
{
9497
borderWidth: isFocused ? 2 : 1,
95-
borderColor: hasError ? activeOutlineColor : inactiveOutlineColor,
98+
borderColor: outlineColor,
9699
},
97100
$fieldStyleOverride,
98101
];
@@ -162,12 +165,33 @@ export const getOutlinedTextFieldData = (
162165
disabled && $disabledStyle,
163166
];
164167

168+
const $animatedPlaceholderStyles: StyleProp<
169+
Animated.WithAnimatedObject<TextStyle> | TextStyle
170+
> = [
171+
$inputStyle,
172+
{
173+
position: 'absolute',
174+
top: PLACEHOLDER_TOP_POSITION,
175+
left: hasAccessory
176+
? LABEL_LEFT_OFFSET_WITH_ACCESSORY
177+
: LABEL_LEFT_OFFSET_WITHOUT_ACCESSORY,
178+
fontSize: INPUT_FONT_SIZE,
179+
color:
180+
textInputProps.placeholderTextColor ?? theme.colors.onSurfaceVariant,
181+
textAlign: isRTL ? 'right' : 'left',
182+
writingDirection: isRTL ? 'rtl' : 'ltr',
183+
},
184+
disabled && $disabledStyle,
185+
$animatedPlaceholderStyle,
186+
];
187+
165188
return {
166189
input,
167190
disabled,
168191
hasError,
169192
$animatedLabelWrapperStyles,
170193
$animatedLabelTextStyles,
194+
$animatedPlaceholderStyles,
171195
$fieldStyles,
172196
$outlineStyles,
173197
$containerStyles,

0 commit comments

Comments
 (0)