@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klyftig/mobile-ui",
3
- "version": "1.3.10",
3
+ "version": "1.3.13",
4
4
  "scripts": {
5
5
  "pack-install": "npm pack && mv klyftig-ui-*.tgz /tmp/klyftig-ui-local.tgz && cd ../../mobile && npm install /tmp/klyftig-ui-local.tgz"
6
6
  },
@@ -1,5 +1,6 @@
1
- import React, { useState, useCallback, useEffect, memo } from 'react';
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
- // Reinforces the `selection` prop below: on Android, setting `selection` on
64
- // the SAME render as a `value` that's longer than the field's width is
65
- // often ignored — the native EditText hasn't measured/laid out the text yet,
66
- // so it doesn't know where to scroll to reveal position 0. `setNativeProps`
67
- // does NOT reliably fix this on Android either (selection isn't reliably
68
- // forwarded through that imperative path for TextInput) — it has to be a
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
- useEffect(() => {
75
- setDelayedSelectionStart(undefined);
76
- if (isFocused || multiline) return;
77
- const id = setTimeout(() => {
78
- setDelayedSelectionStart({ start: 0, end: 0 });
79
- }, 50);
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 style={[styles.inputWrapper, multiline && styles.inputWrapperMultiline]}>
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 ? (delayedSelectionStart ?? SELECTION_START) : undefined}
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
  },