@klyftig/mobile-ui 1.3.10 → 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 +51 -22
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,29 +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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// genuine PROP change through normal React reconciliation to hit the native
|
|
70
|
-
// "set selection and scroll" command. A fresh object (new reference) fed
|
|
71
|
-
// through state a tick after mount, once layout has committed, does that.
|
|
72
|
-
const [delayedSelectionStart, setDelayedSelectionStart] = useState<typeof SELECTION_START | undefined>(undefined);
|
|
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
|
+
}, []);
|
|
73
85
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return () => clearTimeout(id);
|
|
81
|
-
}, [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);
|
|
82
92
|
|
|
83
93
|
const handleFocus = useCallback(() => {
|
|
84
94
|
setIsFocused(true);
|
|
@@ -106,7 +116,13 @@ const GlassInputComponent: React.FC<GlassInputProps> = ({
|
|
|
106
116
|
disabled && styles.inputContainerDisabled,
|
|
107
117
|
]}
|
|
108
118
|
>
|
|
109
|
-
<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
|
+
>
|
|
110
126
|
{icon && (
|
|
111
127
|
<Icon name={icon} size={20} color="#9B9B9B" style={styles.leftIcon} />
|
|
112
128
|
)}
|
|
@@ -123,8 +139,9 @@ const GlassInputComponent: React.FC<GlassInputProps> = ({
|
|
|
123
139
|
editable={!disabled}
|
|
124
140
|
onFocus={handleFocus}
|
|
125
141
|
onBlur={handleBlur}
|
|
142
|
+
onContentSizeChange={isAutoGrow ? handleContentSizeChange : undefined}
|
|
126
143
|
selectTextOnFocus={selectTextOnFocus}
|
|
127
|
-
selection={!isFocused && !multiline ?
|
|
144
|
+
selection={!isFocused && !multiline ? SELECTION_START : undefined}
|
|
128
145
|
multiline={multiline}
|
|
129
146
|
numberOfLines={numberOfLines}
|
|
130
147
|
textAlignVertical={multiline ? 'top' : undefined}
|
|
@@ -137,7 +154,8 @@ const GlassInputComponent: React.FC<GlassInputProps> = ({
|
|
|
137
154
|
styles.input,
|
|
138
155
|
icon && styles.inputWithLeftIcon,
|
|
139
156
|
(rightText || showPasswordToggle) && styles.inputWithRightIcon,
|
|
140
|
-
multiline && styles.inputMultiline,
|
|
157
|
+
multiline && !isAutoGrow && styles.inputMultiline,
|
|
158
|
+
isAutoGrow && [styles.inputAutoGrow, { height: growTextHeight }],
|
|
141
159
|
]}
|
|
142
160
|
/>
|
|
143
161
|
{rightText && (
|
|
@@ -206,6 +224,17 @@ const styles = StyleSheet.create({
|
|
|
206
224
|
minHeight: 72,
|
|
207
225
|
paddingTop: 4,
|
|
208
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
|
+
},
|
|
209
238
|
leftIcon: {
|
|
210
239
|
marginRight: 12,
|
|
211
240
|
},
|