jfs-components 0.0.77 → 0.0.78
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 +17 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
- package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
- package/lib/commonjs/components/FormField/FormField.js +14 -1
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
- package/lib/commonjs/components/ListItem/ListItem.js +25 -10
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/Stepper/Step.js +47 -60
- package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
- package/lib/commonjs/components/Stepper/Stepper.js +15 -17
- package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
- package/lib/commonjs/components/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +28 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
- package/lib/module/components/Checkbox/Checkbox.js +22 -10
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
- package/lib/module/components/FormField/FormField.js +16 -3
- package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
- package/lib/module/components/ListItem/ListItem.js +25 -10
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/Stepper/Step.js +48 -61
- package/lib/module/components/Stepper/StepLabel.js +40 -10
- package/lib/module/components/Stepper/Stepper.js +15 -17
- package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
- package/lib/module/components/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +4 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
- package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
- package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
- package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
- package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
- package/lib/typescript/src/components/index.d.ts +7 -3
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +56 -4
- package/src/components/Checkbox/Checkbox.tsx +22 -9
- package/src/components/DropdownInput/DropdownInput.tsx +67 -39
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
- package/src/components/FormField/FormField.tsx +19 -3
- package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
- package/src/components/ListItem/ListItem.tsx +21 -10
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/Stepper/Step.tsx +52 -51
- package/src/components/Stepper/StepLabel.tsx +46 -9
- package/src/components/Stepper/Stepper.tsx +20 -15
- package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
- package/src/components/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +7 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react';
|
|
3
|
+
import React, { useMemo, useState } from 'react';
|
|
4
4
|
import { View, Text, Pressable, LayoutAnimation, Platform, UIManager } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import Icon from '../../icons/Icon';
|
|
@@ -12,34 +12,45 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
|
13
13
|
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
14
14
|
}
|
|
15
|
+
function resolveAccordionStateMode(disabled, isExpanded, isHovered, contained) {
|
|
16
|
+
if (disabled) return 'Disabled';
|
|
17
|
+
if (contained) {
|
|
18
|
+
return isExpanded ? 'Open Hover' : 'Hover';
|
|
19
|
+
}
|
|
20
|
+
if (isExpanded) {
|
|
21
|
+
return isHovered ? 'Open Hover' : 'Open';
|
|
22
|
+
}
|
|
23
|
+
return isHovered ? 'Hover' : 'Idle';
|
|
24
|
+
}
|
|
25
|
+
function toFontWeight(value, fallback) {
|
|
26
|
+
if (typeof value === 'number') return String(value);
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
const normalized = value.trim().toLowerCase();
|
|
29
|
+
if (normalized === 'bold') return '700';
|
|
30
|
+
if (normalized === 'medium') return '500';
|
|
31
|
+
if (normalized === 'regular' || normalized === 'normal') return '400';
|
|
32
|
+
if (/^\d+$/.test(normalized)) return normalized;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
15
37
|
/**
|
|
16
38
|
* Accordion component that mirrors the Figma "Accordion" component.
|
|
17
39
|
*
|
|
18
|
-
*
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
* -
|
|
22
|
-
* - **Design-token driven styling** via `getVariableByName` and `modes`
|
|
40
|
+
* Supports two visual treatments via the `contained` prop:
|
|
41
|
+
* - **`contained={false}`** (default) — transparent header at rest; filled
|
|
42
|
+
* background on hover / press.
|
|
43
|
+
* - **`contained={true}`** — header always uses the filled background.
|
|
23
44
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
45
|
+
* Interaction states (Idle, Hover, Open, Disabled) are resolved automatically
|
|
46
|
+
* from `expanded`, `disabled`, hover, and `contained` — consumers should not
|
|
47
|
+
* pass `'Accordion States'` in `modes`.
|
|
27
48
|
*
|
|
28
49
|
* @component
|
|
29
|
-
* @param {Object} props
|
|
30
|
-
* @param {string} [props.title='Accordion title'] - The accordion header title
|
|
31
|
-
* @param {boolean} [props.defaultExpanded=false] - Initial expanded state
|
|
32
|
-
* @param {boolean} [props.expanded] - Controlled expanded state
|
|
33
|
-
* @param {Function} [props.onExpandedChange] - Callback fired when expanded state changes
|
|
34
|
-
* @param {boolean} [props.disabled=false] - Whether the accordion is disabled
|
|
35
|
-
* @param {React.ReactNode} [props.children] - Content to display when expanded
|
|
36
|
-
* @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens
|
|
37
|
-
* @param {Object} [props.style] - Optional container style overrides
|
|
38
|
-
* @param {string} [props.accessibilityLabel] - Accessibility label for the accordion. If not provided, uses title
|
|
39
|
-
* @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
|
|
40
50
|
*/
|
|
41
51
|
function Accordion({
|
|
42
52
|
title = 'Accordion title',
|
|
53
|
+
contained = false,
|
|
43
54
|
defaultExpanded = false,
|
|
44
55
|
expanded: controlledExpanded,
|
|
45
56
|
onExpandedChange,
|
|
@@ -53,21 +64,19 @@ function Accordion({
|
|
|
53
64
|
webAccessibilityProps,
|
|
54
65
|
...rest
|
|
55
66
|
}) {
|
|
56
|
-
// Internal state for uncontrolled mode
|
|
57
67
|
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
58
|
-
|
|
59
|
-
// Determine if controlled or uncontrolled
|
|
68
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
60
69
|
const isControlled = controlledExpanded !== undefined;
|
|
61
70
|
const isExpanded = isControlled ? controlledExpanded : internalExpanded;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
const resolvedModes = useMemo(() => {
|
|
72
|
+
const accordionState = resolveAccordionStateMode(disabled, isExpanded, isHovered, contained);
|
|
73
|
+
return {
|
|
74
|
+
...modes,
|
|
75
|
+
'Accordion States': accordionState
|
|
76
|
+
};
|
|
77
|
+
}, [contained, disabled, isExpanded, isHovered, modes]);
|
|
67
78
|
const handleToggle = () => {
|
|
68
79
|
if (disabled) return;
|
|
69
|
-
|
|
70
|
-
// Animate the layout change
|
|
71
80
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
72
81
|
if (isControlled) {
|
|
73
82
|
onExpandedChange?.(!isExpanded);
|
|
@@ -76,23 +85,20 @@ function Accordion({
|
|
|
76
85
|
onExpandedChange?.(!isExpanded);
|
|
77
86
|
}
|
|
78
87
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const borderColor = getVariableByName('accordion/border/color', modes) || '#e6e6e6';
|
|
94
|
-
|
|
95
|
-
// Styles
|
|
88
|
+
const titleColor = getVariableByName('accordion/title/color', resolvedModes) ?? '#0d0d0d';
|
|
89
|
+
const titleFontSize = getVariableByName('accordion/title/fontSize', resolvedModes) ?? 14;
|
|
90
|
+
const titleLineHeight = getVariableByName('accordion/title/lineHeight', resolvedModes) ?? 20;
|
|
91
|
+
const titleFontFamily = getVariableByName('accordion/title/fontFamily', resolvedModes) ?? 'System';
|
|
92
|
+
const titleFontWeight = toFontWeight(getVariableByName('accordion/title/fontWeight', resolvedModes), '700');
|
|
93
|
+
const iconColor = getVariableByName('accordion/icon/color', resolvedModes) ?? '#141414';
|
|
94
|
+
const iconSize = getVariableByName('accordion/icon/size', resolvedModes) ?? 24;
|
|
95
|
+
const headerGap = getVariableByName('accordion/header/gap', resolvedModes) ?? 12;
|
|
96
|
+
const headerPaddingVertical = getVariableByName('accordion/header/padding/vertical', resolvedModes) ?? 8;
|
|
97
|
+
const headerBackground = getVariableByName('accordion/header/background', resolvedModes) ?? 'transparent';
|
|
98
|
+
const contentGap = getVariableByName('accordion/content/gap', resolvedModes) ?? 12;
|
|
99
|
+
const contentPaddingTop = getVariableByName('accordion/content/padding/top', resolvedModes) ?? 8;
|
|
100
|
+
const contentPaddingBottom = getVariableByName('accordion/content/padding/bottom', resolvedModes) ?? 8;
|
|
101
|
+
const borderColor = getVariableByName('accordion/border/color', resolvedModes) ?? '#e6e6e6';
|
|
96
102
|
const containerStyle = {
|
|
97
103
|
borderBottomWidth: 1,
|
|
98
104
|
borderBottomColor: borderColor
|
|
@@ -112,7 +118,7 @@ function Accordion({
|
|
|
112
118
|
fontSize: titleFontSize,
|
|
113
119
|
lineHeight: titleLineHeight,
|
|
114
120
|
fontFamily: titleFontFamily,
|
|
115
|
-
fontWeight:
|
|
121
|
+
fontWeight: titleFontWeight
|
|
116
122
|
};
|
|
117
123
|
const contentStyle = {
|
|
118
124
|
backgroundColor: 'transparent',
|
|
@@ -122,11 +128,7 @@ function Accordion({
|
|
|
122
128
|
paddingHorizontal: 0,
|
|
123
129
|
overflow: 'hidden'
|
|
124
130
|
};
|
|
125
|
-
|
|
126
|
-
// Generate default accessibility label
|
|
127
131
|
const defaultAccessibilityLabel = accessibilityLabel || title;
|
|
128
|
-
|
|
129
|
-
// Web platform support
|
|
130
132
|
const webProps = usePressableWebSupport({
|
|
131
133
|
restProps: {},
|
|
132
134
|
onPress: handleToggle,
|
|
@@ -134,9 +136,7 @@ function Accordion({
|
|
|
134
136
|
accessibilityLabel: defaultAccessibilityLabel,
|
|
135
137
|
webAccessibilityProps
|
|
136
138
|
});
|
|
137
|
-
|
|
138
|
-
// Process children to pass modes
|
|
139
|
-
const processedChildren = children ? cloneChildrenWithModes(React.Children.toArray(children), modes) : null;
|
|
139
|
+
const processedChildren = children ? cloneChildrenWithModes(React.Children.toArray(children), resolvedModes) : null;
|
|
140
140
|
return /*#__PURE__*/_jsxs(View, {
|
|
141
141
|
style: [containerStyle, style],
|
|
142
142
|
...rest,
|
|
@@ -166,7 +166,7 @@ function Accordion({
|
|
|
166
166
|
}), /*#__PURE__*/_jsx(Icon, {
|
|
167
167
|
name: isExpanded ? 'ic_minus' : 'ic_add',
|
|
168
168
|
size: iconSize,
|
|
169
|
-
color:
|
|
169
|
+
color: iconColor,
|
|
170
170
|
accessibilityElementsHidden: true,
|
|
171
171
|
importantForAccessibility: "no"
|
|
172
172
|
})]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useMemo } from 'react';
|
|
4
|
-
import { View, Platform } from 'react-native';
|
|
3
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import { Animated, Keyboard, View, Platform } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
|
|
7
7
|
import IconButton from '../IconButton/IconButton';
|
|
@@ -105,6 +105,44 @@ function ActionFooter({
|
|
|
105
105
|
style,
|
|
106
106
|
accessibilityLabel
|
|
107
107
|
}) {
|
|
108
|
+
// -------------------------------------------------------------------------
|
|
109
|
+
// Keep the footer locked in place behind the software keyboard (Android).
|
|
110
|
+
// -------------------------------------------------------------------------
|
|
111
|
+
//
|
|
112
|
+
// The Android activity is configured with `windowSoftInputMode="adjustResize"`,
|
|
113
|
+
// which shrinks the app window by the keyboard height when the keyboard
|
|
114
|
+
// opens. A bottom-anchored footer therefore gets lifted UP by the keyboard
|
|
115
|
+
// height — exactly the jump the design does not want.
|
|
116
|
+
//
|
|
117
|
+
// To counteract that, we translate the footer back DOWN by the same keyboard
|
|
118
|
+
// height so it visually stays exactly where it was (now sitting behind the
|
|
119
|
+
// keyboard). iOS does not resize the window for the keyboard, so the footer
|
|
120
|
+
// already stays put there; we only run this on Android to avoid pushing the
|
|
121
|
+
// footer off-screen on platforms that don't lift it in the first place.
|
|
122
|
+
const keyboardOffset = useRef(new Animated.Value(0)).current;
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (Platform.OS !== 'android') return undefined;
|
|
125
|
+
const animateTo = (toValue, duration) => {
|
|
126
|
+
Animated.timing(keyboardOffset, {
|
|
127
|
+
toValue,
|
|
128
|
+
// Match the OS keyboard animation so the resize and our counter-shift
|
|
129
|
+
// cancel out smoothly with no visible footer movement.
|
|
130
|
+
duration: typeof duration === 'number' && duration > 0 ? duration : 150,
|
|
131
|
+
useNativeDriver: true
|
|
132
|
+
}).start();
|
|
133
|
+
};
|
|
134
|
+
const showSub = Keyboard.addListener('keyboardDidShow', e => {
|
|
135
|
+
animateTo(e?.endCoordinates?.height ?? 0, e?.duration);
|
|
136
|
+
});
|
|
137
|
+
const hideSub = Keyboard.addListener('keyboardDidHide', e => {
|
|
138
|
+
animateTo(0, e?.duration);
|
|
139
|
+
});
|
|
140
|
+
return () => {
|
|
141
|
+
showSub.remove();
|
|
142
|
+
hideSub.remove();
|
|
143
|
+
};
|
|
144
|
+
}, [keyboardOffset]);
|
|
145
|
+
|
|
108
146
|
// All token reads collapsed into a single useMemo keyed on `modes`. With
|
|
109
147
|
// the shared `EMPTY_MODES` default this resolves once for the common path
|
|
110
148
|
// and never re-allocates the container/slot style objects between renders.
|
|
@@ -163,8 +201,16 @@ function ActionFooter({
|
|
|
163
201
|
});
|
|
164
202
|
});
|
|
165
203
|
}, [children, modes]);
|
|
166
|
-
return /*#__PURE__*/_jsx(View, {
|
|
167
|
-
style: [containerStyle, WEB_SHADOW, style
|
|
204
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
205
|
+
style: [containerStyle, WEB_SHADOW, style,
|
|
206
|
+
// Counter-translate by the keyboard height on Android so `adjustResize`
|
|
207
|
+
// can't lift the footer above the keyboard (no-op on iOS/web where the
|
|
208
|
+
// value stays at 0).
|
|
209
|
+
{
|
|
210
|
+
transform: [{
|
|
211
|
+
translateY: keyboardOffset
|
|
212
|
+
}]
|
|
213
|
+
}],
|
|
168
214
|
accessibilityRole: "toolbar",
|
|
169
215
|
accessibilityLabel: accessibilityLabel,
|
|
170
216
|
children: /*#__PURE__*/_jsx(View, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { Pressable, Platform } from 'react-native';
|
|
4
|
+
import { Pressable, Platform, View } from 'react-native';
|
|
5
5
|
import Svg, { Path } from 'react-native-svg';
|
|
6
6
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
7
7
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
@@ -46,6 +46,15 @@ function useFocusVisible() {
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/** Minimum touch target per iOS HIG / Material accessibility guidance. */
|
|
51
|
+
const MIN_TOUCH_TARGET = 44;
|
|
52
|
+
const touchTargetStyle = {
|
|
53
|
+
minWidth: MIN_TOUCH_TARGET,
|
|
54
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
justifyContent: 'center'
|
|
57
|
+
};
|
|
49
58
|
/**
|
|
50
59
|
* Checkbox component that maps directly to the Figma design using design tokens.
|
|
51
60
|
*
|
|
@@ -171,7 +180,7 @@ function Checkbox({
|
|
|
171
180
|
};
|
|
172
181
|
const markColor = disabled && isChecked ? disabledActiveMark : selectedMarkColor;
|
|
173
182
|
return /*#__PURE__*/_jsx(Pressable, {
|
|
174
|
-
style: [
|
|
183
|
+
style: [touchTargetStyle, style],
|
|
175
184
|
onPress: handlePress,
|
|
176
185
|
disabled: disabled,
|
|
177
186
|
onHoverIn: () => setIsHovered(true),
|
|
@@ -183,14 +192,17 @@ function Checkbox({
|
|
|
183
192
|
disabled
|
|
184
193
|
},
|
|
185
194
|
accessibilityLabel: accessibilityLabel,
|
|
186
|
-
children:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
196
|
+
style: resolveStyle(),
|
|
197
|
+
children: isChecked && /*#__PURE__*/_jsx(Svg, {
|
|
198
|
+
width: 12,
|
|
199
|
+
height: 9,
|
|
200
|
+
viewBox: "0 0 12 9",
|
|
201
|
+
fill: "none",
|
|
202
|
+
children: /*#__PURE__*/_jsx(Path, {
|
|
203
|
+
d: "M4.00091 8.66939C3.91321 8.6699 3.82628 8.65309 3.74509 8.61991C3.6639 8.58673 3.59006 8.53785 3.52779 8.47606L0.195972 5.14273C0.0704931 5.01719 -1.86978e-09 4.84693 0 4.66939C1.86978e-09 4.49186 0.0704931 4.3216 0.195972 4.19606C0.321451 4.07053 0.491636 4 0.66909 4C0.846544 4 1.01673 4.07053 1.14221 4.19606L4.00091 7.06273L10.8578 0.196061C10.9833 0.0705253 11.1535 0 11.3309 0C11.5084 0 11.6785 0.0705253 11.804 0.196061C11.9295 0.321597 12 0.49186 12 0.669394C12 0.846929 11.9295 1.01719 11.804 1.14273L4.47403 8.47606C4.41176 8.53785 4.33792 8.58673 4.25673 8.61991C4.17554 8.65309 4.08861 8.6699 4.00091 8.66939Z",
|
|
204
|
+
fill: markColor
|
|
205
|
+
})
|
|
194
206
|
})
|
|
195
207
|
})
|
|
196
208
|
});
|
|
@@ -30,25 +30,36 @@ function useChevronTokens(modes) {
|
|
|
30
30
|
};
|
|
31
31
|
}, [modes]);
|
|
32
32
|
}
|
|
33
|
+
function toNumber(value, fallback) {
|
|
34
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
35
|
+
if (typeof value === 'string') {
|
|
36
|
+
const parsed = parseFloat(value);
|
|
37
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
38
|
+
}
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
33
41
|
function useFormFieldTokens(modes) {
|
|
34
42
|
return useMemo(() => {
|
|
35
|
-
const labelColor = getVariableByName('formField/label/color', modes) || '#
|
|
43
|
+
const labelColor = getVariableByName('formField/label/color', modes) || '#000000';
|
|
36
44
|
const labelFontFamily = getVariableByName('formField/label/fontFamily', modes) || 'JioType Var';
|
|
37
|
-
const labelFontSize =
|
|
38
|
-
const labelLineHeight =
|
|
45
|
+
const labelFontSize = toNumber(getVariableByName('formField/label/fontSize', modes), 14);
|
|
46
|
+
const labelLineHeight = toNumber(getVariableByName('formField/label/lineHeight', modes), 17);
|
|
39
47
|
const labelFontWeight = getVariableByName('formField/label/fontWeight', modes) || '500';
|
|
40
|
-
const gap =
|
|
41
|
-
const inputPaddingH =
|
|
42
|
-
const inputGap =
|
|
43
|
-
const inputRadius =
|
|
48
|
+
const gap = toNumber(getVariableByName('formField/gap', modes), 8);
|
|
49
|
+
const inputPaddingH = toNumber(getVariableByName('formField/input/padding/horizontal', modes), 12);
|
|
50
|
+
const inputGap = toNumber(getVariableByName('formField/input/gap', modes), 8);
|
|
51
|
+
const inputRadius = toNumber(getVariableByName('formField/input/radius', modes), 8);
|
|
44
52
|
const inputBackground = getVariableByName('formField/input/background', modes) || '#ffffff';
|
|
45
|
-
const inputFontSize =
|
|
46
|
-
const inputLineHeight =
|
|
53
|
+
const inputFontSize = toNumber(getVariableByName('formField/input/label/fontSize', modes), 16);
|
|
54
|
+
const inputLineHeight = toNumber(getVariableByName('formField/input/label/lineHeight', modes), 45);
|
|
47
55
|
const inputFontFamily = getVariableByName('formField/input/label/fontFamily', modes) || 'JioType Var';
|
|
48
56
|
const inputFontWeight = getVariableByName('formField/input/label/fontWeight', modes) || '400';
|
|
49
57
|
const inputTextColor = getVariableByName('states/formField/input/label/color', modes) || getVariableByName('formField/input/label/color', modes) || '#24262b';
|
|
50
58
|
const inputBorderColor = getVariableByName('states/formField/input/border/color', modes) || getVariableByName('formField/input/border/color', modes) || '#b5b6b7';
|
|
51
|
-
|
|
59
|
+
// Figma spec: 1.5px. Using parseFloat (via toNumber) preserves the
|
|
60
|
+
// fractional value — parseInt was truncating it to 1, leaving the
|
|
61
|
+
// resolved row height ~1px shorter than the Figma reference.
|
|
62
|
+
const inputBorderSize = toNumber(getVariableByName('formField/input/border/size', modes), 1.5);
|
|
52
63
|
return {
|
|
53
64
|
labelColor,
|
|
54
65
|
labelFontFamily,
|
|
@@ -128,7 +139,7 @@ function DropdownInput({
|
|
|
128
139
|
supportText,
|
|
129
140
|
errorMessage,
|
|
130
141
|
menuMaxHeight = 240,
|
|
131
|
-
menuOffset =
|
|
142
|
+
menuOffset = 6,
|
|
132
143
|
matchTriggerWidth = true,
|
|
133
144
|
closeOnBackdropPress = true,
|
|
134
145
|
modes: propModes = EMPTY_MODES,
|
|
@@ -334,19 +345,23 @@ function DropdownInput({
|
|
|
334
345
|
};
|
|
335
346
|
|
|
336
347
|
// Focus ring uses the resolved input border color from FormField States so
|
|
337
|
-
// active/error look consistent with TextInput-based FormField.
|
|
338
|
-
//
|
|
348
|
+
// active/error look consistent with TextInput-based FormField. Only the
|
|
349
|
+
// color changes between states — width stays constant to avoid layout
|
|
350
|
+
// shift when opening the menu (a shift would invalidate the measured
|
|
351
|
+
// trigger rect and visually shove the popup).
|
|
339
352
|
const inputRowStyle = {
|
|
340
353
|
flexDirection: 'row',
|
|
341
354
|
alignItems: 'center',
|
|
342
355
|
backgroundColor: tokens.inputBackground,
|
|
343
356
|
borderColor: tokens.inputBorderColor,
|
|
344
|
-
borderWidth:
|
|
357
|
+
borderWidth: tokens.inputBorderSize,
|
|
358
|
+
borderStyle: 'solid',
|
|
345
359
|
borderRadius: tokens.inputRadius,
|
|
346
360
|
paddingHorizontal: tokens.inputPaddingH,
|
|
347
361
|
paddingVertical: 0,
|
|
348
362
|
gap: tokens.inputGap,
|
|
349
|
-
minHeight: tokens.inputLineHeight
|
|
363
|
+
minHeight: tokens.inputLineHeight,
|
|
364
|
+
width: '100%'
|
|
350
365
|
};
|
|
351
366
|
const valueTextStyle = {
|
|
352
367
|
flex: 1,
|
|
@@ -494,7 +509,6 @@ function DropdownInput({
|
|
|
494
509
|
transparent: true,
|
|
495
510
|
animationType: "fade",
|
|
496
511
|
onRequestClose: closeMenu,
|
|
497
|
-
statusBarTranslucent: true,
|
|
498
512
|
children: /*#__PURE__*/_jsx(Pressable, {
|
|
499
513
|
style: StyleSheet.absoluteFill,
|
|
500
514
|
onPress: closeOnBackdropPress ? closeMenu : undefined,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { View, Text } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
+
import Checkbox from '../Checkbox/Checkbox';
|
|
8
|
+
import Button from '../Button/Button';
|
|
9
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
/**
|
|
11
|
+
* Default modes applied to the inner toggle `Button`. These resolve the
|
|
12
|
+
* tertiary-style pill in the Figma reference (small, transparent background,
|
|
13
|
+
* brand purple foreground). Any value supplied via the consumer `modes` prop
|
|
14
|
+
* takes precedence over these defaults.
|
|
15
|
+
*/
|
|
16
|
+
const BUTTON_DEFAULT_MODES = {
|
|
17
|
+
'Button / Size': 'XS',
|
|
18
|
+
AppearanceBrand: 'Secondary',
|
|
19
|
+
Emphasis: 'Low'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ExpandableCheckbox composes a `Checkbox`, a long-form label and a
|
|
24
|
+
* "Read more" / "Read less" toggle. Mirrors the Figma "Expandable Checkbox"
|
|
25
|
+
* component with two states:
|
|
26
|
+
*
|
|
27
|
+
* - **Idle (collapsed)** — checkbox + truncated label + toggle button arranged
|
|
28
|
+
* in a horizontal row (cross-axis centered).
|
|
29
|
+
* - **Open (expanded)** — checkbox + full multi-line label, with the toggle
|
|
30
|
+
* button right-aligned beneath the row.
|
|
31
|
+
*
|
|
32
|
+
* The checkbox and the toggle button have independent press handlers — pressing
|
|
33
|
+
* the toggle does not affect the checked state, and toggling the checkbox does
|
|
34
|
+
* not collapse / expand the row.
|
|
35
|
+
*
|
|
36
|
+
* @component
|
|
37
|
+
* @param {ExpandableCheckboxProps} props
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <ExpandableCheckbox
|
|
42
|
+
* label="By checking this box, I (a) acknowledge and (b) agree to the full terms…"
|
|
43
|
+
* defaultChecked
|
|
44
|
+
* onValueChange={setAccepted}
|
|
45
|
+
* modes={{ 'Color Mode': 'Light' }}
|
|
46
|
+
* />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
function ExpandableCheckbox({
|
|
50
|
+
label = '',
|
|
51
|
+
checked: controlledChecked,
|
|
52
|
+
defaultChecked = false,
|
|
53
|
+
onValueChange,
|
|
54
|
+
expanded: controlledExpanded,
|
|
55
|
+
defaultExpanded = false,
|
|
56
|
+
onExpandedChange,
|
|
57
|
+
disabled = false,
|
|
58
|
+
readMoreLabel = 'Read more',
|
|
59
|
+
readLessLabel = 'Read less',
|
|
60
|
+
collapsedLines = 1,
|
|
61
|
+
modes = EMPTY_MODES,
|
|
62
|
+
style,
|
|
63
|
+
labelStyle,
|
|
64
|
+
accessibilityLabel
|
|
65
|
+
}) {
|
|
66
|
+
const isCheckedControlled = controlledChecked !== undefined;
|
|
67
|
+
const [internalChecked, setInternalChecked] = useState(defaultChecked);
|
|
68
|
+
const isChecked = isCheckedControlled ? controlledChecked : internalChecked;
|
|
69
|
+
const isExpandedControlled = controlledExpanded !== undefined;
|
|
70
|
+
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
71
|
+
const isExpanded = isExpandedControlled ? controlledExpanded : internalExpanded;
|
|
72
|
+
const handleToggleChecked = useCallback(next => {
|
|
73
|
+
if (disabled) return;
|
|
74
|
+
if (!isCheckedControlled) setInternalChecked(next);
|
|
75
|
+
onValueChange?.(next);
|
|
76
|
+
}, [disabled, isCheckedControlled, onValueChange]);
|
|
77
|
+
const handleToggleExpanded = useCallback(() => {
|
|
78
|
+
if (disabled) return;
|
|
79
|
+
const next = !isExpanded;
|
|
80
|
+
if (!isExpandedControlled) setInternalExpanded(next);
|
|
81
|
+
onExpandedChange?.(next);
|
|
82
|
+
}, [disabled, isExpanded, isExpandedControlled, onExpandedChange]);
|
|
83
|
+
const gap = getVariableByName('expandableCheckbox/gap', modes) ?? 8;
|
|
84
|
+
const rowGap = getVariableByName('checkboxItem/gap', modes) ?? 8;
|
|
85
|
+
const rowPaddingHorizontal = getVariableByName('checkboxItem/padding/horizontal', modes) ?? 0;
|
|
86
|
+
const rowPaddingVertical = getVariableByName('checkboxItem/padding/vertical', modes) ?? 0;
|
|
87
|
+
const labelColor = getVariableByName('checkboxItem/foreground', modes) ?? '#1a1c1f';
|
|
88
|
+
const labelFontFamily = getVariableByName('checkboxItem/label/fontFamily', modes) ?? 'JioType Var';
|
|
89
|
+
const labelFontSize = getVariableByName('checkboxItem/label/fontSize', modes) ?? 14;
|
|
90
|
+
const labelLineHeight = getVariableByName('checkboxItem/label/lineHeight', modes) ?? 19;
|
|
91
|
+
const labelFontWeightRaw = getVariableByName('checkboxItem/label/fontWeight', modes) ?? 400;
|
|
92
|
+
const labelFontWeight = String(labelFontWeightRaw);
|
|
93
|
+
const containerStyle = useMemo(() => ({
|
|
94
|
+
flexDirection: isExpanded ? 'column' : 'row',
|
|
95
|
+
alignItems: isExpanded ? 'flex-end' : 'center',
|
|
96
|
+
gap,
|
|
97
|
+
width: '100%',
|
|
98
|
+
...(disabled ? {
|
|
99
|
+
opacity: 0.6
|
|
100
|
+
} : null)
|
|
101
|
+
}), [isExpanded, gap, disabled]);
|
|
102
|
+
const rowStyle = useMemo(() => ({
|
|
103
|
+
flex: isExpanded ? undefined : 1,
|
|
104
|
+
alignSelf: isExpanded ? 'stretch' : 'auto',
|
|
105
|
+
minWidth: 0,
|
|
106
|
+
flexDirection: 'row',
|
|
107
|
+
alignItems: 'flex-start',
|
|
108
|
+
gap: rowGap,
|
|
109
|
+
paddingHorizontal: rowPaddingHorizontal,
|
|
110
|
+
paddingVertical: rowPaddingVertical
|
|
111
|
+
}), [isExpanded, rowGap, rowPaddingHorizontal, rowPaddingVertical]);
|
|
112
|
+
const resolvedLabelStyle = useMemo(() => ({
|
|
113
|
+
flex: 1,
|
|
114
|
+
minWidth: 0,
|
|
115
|
+
color: labelColor,
|
|
116
|
+
fontFamily: labelFontFamily,
|
|
117
|
+
fontSize: labelFontSize,
|
|
118
|
+
lineHeight: labelLineHeight,
|
|
119
|
+
fontWeight: labelFontWeight
|
|
120
|
+
}), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight]);
|
|
121
|
+
const buttonModes = useMemo(() => ({
|
|
122
|
+
...BUTTON_DEFAULT_MODES,
|
|
123
|
+
...modes
|
|
124
|
+
}), [modes]);
|
|
125
|
+
const a11yLabel = accessibilityLabel ?? (typeof label === 'string' ? label : undefined);
|
|
126
|
+
const buttonLabel = isExpanded ? readLessLabel : readMoreLabel;
|
|
127
|
+
const labelNumberOfLinesProps = !isExpanded && collapsedLines > 0 ? {
|
|
128
|
+
numberOfLines: collapsedLines,
|
|
129
|
+
ellipsizeMode: 'tail'
|
|
130
|
+
} : null;
|
|
131
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
132
|
+
style: [containerStyle, style],
|
|
133
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
134
|
+
style: rowStyle,
|
|
135
|
+
children: [/*#__PURE__*/_jsx(Checkbox, {
|
|
136
|
+
checked: isChecked,
|
|
137
|
+
disabled: disabled,
|
|
138
|
+
onValueChange: handleToggleChecked,
|
|
139
|
+
modes: modes,
|
|
140
|
+
...(a11yLabel !== undefined ? {
|
|
141
|
+
accessibilityLabel: a11yLabel
|
|
142
|
+
} : {})
|
|
143
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
144
|
+
style: [resolvedLabelStyle, labelStyle],
|
|
145
|
+
selectable: false,
|
|
146
|
+
...(labelNumberOfLinesProps ?? {}),
|
|
147
|
+
children: label
|
|
148
|
+
})]
|
|
149
|
+
}), /*#__PURE__*/_jsx(Button, {
|
|
150
|
+
label: buttonLabel,
|
|
151
|
+
onPress: handleToggleExpanded,
|
|
152
|
+
disabled: disabled,
|
|
153
|
+
modes: buttonModes,
|
|
154
|
+
accessibilityLabel: buttonLabel,
|
|
155
|
+
accessibilityState: {
|
|
156
|
+
expanded: isExpanded
|
|
157
|
+
}
|
|
158
|
+
})]
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export default ExpandableCheckbox;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
-
import { View, Text, TextInput as RNTextInput } from 'react-native';
|
|
3
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { View, Text, Pressable, TextInput as RNTextInput } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
7
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
@@ -202,6 +202,16 @@ function FormField({
|
|
|
202
202
|
const [isFocused, setIsFocused] = useState(false);
|
|
203
203
|
const interactive = !isDisabled && !isReadOnly;
|
|
204
204
|
|
|
205
|
+
// Ref to the native input so tapping anywhere in the input row (padding,
|
|
206
|
+
// leading/trailing gutters) focuses it on the FIRST tap — fixing the Android
|
|
207
|
+
// "two taps to open the keyboard" issue caused by the row intercepting the
|
|
208
|
+
// initial touch.
|
|
209
|
+
const inputRef = useRef(null);
|
|
210
|
+
const focusInput = useCallback(() => {
|
|
211
|
+
if (!interactive) return;
|
|
212
|
+
inputRef.current?.focus();
|
|
213
|
+
}, [interactive]);
|
|
214
|
+
|
|
205
215
|
// FormField States cascade — error > read only/disabled > active (focused) > idle.
|
|
206
216
|
// Disabled maps to "Read Only" since there is no dedicated disabled mode and
|
|
207
217
|
// the visual treatment is closest. This is only the DEFAULT — an explicit
|
|
@@ -334,13 +344,16 @@ function FormField({
|
|
|
334
344
|
style: requiredIndicatorStyle,
|
|
335
345
|
children: " *"
|
|
336
346
|
})]
|
|
337
|
-
}), /*#__PURE__*/_jsxs(
|
|
347
|
+
}), /*#__PURE__*/_jsxs(Pressable, {
|
|
338
348
|
style: [inputRowStyle, inputStyle],
|
|
349
|
+
onPress: focusInput,
|
|
350
|
+
accessible: false,
|
|
339
351
|
children: [processedLeading != null && /*#__PURE__*/_jsx(View, {
|
|
340
352
|
accessibilityElementsHidden: true,
|
|
341
353
|
importantForAccessibility: "no",
|
|
342
354
|
children: processedLeading
|
|
343
355
|
}), /*#__PURE__*/_jsx(RNTextInput, {
|
|
356
|
+
ref: inputRef,
|
|
344
357
|
style: [inputTextStyles, inputTextStyle],
|
|
345
358
|
value: value ?? '',
|
|
346
359
|
onChangeText: handleChangeText,
|