@klyftig/mobile-ui 1.3.9 → 1.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/GlassInput.tsx +50 -18
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useState, useCallback,
|
|
1
|
+
import React, { useState, useCallback, memo } from 'react';
|
|
2
2
|
import { View, TextInput as RNTextInput, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
|
3
|
+
import type { NativeSyntheticEvent, TextInputContentSizeChangeEventData } from 'react-native';
|
|
3
4
|
import { MaterialCommunityIcons as Icon } from '@expo/vector-icons';
|
|
4
5
|
import { useTheme } from '../hooks/useTheme';
|
|
5
6
|
|
|
@@ -13,6 +14,13 @@ interface GlassInputProps {
|
|
|
13
14
|
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
|
|
14
15
|
multiline?: boolean;
|
|
15
16
|
numberOfLines?: number;
|
|
17
|
+
// Only meaningful with multiline. Instead of `inputWrapperMultiline`'s fixed
|
|
18
|
+
// minHeight (96, a sensible default for a notes/description field), sizes
|
|
19
|
+
// the box to the actual content: compact for one short line, growing up to
|
|
20
|
+
// `numberOfLines` only when the value actually needs it. For a title-style
|
|
21
|
+
// field (e.g. a composite dish name) where most values are short, a fixed
|
|
22
|
+
// multi-line reservation looks oversized.
|
|
23
|
+
autoGrow?: boolean;
|
|
16
24
|
// Off by default for fields where iOS would otherwise replace partially-edited
|
|
17
25
|
// multi-word values with its autocorrect suggestion and snap the caret to end.
|
|
18
26
|
autoCorrect?: boolean;
|
|
@@ -35,6 +43,13 @@ interface GlassInputProps {
|
|
|
35
43
|
// so the caret follows taps and typing normally.
|
|
36
44
|
const SELECTION_START = { start: 0, end: 0 } as const;
|
|
37
45
|
|
|
46
|
+
// Matches `input`'s fontSize: 16 at RN's default ~1.2 line-height ratio.
|
|
47
|
+
// Used only by autoGrow to clamp the measured content height per line.
|
|
48
|
+
const LINE_HEIGHT = 20;
|
|
49
|
+
// inputWrapperAutoGrow's paddingVertical (12 top + 12 bottom) — added on top
|
|
50
|
+
// of the measured text height to get the wrapper's total minHeight.
|
|
51
|
+
const AUTO_GROW_PADDING_V = 24;
|
|
52
|
+
|
|
38
53
|
const GlassInputComponent: React.FC<GlassInputProps> = ({
|
|
39
54
|
label,
|
|
40
55
|
value,
|
|
@@ -56,25 +71,24 @@ const GlassInputComponent: React.FC<GlassInputProps> = ({
|
|
|
56
71
|
selectTextOnFocus,
|
|
57
72
|
multiline = false,
|
|
58
73
|
numberOfLines,
|
|
74
|
+
autoGrow = false,
|
|
59
75
|
}) => {
|
|
60
76
|
const { colors } = useTheme();
|
|
61
77
|
const [isSecure, setIsSecure] = useState(secureTextEntry);
|
|
62
78
|
const [isFocused, setIsFocused] = useState(false);
|
|
63
|
-
const
|
|
79
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
80
|
+
const isAutoGrow = multiline && autoGrow;
|
|
81
|
+
|
|
82
|
+
const handleContentSizeChange = useCallback((e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
|
|
83
|
+
setContentHeight(e.nativeEvent.contentSize.height);
|
|
84
|
+
}, []);
|
|
64
85
|
|
|
65
|
-
//
|
|
66
|
-
// on
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (isFocused || multiline) return;
|
|
73
|
-
const id = setTimeout(() => {
|
|
74
|
-
inputRef.current?.setNativeProps({ selection: SELECTION_START });
|
|
75
|
-
}, 0);
|
|
76
|
-
return () => clearTimeout(id);
|
|
77
|
-
}, [value, isFocused, multiline]);
|
|
86
|
+
// Text-only height (no padding) — the wrapper's own paddingVertical is
|
|
87
|
+
// added on top via inputWrapperAutoGrow, matching how inputWrapperMultiline
|
|
88
|
+
// composes minHeight with its own paddingVertical for the fixed variant.
|
|
89
|
+
const growTextHeight = numberOfLines
|
|
90
|
+
? Math.min(Math.max(contentHeight, LINE_HEIGHT), numberOfLines * LINE_HEIGHT)
|
|
91
|
+
: Math.max(contentHeight, LINE_HEIGHT);
|
|
78
92
|
|
|
79
93
|
const handleFocus = useCallback(() => {
|
|
80
94
|
setIsFocused(true);
|
|
@@ -102,12 +116,17 @@ const GlassInputComponent: React.FC<GlassInputProps> = ({
|
|
|
102
116
|
disabled && styles.inputContainerDisabled,
|
|
103
117
|
]}
|
|
104
118
|
>
|
|
105
|
-
<View
|
|
119
|
+
<View
|
|
120
|
+
style={[
|
|
121
|
+
styles.inputWrapper,
|
|
122
|
+
multiline && !isAutoGrow && styles.inputWrapperMultiline,
|
|
123
|
+
isAutoGrow && [styles.inputWrapperAutoGrow, { minHeight: growTextHeight + AUTO_GROW_PADDING_V }],
|
|
124
|
+
]}
|
|
125
|
+
>
|
|
106
126
|
{icon && (
|
|
107
127
|
<Icon name={icon} size={20} color="#9B9B9B" style={styles.leftIcon} />
|
|
108
128
|
)}
|
|
109
129
|
<RNTextInput
|
|
110
|
-
ref={inputRef}
|
|
111
130
|
value={value}
|
|
112
131
|
onChangeText={onChangeText}
|
|
113
132
|
placeholder={placeholder}
|
|
@@ -120,6 +139,7 @@ const GlassInputComponent: React.FC<GlassInputProps> = ({
|
|
|
120
139
|
editable={!disabled}
|
|
121
140
|
onFocus={handleFocus}
|
|
122
141
|
onBlur={handleBlur}
|
|
142
|
+
onContentSizeChange={isAutoGrow ? handleContentSizeChange : undefined}
|
|
123
143
|
selectTextOnFocus={selectTextOnFocus}
|
|
124
144
|
selection={!isFocused && !multiline ? SELECTION_START : undefined}
|
|
125
145
|
multiline={multiline}
|
|
@@ -134,7 +154,8 @@ const GlassInputComponent: React.FC<GlassInputProps> = ({
|
|
|
134
154
|
styles.input,
|
|
135
155
|
icon && styles.inputWithLeftIcon,
|
|
136
156
|
(rightText || showPasswordToggle) && styles.inputWithRightIcon,
|
|
137
|
-
multiline && styles.inputMultiline,
|
|
157
|
+
multiline && !isAutoGrow && styles.inputMultiline,
|
|
158
|
+
isAutoGrow && [styles.inputAutoGrow, { height: growTextHeight }],
|
|
138
159
|
]}
|
|
139
160
|
/>
|
|
140
161
|
{rightText && (
|
|
@@ -203,6 +224,17 @@ const styles = StyleSheet.create({
|
|
|
203
224
|
minHeight: 72,
|
|
204
225
|
paddingTop: 4,
|
|
205
226
|
},
|
|
227
|
+
// minHeight is applied inline (computed from measured content) — see
|
|
228
|
+
// isAutoGrow in the component.
|
|
229
|
+
inputWrapperAutoGrow: {
|
|
230
|
+
height: undefined,
|
|
231
|
+
paddingVertical: 12,
|
|
232
|
+
alignItems: 'flex-start',
|
|
233
|
+
},
|
|
234
|
+
// height is applied inline (computed from measured content).
|
|
235
|
+
inputAutoGrow: {
|
|
236
|
+
paddingTop: 0,
|
|
237
|
+
},
|
|
206
238
|
leftIcon: {
|
|
207
239
|
marginRight: 12,
|
|
208
240
|
},
|