jfs-components 0.1.28 → 0.1.30

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +15 -2
  2. package/lib/commonjs/components/CompareTable/CompareTable.js +191 -25
  3. package/lib/commonjs/components/ContentSheet/ContentSheet.js +66 -6
  4. package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
  5. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
  6. package/lib/commonjs/components/Table/Table.js +27 -7
  7. package/lib/commonjs/icons/registry.js +1 -1
  8. package/lib/module/components/CompareTable/CompareTable.js +191 -26
  9. package/lib/module/components/ContentSheet/ContentSheet.js +69 -9
  10. package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
  11. package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
  12. package/lib/module/components/Table/Table.js +27 -7
  13. package/lib/module/icons/registry.js +1 -1
  14. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +21 -1
  15. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +14 -1
  16. package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
  17. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
  18. package/lib/typescript/src/components/Table/Table.d.ts +9 -1
  19. package/lib/typescript/src/icons/registry.d.ts +1 -1
  20. package/package.json +1 -1
  21. package/src/components/CompareTable/CompareTable.tsx +224 -30
  22. package/src/components/ContentSheet/ContentSheet.tsx +88 -8
  23. package/src/components/MoneyValue/MoneyValue.tsx +216 -65
  24. package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
  25. package/src/components/Table/Table.tsx +29 -4
  26. package/src/icons/registry.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
- import React from 'react';
4
- import { View, Text, Pressable, Platform } from 'react-native';
3
+ import React, { useCallback, useRef } from 'react';
4
+ import { View, Text, Pressable, Platform, ScrollView } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES, cloneChildrenWithModes, resolveTruncation } from '../../utils/react-utils';
7
7
  import Accordion from '../Accordion/Accordion';
@@ -106,7 +106,8 @@ function CompareTable({
106
106
  maxColumns = 4,
107
107
  modes = EMPTY_MODES,
108
108
  style,
109
- disableTruncation
109
+ disableTruncation,
110
+ columnWidth
110
111
  }) {
111
112
  // --- selection card tokens ------------------------------------------------
112
113
  const cardBg = getVariableByName('selectionCard/background/color', modes) ?? '#ffffff';
@@ -163,6 +164,70 @@ function CompareTable({
163
164
  const showAddCard = onAddColumn != null && columnCount < maxColumns;
164
165
  /** Grid columns shared by the selection-card row and every table row. */
165
166
  const gridColumnCount = columnCount + (showAddCard ? 1 : 0);
167
+ const showCardsRow = columnCount > 0 || showAddCard;
168
+
169
+ // --- fixed-width scroll mode -------------------------------------------
170
+ // `columnWidth` opts the component into scroll mode (see prop doc). In that
171
+ // mode the cards row and each section's table body are independent
172
+ // horizontal ScrollViews; their scroll events are chained so they move in
173
+ // lockstep, and the cards row pins to the top on the vertical axis.
174
+ const useFixedWidth = columnWidth != null;
175
+ /** Total scrollable content width shared by every horizontal surface. */
176
+ const contentWidth = useFixedWidth ? gridColumnCount * columnWidth : 0;
177
+ const cardsScrollRef = useRef(null);
178
+ const bodyScrollRefs = useRef(new Map());
179
+ // Stable per-section ref-callback instances so React doesn't detach/reattach
180
+ // on every render (which would churn the map and re-fire scrollTo).
181
+ const bodyRefCallbacks = useRef(new Map());
182
+ /** Last offset we synced everyone to. Echoed onScroll events reporting this
183
+ * same value are ignored — this is what prevents feedback loops. */
184
+ const lastSyncX = useRef(0);
185
+ const applySync = useCallback((x, source) => {
186
+ if (Math.abs(x - lastSyncX.current) < 0.5) return;
187
+ lastSyncX.current = x;
188
+ if (source !== 'cards' && cardsScrollRef.current) {
189
+ cardsScrollRef.current.scrollTo({
190
+ x,
191
+ animated: false
192
+ });
193
+ }
194
+ bodyScrollRefs.current.forEach((ref, idx) => {
195
+ if (source === 'cards' || idx !== source) {
196
+ ref.scrollTo({
197
+ x,
198
+ animated: false
199
+ });
200
+ }
201
+ });
202
+ }, []);
203
+ const onCardsScroll = useCallback(e => {
204
+ applySync(e.nativeEvent.contentOffset.x, 'cards');
205
+ }, [applySync]);
206
+ const onBodyScroll = useCallback(idx => e => {
207
+ applySync(e.nativeEvent.contentOffset.x, idx);
208
+ }, [applySync]);
209
+ const getBodyRefCallback = useCallback(idx => {
210
+ let cb = bodyRefCallbacks.current.get(idx);
211
+ if (!cb) {
212
+ cb = ref => {
213
+ if (ref) {
214
+ bodyScrollRefs.current.set(idx, ref);
215
+ // Bring a freshly-expanded section's body into sync with
216
+ // the rest of the surfaces.
217
+ if (lastSyncX.current > 0) {
218
+ ref.scrollTo({
219
+ x: lastSyncX.current,
220
+ animated: false
221
+ });
222
+ }
223
+ } else {
224
+ bodyScrollRefs.current.delete(idx);
225
+ }
226
+ };
227
+ bodyRefCallbacks.current.set(idx, cb);
228
+ }
229
+ return cb;
230
+ }, []);
166
231
  const cardLabelStyle = {
167
232
  ...NO_WRAP_TEXT,
168
233
  color: cardLabelColor,
@@ -193,8 +258,12 @@ function CompareTable({
193
258
  fontWeight: toWeight(cellFontWeight)
194
259
  };
195
260
  const cardStyle = {
196
- flex: 1,
197
- minWidth: 0,
261
+ ...(useFixedWidth ? {
262
+ width: columnWidth
263
+ } : {
264
+ flex: 1,
265
+ minWidth: 0
266
+ }),
198
267
  minHeight: 147,
199
268
  backgroundColor: cardBg,
200
269
  borderRadius: cardRadius,
@@ -313,6 +382,7 @@ function CompareTable({
313
382
  return /*#__PURE__*/_jsx(Table.Cell, {
314
383
  modes: modes,
315
384
  flex: 1,
385
+ width: useFixedWidth ? columnWidth : undefined,
316
386
  align: "left",
317
387
  isLastColumn: isLastCol,
318
388
  isLastRow: isLastRow,
@@ -366,31 +436,126 @@ function CompareTable({
366
436
  })
367
437
  })]
368
438
  });
369
- return /*#__PURE__*/_jsxs(View, {
370
- style: [{
371
- width: '100%',
372
- backgroundColor: cardBg,
373
- borderWidth: 1,
374
- borderColor: '#e8e8e8',
375
- overflow: 'hidden'
376
- }, style],
377
- children: [(columnCount > 0 || showAddCard) && /*#__PURE__*/_jsxs(View, {
439
+
440
+ // Render a single section in fixed-width scroll mode. Unlike `renderTable`
441
+ // (used in flex mode), the gray header row is rendered full-width OUTSIDE
442
+ // the body's horizontal ScrollView so it stays put horizontally (it does
443
+ // not slide with the cells) while still scrolling vertically with the table.
444
+ // The body rows live inside a horizontal ScrollView that is scroll-synced
445
+ // with the cards row and every other section's body.
446
+ const renderSection = (section, index) => /*#__PURE__*/_jsxs(Accordion, {
447
+ title: section.title,
448
+ defaultExpanded: section.defaultExpanded ?? index === 0,
449
+ modes: modes,
450
+ children: [section.header != null && /*#__PURE__*/_jsx(View, {
378
451
  style: {
379
- flexDirection: 'row',
380
- width: '100%'
452
+ width: '100%',
453
+ backgroundColor: headerBg,
454
+ paddingHorizontal: headerPaddingH,
455
+ paddingVertical: headerPaddingV,
456
+ borderBottomWidth: cellBorderSize,
457
+ borderBottomColor: cellBorderColor
381
458
  },
382
- children: [columns.map(renderCard), showAddCard && renderAddCard()]
383
- }), /*#__PURE__*/_jsx(View, {
459
+ children: /*#__PURE__*/_jsx(Text, {
460
+ style: headerTextStyle,
461
+ children: section.header
462
+ })
463
+ }), /*#__PURE__*/_jsx(ScrollView, {
464
+ ref: getBodyRefCallback(index),
465
+ horizontal: true,
466
+ showsHorizontalScrollIndicator: false,
467
+ scrollEventThrottle: 16,
468
+ onScroll: onBodyScroll(index),
469
+ children: /*#__PURE__*/_jsx(View, {
470
+ style: {
471
+ width: contentWidth
472
+ },
473
+ children: section.rows.map((row, rowIndex) => {
474
+ const isLastRow = rowIndex === section.rows.length - 1;
475
+ return /*#__PURE__*/_jsxs(View, {
476
+ style: {
477
+ flexDirection: 'row',
478
+ width: '100%'
479
+ },
480
+ children: [Array.from({
481
+ length: columnCount
482
+ }).map((_, colIndex) => renderTableCell(renderCellContent(row.values?.[colIndex], `${rowIndex}-${colIndex}`), colIndex, isLastRow, colIndex)), showAddCard && renderTableCell(null, gridColumnCount - 1, isLastRow, '__add_spacer__')]
483
+ }, row.key ?? rowIndex);
484
+ })
485
+ })
486
+ })]
487
+ }, section.key ?? section.title ?? index);
488
+ const outerStyle = {
489
+ width: '100%',
490
+ backgroundColor: cardBg,
491
+ borderWidth: 1,
492
+ borderColor: '#e8e8e8',
493
+ overflow: 'hidden'
494
+ };
495
+
496
+ // --- flex mode (columnWidth omitted): original behaviour, unchanged -------
497
+ if (!useFixedWidth) {
498
+ return /*#__PURE__*/_jsxs(View, {
499
+ style: [outerStyle, style],
500
+ children: [showCardsRow && /*#__PURE__*/_jsxs(View, {
501
+ style: {
502
+ flexDirection: 'row',
503
+ width: '100%'
504
+ },
505
+ children: [columns.map(renderCard), showAddCard && renderAddCard()]
506
+ }), /*#__PURE__*/_jsx(View, {
507
+ style: {
508
+ width: '100%'
509
+ },
510
+ children: sections.map((section, index) => /*#__PURE__*/_jsx(Accordion, {
511
+ title: section.title,
512
+ defaultExpanded: section.defaultExpanded ?? index === 0,
513
+ modes: modes,
514
+ children: renderTable(section)
515
+ }, section.key ?? section.title ?? index))
516
+ })]
517
+ });
518
+ }
519
+
520
+ // --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
521
+ // The cards row (index 0) is a sticky header of the vertical ScrollView so
522
+ // it pins to the top while the sections scroll beneath. The cards row is
523
+ // itself a horizontal ScrollView, synced with every section body. For the
524
+ // vertical scroll (and thus the sticky behaviour) to engage, the host must
525
+ // give CompareTable a bounded height (e.g. a sized container or flex:1).
526
+ return /*#__PURE__*/_jsx(View, {
527
+ style: [outerStyle, style, {
528
+ flexDirection: 'column',
529
+ flex: 1
530
+ }],
531
+ children: /*#__PURE__*/_jsxs(ScrollView, {
384
532
  style: {
385
- width: '100%'
533
+ width: '100%',
534
+ flex: 1
386
535
  },
387
- children: sections.map((section, index) => /*#__PURE__*/_jsx(Accordion, {
388
- title: section.title,
389
- defaultExpanded: section.defaultExpanded ?? index === 0,
390
- modes: modes,
391
- children: renderTable(section)
392
- }, section.key ?? section.title ?? index))
393
- })]
536
+ stickyHeaderIndices: showCardsRow ? [0] : [],
537
+ showsVerticalScrollIndicator: false,
538
+ directionalLockEnabled: true,
539
+ children: [showCardsRow && /*#__PURE__*/_jsx(ScrollView, {
540
+ ref: cardsScrollRef,
541
+ horizontal: true,
542
+ showsHorizontalScrollIndicator: false,
543
+ scrollEventThrottle: 16,
544
+ onScroll: onCardsScroll,
545
+ children: /*#__PURE__*/_jsxs(View, {
546
+ style: {
547
+ flexDirection: 'row',
548
+ width: contentWidth
549
+ },
550
+ children: [columns.map(renderCard), showAddCard && renderAddCard()]
551
+ })
552
+ }), /*#__PURE__*/_jsx(View, {
553
+ style: {
554
+ width: '100%'
555
+ },
556
+ children: sections.map(renderSection)
557
+ })]
558
+ })
394
559
  });
395
560
  }
396
561
  export default CompareTable;
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
2
 
3
- import React, { useContext, useMemo } from 'react';
4
- import { Platform, Text, View } from 'react-native';
5
- import Animated, { useAnimatedKeyboard, useAnimatedStyle } from 'react-native-reanimated';
3
+ import React, { useContext, useEffect, useMemo } from 'react';
4
+ import { Platform, Text, View, useWindowDimensions } from 'react-native';
5
+ import Animated, { useAnimatedKeyboard, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
6
6
  import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
7
7
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
8
8
  import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
9
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  const IS_WEB = Platform.OS === 'web';
11
+
12
+ // Spring config — mirrors the Drawer's bottom pop-up spring so the two
13
+ // bottom-sheet surfaces share the exact same entrance feel (slight bounce,
14
+ // snappy). Keep in sync with `Drawer.tsx` SPRING_CONFIG.
15
+ const SPRING_CONFIG = {
16
+ damping: 32,
17
+ stiffness: 300,
18
+ mass: 1,
19
+ overshootClamping: false,
20
+ restDisplacementThreshold: 0.1,
21
+ restSpeedThreshold: 0.1
22
+ };
11
23
  function resolveContentSheetStyle(modes) {
12
24
  const backgroundColor = getVariableByName('contentSheet/bg', modes);
13
25
  const gap = getVariableByName('contentSheet/gap', modes);
@@ -91,6 +103,7 @@ function ContentSheet({
91
103
  keyboardSpacing = 0,
92
104
  safeAreaBottom = true,
93
105
  pinToBottom = true,
106
+ visible = true,
94
107
  style,
95
108
  ...rest
96
109
  }) {
@@ -106,10 +119,25 @@ function ContentSheet({
106
119
  paddingBottom
107
120
  }, style], [pinToBottom, resolved.container, paddingBottom, style]);
108
121
 
122
+ // Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
123
+ // sheet is bottom-anchored, so a positive `translateY` equal to the window
124
+ // height parks it fully below the bottom edge. Springing it back to `0`
125
+ // produces the same bottom pop-up the Drawer uses. The shared value is
126
+ // initialised off-screen so the very first mount pops in (no flash).
127
+ const {
128
+ height: windowHeight
129
+ } = useWindowDimensions();
130
+ const entrance = useSharedValue(windowHeight);
131
+ useEffect(() => {
132
+ entrance.value = withSpring(visible ? 0 : windowHeight, SPRING_CONFIG);
133
+ }, [visible, windowHeight, entrance]);
134
+
109
135
  // Switching between the keyboard-aware and plain implementation is keyed off
110
136
  // `avoidKeyboard` (and platform). Because they are distinct component types,
111
137
  // React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
112
- // inside `KeyboardAwareSheet` always runs in a stable hook order.
138
+ // inside `KeyboardAwareSheet` always runs in a stable hook order. Both
139
+ // branches receive the `entrance` shared value so the bottom pop-up plays in
140
+ // every configuration.
113
141
  const header = title ? /*#__PURE__*/_jsx(View, {
114
142
  style: resolved.headerStyle,
115
143
  children: /*#__PURE__*/_jsx(Text, {
@@ -121,12 +149,14 @@ function ContentSheet({
121
149
  return /*#__PURE__*/_jsxs(KeyboardAwareSheet, {
122
150
  style: containerStyle,
123
151
  spacing: keyboardSpacing,
152
+ entrance: entrance,
124
153
  ...rest,
125
154
  children: [header, processedChildren]
126
155
  });
127
156
  }
128
- return /*#__PURE__*/_jsxs(View, {
157
+ return /*#__PURE__*/_jsxs(AnimatedSheet, {
129
158
  style: containerStyle,
159
+ entrance: entrance,
130
160
  ...rest,
131
161
  children: [header, processedChildren]
132
162
  });
@@ -134,25 +164,55 @@ function ContentSheet({
134
164
  /**
135
165
  * Native-only wrapper that lifts the sheet by the live keyboard height. The
136
166
  * translation is computed on the UI thread from `useAnimatedKeyboard`, so the
137
- * sheet tracks the keyboard frame-for-frame without any JS work.
167
+ * sheet tracks the keyboard frame-for-frame without any JS work. The keyboard
168
+ * offset is summed with the entrance `translateY` (the bottom pop-up) inside a
169
+ * single `useAnimatedStyle` so both motions stay on the UI thread and never
170
+ * fight each other.
138
171
  */
139
172
  function KeyboardAwareSheet({
140
173
  style,
141
174
  spacing,
175
+ entrance,
142
176
  children,
143
177
  ...rest
144
178
  }) {
145
179
  const keyboard = useAnimatedKeyboard();
146
- const keyboardStyle = useAnimatedStyle(() => {
180
+ const animatedStyle = useAnimatedStyle(() => {
147
181
  const height = keyboard.height.value;
182
+ const keyboardOffset = height > 0 ? -(height + spacing) : 0;
183
+ return {
184
+ transform: [{
185
+ translateY: entrance.value + keyboardOffset
186
+ }]
187
+ };
188
+ });
189
+ return /*#__PURE__*/_jsx(Animated.View, {
190
+ style: [style, animatedStyle],
191
+ ...rest,
192
+ children: children
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Entrance-only animated sheet (web, or when keyboard avoidance is disabled).
198
+ * Applies the same bottom pop-up `translateY` as `KeyboardAwareSheet`, minus
199
+ * the keyboard offset.
200
+ */
201
+ function AnimatedSheet({
202
+ style,
203
+ entrance,
204
+ children,
205
+ ...rest
206
+ }) {
207
+ const animatedStyle = useAnimatedStyle(() => {
148
208
  return {
149
209
  transform: [{
150
- translateY: height > 0 ? -(height + spacing) : 0
210
+ translateY: entrance.value
151
211
  }]
152
212
  };
153
213
  });
154
214
  return /*#__PURE__*/_jsx(Animated.View, {
155
- style: [style, keyboardStyle],
215
+ style: [style, animatedStyle],
156
216
  ...rest,
157
217
  children: children
158
218
  });
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
 
3
- import React, { useMemo, useEffect, useRef } from 'react';
4
- import { View, Text, Pressable, Animated } from 'react-native';
3
+ import React, { useMemo, useEffect, useRef, useState, useCallback } from 'react';
4
+ import { View, Text, TextInput, Pressable, Animated, Platform, Keyboard } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES } from '../../utils/react-utils';
7
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  // Map of common ISO 4217 currency codes to display symbols
9
9
  const CURRENCY_SYMBOLS = {
10
10
  INR: '₹',
@@ -56,6 +56,8 @@ function MoneyValue({
56
56
  negative,
57
57
  focused = false,
58
58
  hidden = false,
59
+ editable = false,
60
+ onValueChange,
59
61
  modes = EMPTY_MODES,
60
62
  style,
61
63
  valueStyle,
@@ -67,24 +69,51 @@ function MoneyValue({
67
69
  ...rest
68
70
  }) {
69
71
  // Auto-detect negative from value and compute display value
70
- const {
71
- isNegative,
72
- displayValue
73
- } = useMemo(() => {
74
- const stringValue = String(value);
75
- const trimmed = stringValue.trim();
76
- const valueIsNegative = trimmed.startsWith('-') || typeof value === 'number' && value < 0;
72
+ const stringValue = String(value);
73
+ const trimmed = stringValue.trim();
74
+ const valueIsNegative = trimmed.startsWith('-') || typeof value === 'number' && value < 0;
77
75
 
78
- // Strip leading minus sign for display (we show it separately)
79
- const absoluteValue = trimmed.startsWith('-') ? trimmed.slice(1) : trimmed;
76
+ // Strip leading minus sign for display (we show it separately)
77
+ const absoluteValue = trimmed.startsWith('-') ? trimmed.slice(1) : trimmed;
80
78
 
81
- // Use explicit negative prop if provided, otherwise use auto-detected
82
- const showNegative = negative !== undefined ? negative : valueIsNegative;
83
- return {
84
- isNegative: hidden ? false : showNegative,
85
- displayValue: hidden ? '•••' : absoluteValue
86
- };
87
- }, [value, negative, hidden]);
79
+ // Use explicit negative prop if provided, otherwise use auto-detected
80
+ const showNegative = negative !== undefined ? negative : valueIsNegative;
81
+ const isNegative = hidden ? false : showNegative;
82
+ const displayValue = hidden ? '•••' : absoluteValue;
83
+
84
+ // Inline editing state
85
+ const [isEditing, setIsEditing] = useState(false);
86
+ const [editBuffer, setEditBuffer] = useState('');
87
+ const inputRef = useRef(null);
88
+ // Always-current snapshot of the display value for callbacks to reference.
89
+ const displayValueRef = useRef(displayValue);
90
+ displayValueRef.current = displayValue;
91
+ const enterEditMode = useCallback(() => {
92
+ setIsEditing(true);
93
+ setEditBuffer(displayValueRef.current);
94
+ // Focus the input on next frame so the keyboard opens
95
+ setTimeout(() => inputRef.current?.focus(), 0);
96
+ }, []);
97
+ const commitEdit = useCallback(() => {
98
+ setIsEditing(false);
99
+ const resolved = editBuffer || '0';
100
+ if (resolved !== displayValueRef.current) {
101
+ onValueChange?.(resolved);
102
+ }
103
+ }, [editBuffer, onValueChange]);
104
+ const handleChangeText = useCallback(text => {
105
+ // Allow only digits and at most one decimal point
106
+ const filtered = text.replace(/[^0-9.]/g, '');
107
+ const parts = filtered.split('.');
108
+ const clean = parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : filtered;
109
+ setEditBuffer(clean);
110
+ }, []);
111
+ const handlePress = useCallback(event => {
112
+ if (editable) {
113
+ enterEditMode();
114
+ }
115
+ onPress?.(event);
116
+ }, [editable, enterEditMode, onPress]);
88
117
 
89
118
  // Resolve typography and layout tokens from Figma
90
119
  const textColor = getVariableByName('moneyValue/text/color', modes);
@@ -145,44 +174,140 @@ function MoneyValue({
145
174
  cursorOpacity.setValue(0);
146
175
  }
147
176
  }, [focused, cursorOpacity]);
148
- return /*#__PURE__*/_jsxs(Pressable, {
149
- style: [containerStyle, style],
150
- accessibilityRole: "text",
151
- accessibilityLabel: undefined,
152
- accessibilityHint: accessibilityHint,
153
- onPress: onPress,
154
- disabled: !onPress,
155
- ...rest,
156
- children: [isNegative && /*#__PURE__*/_jsx(Text, {
157
- style: [currencyTextStyle, negativeSignStyle],
158
- accessibilityElementsHidden: true,
159
- importantForAccessibility: "no",
160
- children: "-"
161
- }), /*#__PURE__*/_jsx(Text, {
162
- style: [currencyTextStyle, currencyStyle],
163
- accessibilityElementsHidden: true,
164
- importantForAccessibility: "no",
165
- children: resolvedCurrency
166
- }), /*#__PURE__*/_jsxs(View, {
167
- style: {
168
- flexDirection: 'row',
169
- alignItems: 'center'
170
- },
171
- children: [/*#__PURE__*/_jsx(Text, {
172
- style: [valueTextStyle, valueStyle],
173
- accessibilityElementsHidden: true,
174
- importantForAccessibility: "no",
175
- children: displayValue
176
- }), focused && /*#__PURE__*/_jsx(Animated.View, {
177
- style: {
178
- opacity: cursorOpacity,
179
- width: 2,
180
- height: valueFontSize * 1.1,
181
- backgroundColor: textColor,
182
- marginLeft: 2
183
- }
184
- })]
185
- })]
177
+
178
+ // Keyboard avoidance — lift the component when the keyboard would cover it.
179
+ // Follows the same pattern as ActionFooter (uses Keyboard.addListener).
180
+ // Resets when editing ends or the keyboard hides.
181
+ const rootRef = useRef(null);
182
+ const keyboardLift = useRef(new Animated.Value(0)).current;
183
+ const rootWindowYRef = useRef(0);
184
+ const rootMeasuredHeightRef = useRef(0);
185
+ const measureRoot = useCallback(() => {
186
+ rootRef.current?.measureInWindow((_x, y, _w, h) => {
187
+ rootWindowYRef.current = y;
188
+ rootMeasuredHeightRef.current = h;
189
+ });
190
+ }, []);
191
+ const handleRootLayout = useCallback(_e => {
192
+ // measureInWindow gives the component's position relative to the window,
193
+ // which is what we need for keyboard overlap calculation. onLayout fires
194
+ // after the component mounts and whenever its size changes.
195
+ measureRoot();
196
+ }, [measureRoot]);
197
+ useEffect(() => {
198
+ if (!editable) return;
199
+ const showSub = Keyboard.addListener('keyboardDidShow', e => {
200
+ measureRoot();
201
+ if (!isEditing) return;
202
+ const kbHeight = e.endCoordinates.height;
203
+ const {
204
+ height: screenH
205
+ } = require('react-native').Dimensions.get('window');
206
+ const componentBottom = rootWindowYRef.current + rootMeasuredHeightRef.current;
207
+ const visibleBottom = screenH - kbHeight;
208
+ const overlap = componentBottom - visibleBottom + 12;
209
+ if (overlap > 0) {
210
+ Animated.timing(keyboardLift, {
211
+ toValue: -overlap,
212
+ duration: e.duration ?? 250,
213
+ useNativeDriver: true
214
+ }).start();
215
+ }
216
+ });
217
+ const hideSub = Keyboard.addListener('keyboardDidHide', () => {
218
+ Animated.timing(keyboardLift, {
219
+ toValue: 0,
220
+ duration: 150,
221
+ useNativeDriver: true
222
+ }).start();
223
+ });
224
+ return () => {
225
+ showSub.remove();
226
+ hideSub.remove();
227
+ };
228
+ }, [editable, isEditing, keyboardLift, measureRoot]);
229
+
230
+ // Reset lift when editing ends
231
+ useEffect(() => {
232
+ if (!isEditing) {
233
+ keyboardLift.setValue(0);
234
+ }
235
+ }, [isEditing, keyboardLift]);
236
+ const handleBlur = useCallback(() => {
237
+ commitEdit();
238
+ }, [commitEdit]);
239
+ const handleSubmitEditing = useCallback(() => {
240
+ commitEdit();
241
+ }, [commitEdit]);
242
+ return /*#__PURE__*/_jsx(Animated.View, {
243
+ style: {
244
+ transform: [{
245
+ translateY: keyboardLift
246
+ }]
247
+ },
248
+ children: /*#__PURE__*/_jsx(View, {
249
+ ref: rootRef,
250
+ onLayout: handleRootLayout,
251
+ children: /*#__PURE__*/_jsxs(Pressable, {
252
+ style: [containerStyle, style],
253
+ accessibilityRole: "text",
254
+ accessibilityLabel: undefined,
255
+ accessibilityHint: accessibilityHint,
256
+ onPress: handlePress,
257
+ disabled: !onPress && !editable,
258
+ ...rest,
259
+ children: [isNegative && /*#__PURE__*/_jsx(Text, {
260
+ style: [currencyTextStyle, negativeSignStyle],
261
+ accessibilityElementsHidden: true,
262
+ importantForAccessibility: "no",
263
+ children: "-"
264
+ }), /*#__PURE__*/_jsx(Text, {
265
+ style: [currencyTextStyle, currencyStyle],
266
+ accessibilityElementsHidden: true,
267
+ importantForAccessibility: "no",
268
+ children: resolvedCurrency
269
+ }), /*#__PURE__*/_jsx(View, {
270
+ style: {
271
+ flexDirection: 'row',
272
+ alignItems: 'center'
273
+ },
274
+ children: isEditing ? /*#__PURE__*/_jsx(TextInput, {
275
+ ref: inputRef,
276
+ value: editBuffer,
277
+ onChangeText: handleChangeText,
278
+ onBlur: handleBlur,
279
+ onSubmitEditing: handleSubmitEditing,
280
+ keyboardType: "decimal-pad",
281
+ returnKeyType: "done",
282
+ selectTextOnFocus: true,
283
+ style: [valueTextStyle, valueStyle, {
284
+ padding: 0,
285
+ margin: 0,
286
+ // On web a thin border helps the cursor appear precisely
287
+ ...(Platform.OS === 'web' ? {
288
+ outlineStyle: 'none'
289
+ } : {})
290
+ }],
291
+ accessibilityLabel: accessibilityLabel || 'Edit value'
292
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
293
+ children: [/*#__PURE__*/_jsx(Text, {
294
+ style: [valueTextStyle, valueStyle],
295
+ accessibilityElementsHidden: true,
296
+ importantForAccessibility: "no",
297
+ children: displayValue
298
+ }), focused && /*#__PURE__*/_jsx(Animated.View, {
299
+ style: {
300
+ opacity: cursorOpacity,
301
+ width: 2,
302
+ height: valueFontSize * 1.1,
303
+ backgroundColor: textColor,
304
+ marginLeft: 2
305
+ }
306
+ })]
307
+ })
308
+ })]
309
+ })
310
+ })
186
311
  });
187
312
  }
188
313
  export default MoneyValue;