jfs-components 0.1.25 → 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 +53 -0
- package/lib/commonjs/components/AutoplayControl/AutoplayControl.js +66 -0
- package/lib/commonjs/components/Carousel/Carousel.js +112 -19
- package/lib/commonjs/components/CompareTable/CompareTable.js +191 -25
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +104 -10
- package/lib/commonjs/components/FormUpload/Additem.js +168 -0
- package/lib/commonjs/components/FormUpload/FormUpload.js +198 -0
- package/lib/commonjs/components/Grid/Grid.js +206 -0
- package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
- package/lib/commonjs/components/NumberPagination/NumberPagination.js +144 -0
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/commonjs/components/ProfileCard/ProfileCard.js +208 -0
- package/lib/commonjs/components/Table/Table.js +27 -7
- package/lib/commonjs/components/UpiHandle/UpiHandle.js +23 -11
- package/lib/commonjs/components/index.js +42 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +21 -10
- package/lib/commonjs/icons/components/IconBrokenimage.js +19 -0
- package/lib/commonjs/icons/components/IconImage.js +19 -0
- package/lib/commonjs/icons/components/IconMic.js +19 -0
- package/lib/commonjs/icons/components/IconMicoff.js +19 -0
- package/lib/commonjs/icons/components/IconPause.js +19 -0
- package/lib/commonjs/icons/components/IconPlay.js +19 -0
- package/lib/commonjs/icons/components/index.js +66 -0
- package/lib/commonjs/icons/registry.js +2 -2
- package/lib/module/components/AutoplayControl/AutoplayControl.js +60 -0
- package/lib/module/components/Carousel/Carousel.js +111 -19
- package/lib/module/components/CompareTable/CompareTable.js +191 -26
- package/lib/module/components/ContentSheet/ContentSheet.js +108 -14
- package/lib/module/components/FormUpload/Additem.js +162 -0
- package/lib/module/components/FormUpload/FormUpload.js +192 -0
- package/lib/module/components/Grid/Grid.js +201 -0
- package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
- package/lib/module/components/NumberPagination/NumberPagination.js +138 -0
- package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/module/components/ProfileCard/ProfileCard.js +202 -0
- package/lib/module/components/Table/Table.js +27 -7
- package/lib/module/components/UpiHandle/UpiHandle.js +23 -11
- package/lib/module/components/index.js +7 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/design-tokens/figma-modes.generated.js +21 -10
- package/lib/module/icons/components/IconBrokenimage.js +12 -0
- package/lib/module/icons/components/IconImage.js +12 -0
- package/lib/module/icons/components/IconMic.js +12 -0
- package/lib/module/icons/components/IconMicoff.js +12 -0
- package/lib/module/icons/components/IconPause.js +12 -0
- package/lib/module/icons/components/IconPlay.js +12 -0
- package/lib/module/icons/components/index.js +6 -0
- package/lib/module/icons/registry.js +2 -2
- package/lib/typescript/src/components/AutoplayControl/AutoplayControl.d.ts +15 -0
- package/lib/typescript/src/components/Carousel/Carousel.d.ts +8 -1
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +21 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -1
- package/lib/typescript/src/components/FormUpload/Additem.d.ts +96 -0
- package/lib/typescript/src/components/FormUpload/FormUpload.d.ts +85 -0
- package/lib/typescript/src/components/Grid/Grid.d.ts +87 -0
- package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
- package/lib/typescript/src/components/NumberPagination/NumberPagination.d.ts +21 -0
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
- package/lib/typescript/src/components/ProfileCard/ProfileCard.d.ts +89 -0
- package/lib/typescript/src/components/Table/Table.d.ts +9 -1
- package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +7 -1
- package/lib/typescript/src/components/index.d.ts +34 -28
- package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +10 -4
- package/lib/typescript/src/icons/components/IconBrokenimage.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconImage.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconMic.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconMicoff.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconPause.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconPlay.d.ts +3 -0
- package/lib/typescript/src/icons/components/index.d.ts +6 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/AutoplayControl/AutoplayControl.tsx +77 -0
- package/src/components/Carousel/Carousel.tsx +133 -23
- package/src/components/CompareTable/CompareTable.tsx +224 -30
- package/src/components/ContentSheet/ContentSheet.tsx +138 -8
- package/src/components/FormUpload/Additem.tsx +262 -0
- package/src/components/FormUpload/FormUpload.tsx +313 -0
- package/src/components/Grid/Grid.tsx +368 -0
- package/src/components/MoneyValue/MoneyValue.tsx +216 -65
- package/src/components/NumberPagination/NumberPagination.tsx +170 -0
- package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
- package/src/components/ProfileCard/ProfileCard.tsx +268 -0
- package/src/components/Table/Table.tsx +29 -4
- package/src/components/UpiHandle/UpiHandle.tsx +35 -24
- package/src/components/index.ts +329 -182
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/design-tokens/figma-modes.generated.ts +21 -10
- package/src/icons/components/IconBrokenimage.tsx +11 -0
- package/src/icons/components/IconImage.tsx +11 -0
- package/src/icons/components/IconMic.tsx +11 -0
- package/src/icons/components/IconMicoff.tsx +11 -0
- package/src/icons/components/IconPause.tsx +11 -0
- package/src/icons/components/IconPlay.tsx +11 -0
- package/src/icons/components/index.ts +6 -0
- package/src/icons/registry.ts +25 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
import { Pressable } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import Icon from '../../icons/Icon';
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
function resolveAutoplayControlTokens(modes, disabled) {
|
|
9
|
+
const width = getVariableByName('autoplayControl/width', modes) ?? 36;
|
|
10
|
+
const height = getVariableByName('autoplayControl/height', modes) ?? 36;
|
|
11
|
+
const radiusRaw = getVariableByName('autoplayControl/radius', modes) ?? 9999;
|
|
12
|
+
const backgroundColor = getVariableByName('autoplayControl/background/color', modes) ?? '#ffffff';
|
|
13
|
+
const iconColor = getVariableByName('autoplayControl/icon/color', modes) ?? '#000000';
|
|
14
|
+
const borderRadius = radiusRaw === 9999 ? width / 2 : radiusRaw;
|
|
15
|
+
return {
|
|
16
|
+
containerStyle: {
|
|
17
|
+
width,
|
|
18
|
+
height,
|
|
19
|
+
borderRadius,
|
|
20
|
+
backgroundColor,
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
opacity: disabled ? 0.5 : 1
|
|
24
|
+
},
|
|
25
|
+
iconColor
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* AutoplayControl is a button component that toggles between play and pause states.
|
|
31
|
+
* All styling values are resolved from Figma design tokens.
|
|
32
|
+
*/
|
|
33
|
+
export default function AutoplayControl({
|
|
34
|
+
state = 'pause',
|
|
35
|
+
modes,
|
|
36
|
+
onPress,
|
|
37
|
+
disabled = false,
|
|
38
|
+
style
|
|
39
|
+
}) {
|
|
40
|
+
const tokens = useMemo(() => resolveAutoplayControlTokens(modes ?? {}, disabled), [modes, disabled]);
|
|
41
|
+
const iconName = state === 'play' ? 'ic_play' : 'ic_pause';
|
|
42
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
43
|
+
onPress: disabled ? undefined : onPress,
|
|
44
|
+
disabled: disabled,
|
|
45
|
+
style: ({
|
|
46
|
+
pressed
|
|
47
|
+
}) => [tokens.containerStyle, pressed && {
|
|
48
|
+
opacity: 0.7
|
|
49
|
+
}, style],
|
|
50
|
+
accessibilityRole: "button",
|
|
51
|
+
accessibilityState: {
|
|
52
|
+
disabled
|
|
53
|
+
},
|
|
54
|
+
children: /*#__PURE__*/_jsx(Icon, {
|
|
55
|
+
name: iconName,
|
|
56
|
+
size: 24,
|
|
57
|
+
color: tokens.iconColor
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -4,6 +4,8 @@ import React, { createContext, useContext, useRef, useState, useEffect, useCallb
|
|
|
4
4
|
import { View, ScrollView, Animated, Pressable } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
+
import AutoplayControl from '../AutoplayControl/AutoplayControl';
|
|
8
|
+
import NumberPagination from '../NumberPagination/NumberPagination';
|
|
7
9
|
|
|
8
10
|
// ---------------------------------------------------------------------------
|
|
9
11
|
// Context
|
|
@@ -37,9 +39,11 @@ export function Carousel({
|
|
|
37
39
|
itemWidth: itemWidthProp,
|
|
38
40
|
paddingHorizontal: paddingHorizontalProp,
|
|
39
41
|
paddingVertical: paddingVerticalProp,
|
|
42
|
+
type = 'Default',
|
|
40
43
|
onIndexChange,
|
|
41
44
|
style
|
|
42
45
|
}) {
|
|
46
|
+
const isNumbered = type === 'Numbered';
|
|
43
47
|
// ---- Token resolution ----
|
|
44
48
|
const tokenGap = parseFloat(getVariableByName('carousel/gap', modes));
|
|
45
49
|
const gap = gapProp ?? tokenGap;
|
|
@@ -47,27 +51,43 @@ export function Carousel({
|
|
|
47
51
|
const containerPaddingV = parseFloat(getVariableByName('carousel/padding/vertical', modes));
|
|
48
52
|
// Outer container max height per Figma (`carousel/maxHeight`).
|
|
49
53
|
const maxHeight = parseFloat(getVariableByName('carousel/maxHeight', modes));
|
|
54
|
+
// Gap between AutoplayControl and NumberPagination in the Numbered overlay.
|
|
55
|
+
const controlGap = parseFloat(getVariableByName('carouselControl/gap', modes));
|
|
56
|
+
|
|
50
57
|
// Spacing between the cards row and the pagination dots uses `carousel/gap`.
|
|
51
58
|
const paginationOffset = gap;
|
|
52
59
|
|
|
53
60
|
// ---- Refs & state ----
|
|
54
61
|
const scrollRef = useRef(null);
|
|
55
62
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
63
|
+
const activeIndexRef = useRef(0);
|
|
56
64
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
57
65
|
const autoPlayTimer = useRef(null);
|
|
58
66
|
const userInteracting = useRef(false);
|
|
67
|
+
// Guard: suppress handleScroll index updates during programmatic scrollTo
|
|
68
|
+
// animations so intermediate offsets don't flicker activeIndex back.
|
|
69
|
+
const programmaticScroll = useRef(false);
|
|
70
|
+
const programmaticTimer = useRef(null);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
activeIndexRef.current = activeIndex;
|
|
73
|
+
}, [activeIndex]);
|
|
74
|
+
|
|
75
|
+
// Interactive play/pause state for the Numbered variant.
|
|
76
|
+
const [isPlaying, setIsPlaying] = useState(autoPlay);
|
|
59
77
|
|
|
60
78
|
// Flatten children so we can count items
|
|
61
79
|
const items = useMemo(() => React.Children.toArray(children).filter(React.isValidElement), [children]);
|
|
62
80
|
const totalItems = items.length;
|
|
63
81
|
|
|
64
|
-
// Effective item width: provided prop, or full container width minus peek offsets
|
|
82
|
+
// Effective item width: provided prop, or full container width minus peek offsets.
|
|
83
|
+
// For the Numbered variant the items are full-bleed (no peek padding).
|
|
65
84
|
const effectiveItemWidth = useMemo(() => {
|
|
66
85
|
if (itemWidthProp != null) return itemWidthProp;
|
|
67
86
|
if (containerWidth === 0) return 0;
|
|
87
|
+
if (isNumbered) return containerWidth;
|
|
68
88
|
// Full-width minus peekOffset on each side
|
|
69
89
|
return containerWidth - containerPaddingH * 4;
|
|
70
|
-
}, [itemWidthProp, containerWidth, containerPaddingH]);
|
|
90
|
+
}, [itemWidthProp, containerWidth, containerPaddingH, isNumbered]);
|
|
71
91
|
|
|
72
92
|
// Snap interval = item width + gap
|
|
73
93
|
const snapInterval = effectiveItemWidth + gap;
|
|
@@ -81,6 +101,17 @@ export function Carousel({
|
|
|
81
101
|
} else {
|
|
82
102
|
targetIndex = Math.max(0, Math.min(index, totalItems - 1));
|
|
83
103
|
}
|
|
104
|
+
// Lock out handleScroll during the animated scroll so intermediate
|
|
105
|
+
// offsets don't flicker activeIndex back to the old value.
|
|
106
|
+
if (animated) {
|
|
107
|
+
programmaticScroll.current = true;
|
|
108
|
+
if (programmaticTimer.current != null) {
|
|
109
|
+
clearTimeout(programmaticTimer.current);
|
|
110
|
+
}
|
|
111
|
+
programmaticTimer.current = setTimeout(() => {
|
|
112
|
+
programmaticScroll.current = false;
|
|
113
|
+
}, 500);
|
|
114
|
+
}
|
|
84
115
|
scrollRef.current?.scrollTo({
|
|
85
116
|
x: targetIndex * snapInterval,
|
|
86
117
|
animated
|
|
@@ -95,6 +126,9 @@ export function Carousel({
|
|
|
95
126
|
// ---- Scroll event handler ----
|
|
96
127
|
const handleScroll = useCallback(event => {
|
|
97
128
|
if (snapInterval === 0) return;
|
|
129
|
+
// Skip index recalculation during programmatic scrollTo animations
|
|
130
|
+
// to prevent the active indicator from flickering back to the old page.
|
|
131
|
+
if (programmaticScroll.current) return;
|
|
98
132
|
const offsetX = event.nativeEvent.contentOffset.x;
|
|
99
133
|
const newIndex = Math.round(offsetX / snapInterval);
|
|
100
134
|
const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));
|
|
@@ -112,22 +146,19 @@ export function Carousel({
|
|
|
112
146
|
}
|
|
113
147
|
}, []);
|
|
114
148
|
const startAutoPlay = useCallback(() => {
|
|
115
|
-
if (!autoPlay || totalItems <= 1) return;
|
|
149
|
+
if (!autoPlay && !isPlaying || totalItems <= 1) return;
|
|
150
|
+
if (!isPlaying) return;
|
|
116
151
|
clearAutoPlay();
|
|
117
152
|
autoPlayTimer.current = setInterval(() => {
|
|
118
153
|
if (!userInteracting.current) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
onIndexChange?.(next);
|
|
126
|
-
return next;
|
|
127
|
-
});
|
|
154
|
+
const prev = activeIndexRef.current;
|
|
155
|
+
const next = loop ? (prev + 1) % totalItems : Math.min(prev + 1, totalItems - 1);
|
|
156
|
+
// Route through scrollToIndex so the programmaticScroll guard
|
|
157
|
+
// suppresses intermediate handleScroll offsets during the animation.
|
|
158
|
+
scrollToIndex(next, true);
|
|
128
159
|
}
|
|
129
160
|
}, autoPlayInterval);
|
|
130
|
-
}, [autoPlay, totalItems, loop,
|
|
161
|
+
}, [autoPlay, isPlaying, totalItems, loop, autoPlayInterval, clearAutoPlay, scrollToIndex]);
|
|
131
162
|
useEffect(() => {
|
|
132
163
|
startAutoPlay();
|
|
133
164
|
return clearAutoPlay;
|
|
@@ -138,8 +169,23 @@ export function Carousel({
|
|
|
138
169
|
const handleScrollEndDrag = useCallback(() => {
|
|
139
170
|
userInteracting.current = false;
|
|
140
171
|
// Reset autoplay timer after interaction
|
|
141
|
-
if (autoPlay) startAutoPlay();
|
|
142
|
-
}, [autoPlay, startAutoPlay]);
|
|
172
|
+
if (autoPlay || isPlaying) startAutoPlay();
|
|
173
|
+
}, [autoPlay, isPlaying, startAutoPlay]);
|
|
174
|
+
const finishProgrammaticScroll = useCallback(event => {
|
|
175
|
+
programmaticScroll.current = false;
|
|
176
|
+
if (programmaticTimer.current != null) {
|
|
177
|
+
clearTimeout(programmaticTimer.current);
|
|
178
|
+
programmaticTimer.current = null;
|
|
179
|
+
}
|
|
180
|
+
if (snapInterval === 0) return;
|
|
181
|
+
const offsetX = event.nativeEvent.contentOffset.x;
|
|
182
|
+
const newIndex = Math.round(offsetX / snapInterval);
|
|
183
|
+
const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));
|
|
184
|
+
if (clampedIndex !== activeIndexRef.current) {
|
|
185
|
+
setActiveIndex(clampedIndex);
|
|
186
|
+
onIndexChange?.(clampedIndex);
|
|
187
|
+
}
|
|
188
|
+
}, [snapInterval, totalItems, onIndexChange]);
|
|
143
189
|
|
|
144
190
|
// ---- Layout ----
|
|
145
191
|
const handleLayout = useCallback(e => {
|
|
@@ -156,14 +202,37 @@ export function Carousel({
|
|
|
156
202
|
goPrev
|
|
157
203
|
}), [modes, activeIndex, totalItems, goTo, goNext, goPrev]);
|
|
158
204
|
|
|
205
|
+
// ---- Toggle handler for Numbered variant ----
|
|
206
|
+
const handleToggleAutoplay = useCallback(() => {
|
|
207
|
+
setIsPlaying(prev => {
|
|
208
|
+
if (prev) {
|
|
209
|
+
// Pausing
|
|
210
|
+
clearAutoPlay();
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
// Resuming — autoplay will restart via the effect
|
|
214
|
+
return true;
|
|
215
|
+
});
|
|
216
|
+
}, [clearAutoPlay]);
|
|
217
|
+
|
|
218
|
+
// Restart autoplay whenever isPlaying flips to true
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (isPlaying) startAutoPlay();else clearAutoPlay();
|
|
221
|
+
}, [isPlaying, startAutoPlay, clearAutoPlay]);
|
|
222
|
+
|
|
159
223
|
// ---- Render ----
|
|
160
224
|
// Explicit padding props win verbatim; otherwise fall back to the
|
|
161
225
|
// token-derived values (horizontal keeps its legacy ×2 "peek" multiplier).
|
|
162
|
-
|
|
163
|
-
const
|
|
226
|
+
// Numbered variant is full-bleed: zero padding.
|
|
227
|
+
const effectivePaddingH = isNumbered ? 0 : paddingHorizontalProp ?? containerPaddingH;
|
|
228
|
+
const effectivePaddingV = isNumbered ? 0 : paddingVerticalProp ?? containerPaddingV;
|
|
164
229
|
const outerStyle = {
|
|
165
230
|
paddingVertical: effectivePaddingV,
|
|
166
|
-
maxHeight
|
|
231
|
+
maxHeight: isNumbered ? undefined : maxHeight,
|
|
232
|
+
// Numbered needs relative positioning for the floating controls overlay.
|
|
233
|
+
...(isNumbered ? {
|
|
234
|
+
overflow: 'hidden'
|
|
235
|
+
} : {})
|
|
167
236
|
};
|
|
168
237
|
const contentContainerStyle = {
|
|
169
238
|
paddingHorizontal: effectivePaddingH,
|
|
@@ -189,6 +258,7 @@ export function Carousel({
|
|
|
189
258
|
scrollEventThrottle: 16,
|
|
190
259
|
onScrollBeginDrag: handleScrollBeginDrag,
|
|
191
260
|
onScrollEndDrag: handleScrollEndDrag,
|
|
261
|
+
onMomentumScrollEnd: finishProgrammaticScroll,
|
|
192
262
|
children: items.map((child, index) => {
|
|
193
263
|
// Strict slot box: width must be honored; never grow or shrink with
|
|
194
264
|
// content, and clip anything that misbehaves (e.g. a child whose
|
|
@@ -225,11 +295,33 @@ export function Carousel({
|
|
|
225
295
|
children: childWithModes
|
|
226
296
|
}, index);
|
|
227
297
|
})
|
|
228
|
-
}), showPagination && totalItems > 1 && /*#__PURE__*/_jsx(Pagination, {
|
|
298
|
+
}), !isNumbered && showPagination && totalItems > 1 && /*#__PURE__*/_jsx(Pagination, {
|
|
229
299
|
modes: modes,
|
|
230
300
|
style: {
|
|
231
301
|
marginTop: paginationOffset
|
|
232
302
|
}
|
|
303
|
+
}), isNumbered && showPagination && totalItems > 1 && /*#__PURE__*/_jsxs(View, {
|
|
304
|
+
style: {
|
|
305
|
+
position: 'absolute',
|
|
306
|
+
bottom: 12,
|
|
307
|
+
left: 0,
|
|
308
|
+
right: 0,
|
|
309
|
+
flexDirection: 'row',
|
|
310
|
+
justifyContent: 'center',
|
|
311
|
+
alignItems: 'center',
|
|
312
|
+
gap: controlGap
|
|
313
|
+
},
|
|
314
|
+
pointerEvents: "box-none",
|
|
315
|
+
children: [/*#__PURE__*/_jsx(AutoplayControl, {
|
|
316
|
+
state: isPlaying ? 'pause' : 'play',
|
|
317
|
+
onPress: handleToggleAutoplay,
|
|
318
|
+
modes: modes
|
|
319
|
+
}), /*#__PURE__*/_jsx(NumberPagination, {
|
|
320
|
+
activePage: activeIndex + 1,
|
|
321
|
+
totalPages: totalItems,
|
|
322
|
+
onPageChange: page => goTo(page - 1),
|
|
323
|
+
modes: modes
|
|
324
|
+
})]
|
|
233
325
|
})]
|
|
234
326
|
})
|
|
235
327
|
});
|
|
@@ -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;
|