@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klyftig/mobile-ui",
3
- "version": "1.3.9",
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, useRef, 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,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 inputRef = useRef<RNTextInput>(null);
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
- // Reinforces the `selection` prop below: on Android, setting `selection`
66
- // on the SAME render as a `value` that's longer than the field's width is
67
- // often ignored — the native EditText hasn't measured/laid out the text
68
- // yet, so it doesn't know where to scroll to reveal position 0. Re-asserting
69
- // via setNativeProps a tick after layout commits (not the same frame)
70
- // reliably forces the scroll where the declarative prop alone doesn't.
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 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
+ >
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
  },