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.
- package/CHANGELOG.md +15 -2
- package/lib/commonjs/components/CompareTable/CompareTable.js +191 -25
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +66 -6
- package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/commonjs/components/Table/Table.js +27 -7
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CompareTable/CompareTable.js +191 -26
- package/lib/module/components/ContentSheet/ContentSheet.js +69 -9
- package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
- package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/module/components/Table/Table.js +27 -7
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +21 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +14 -1
- package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
- package/lib/typescript/src/components/Table/Table.d.ts +9 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CompareTable/CompareTable.tsx +224 -30
- package/src/components/ContentSheet/ContentSheet.tsx +88 -8
- package/src/components/MoneyValue/MoneyValue.tsx +216 -65
- package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
- package/src/components/Table/Table.tsx +29 -4
- 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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
380
|
-
|
|
452
|
+
width: '100%',
|
|
453
|
+
backgroundColor: headerBg,
|
|
454
|
+
paddingHorizontal: headerPaddingH,
|
|
455
|
+
paddingVertical: headerPaddingV,
|
|
456
|
+
borderBottomWidth: cellBorderSize,
|
|
457
|
+
borderBottomColor: cellBorderColor
|
|
381
458
|
},
|
|
382
|
-
children:
|
|
383
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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(
|
|
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
|
|
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:
|
|
210
|
+
translateY: entrance.value
|
|
151
211
|
}]
|
|
152
212
|
};
|
|
153
213
|
});
|
|
154
214
|
return /*#__PURE__*/_jsx(Animated.View, {
|
|
155
|
-
style: [style,
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
79
|
-
|
|
76
|
+
// Strip leading minus sign for display (we show it separately)
|
|
77
|
+
const absoluteValue = trimmed.startsWith('-') ? trimmed.slice(1) : trimmed;
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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;
|