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,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
1
|
+
import React, { useMemo, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -21,9 +21,49 @@ if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental
|
|
|
21
21
|
UIManager.setLayoutAnimationEnabledExperimental(true)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
type AccordionStateMode = 'Idle' | 'Hover' | 'Open' | 'Open Hover' | 'Disabled'
|
|
25
|
+
|
|
26
|
+
function resolveAccordionStateMode(
|
|
27
|
+
disabled: boolean,
|
|
28
|
+
isExpanded: boolean,
|
|
29
|
+
isHovered: boolean,
|
|
30
|
+
contained: boolean,
|
|
31
|
+
): AccordionStateMode {
|
|
32
|
+
if (disabled) return 'Disabled'
|
|
33
|
+
|
|
34
|
+
if (contained) {
|
|
35
|
+
return isExpanded ? 'Open Hover' : 'Hover'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isExpanded) {
|
|
39
|
+
return isHovered ? 'Open Hover' : 'Open'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return isHovered ? 'Hover' : 'Idle'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toFontWeight(value: unknown, fallback: TextStyle['fontWeight']): TextStyle['fontWeight'] {
|
|
46
|
+
if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
const normalized = value.trim().toLowerCase()
|
|
49
|
+
if (normalized === 'bold') return '700'
|
|
50
|
+
if (normalized === 'medium') return '500'
|
|
51
|
+
if (normalized === 'regular' || normalized === 'normal') return '400'
|
|
52
|
+
if (/^\d+$/.test(normalized)) return normalized as TextStyle['fontWeight']
|
|
53
|
+
return value as TextStyle['fontWeight']
|
|
54
|
+
}
|
|
55
|
+
return fallback
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
export type AccordionProps = {
|
|
25
59
|
/** The accordion header title */
|
|
26
60
|
title?: string;
|
|
61
|
+
/**
|
|
62
|
+
* When `true`, the header always uses the filled background treatment
|
|
63
|
+
* (Figma Hover / Open Hover visuals). Defaults to `false` (transparent at
|
|
64
|
+
* rest, filled only while hovered or pressed).
|
|
65
|
+
*/
|
|
66
|
+
contained?: boolean;
|
|
27
67
|
/** Initial expanded state. Defaults to false (collapsed) */
|
|
28
68
|
defaultExpanded?: boolean;
|
|
29
69
|
/** Controlled expanded state. When provided, the component becomes controlled */
|
|
@@ -51,31 +91,20 @@ export type AccordionProps = {
|
|
|
51
91
|
/**
|
|
52
92
|
* Accordion component that mirrors the Figma "Accordion" component.
|
|
53
93
|
*
|
|
54
|
-
*
|
|
55
|
-
* -
|
|
56
|
-
*
|
|
57
|
-
* -
|
|
58
|
-
* - **Design-token driven styling** via `getVariableByName` and `modes`
|
|
94
|
+
* Supports two visual treatments via the `contained` prop:
|
|
95
|
+
* - **`contained={false}`** (default) — transparent header at rest; filled
|
|
96
|
+
* background on hover / press.
|
|
97
|
+
* - **`contained={true}`** — header always uses the filled background.
|
|
59
98
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
99
|
+
* Interaction states (Idle, Hover, Open, Disabled) are resolved automatically
|
|
100
|
+
* from `expanded`, `disabled`, hover, and `contained` — consumers should not
|
|
101
|
+
* pass `'Accordion States'` in `modes`.
|
|
63
102
|
*
|
|
64
103
|
* @component
|
|
65
|
-
* @param {Object} props
|
|
66
|
-
* @param {string} [props.title='Accordion title'] - The accordion header title
|
|
67
|
-
* @param {boolean} [props.defaultExpanded=false] - Initial expanded state
|
|
68
|
-
* @param {boolean} [props.expanded] - Controlled expanded state
|
|
69
|
-
* @param {Function} [props.onExpandedChange] - Callback fired when expanded state changes
|
|
70
|
-
* @param {boolean} [props.disabled=false] - Whether the accordion is disabled
|
|
71
|
-
* @param {React.ReactNode} [props.children] - Content to display when expanded
|
|
72
|
-
* @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens
|
|
73
|
-
* @param {Object} [props.style] - Optional container style overrides
|
|
74
|
-
* @param {string} [props.accessibilityLabel] - Accessibility label for the accordion. If not provided, uses title
|
|
75
|
-
* @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
|
|
76
104
|
*/
|
|
77
105
|
function Accordion({
|
|
78
106
|
title = 'Accordion title',
|
|
107
|
+
contained = false,
|
|
79
108
|
defaultExpanded = false,
|
|
80
109
|
expanded: controlledExpanded,
|
|
81
110
|
onExpandedChange,
|
|
@@ -89,23 +118,31 @@ function Accordion({
|
|
|
89
118
|
webAccessibilityProps,
|
|
90
119
|
...rest
|
|
91
120
|
}: AccordionProps) {
|
|
92
|
-
// Internal state for uncontrolled mode
|
|
93
121
|
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded)
|
|
94
|
-
|
|
95
|
-
|
|
122
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
123
|
+
|
|
96
124
|
const isControlled = controlledExpanded !== undefined
|
|
97
125
|
const isExpanded = isControlled ? controlledExpanded : internalExpanded
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
|
|
127
|
+
const resolvedModes = useMemo(() => {
|
|
128
|
+
const accordionState = resolveAccordionStateMode(
|
|
129
|
+
disabled,
|
|
130
|
+
isExpanded,
|
|
131
|
+
isHovered,
|
|
132
|
+
contained,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...modes,
|
|
137
|
+
'Accordion States': accordionState,
|
|
138
|
+
}
|
|
139
|
+
}, [contained, disabled, isExpanded, isHovered, modes])
|
|
140
|
+
|
|
103
141
|
const handleToggle = () => {
|
|
104
142
|
if (disabled) return
|
|
105
|
-
|
|
106
|
-
// Animate the layout change
|
|
143
|
+
|
|
107
144
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
|
108
|
-
|
|
145
|
+
|
|
109
146
|
if (isControlled) {
|
|
110
147
|
onExpandedChange?.(!isExpanded)
|
|
111
148
|
} else {
|
|
@@ -113,38 +150,45 @@ function Accordion({
|
|
|
113
150
|
onExpandedChange?.(!isExpanded)
|
|
114
151
|
}
|
|
115
152
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const titleFontFamily =
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
153
|
+
|
|
154
|
+
const titleColor =
|
|
155
|
+
(getVariableByName('accordion/title/color', resolvedModes) as string | null) ?? '#0d0d0d'
|
|
156
|
+
const titleFontSize =
|
|
157
|
+
(getVariableByName('accordion/title/fontSize', resolvedModes) as number | null) ?? 14
|
|
158
|
+
const titleLineHeight =
|
|
159
|
+
(getVariableByName('accordion/title/lineHeight', resolvedModes) as number | null) ?? 20
|
|
160
|
+
const titleFontFamily =
|
|
161
|
+
(getVariableByName('accordion/title/fontFamily', resolvedModes) as string | null) ?? 'System'
|
|
162
|
+
const titleFontWeight = toFontWeight(
|
|
163
|
+
getVariableByName('accordion/title/fontWeight', resolvedModes),
|
|
164
|
+
'700',
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
const iconColor =
|
|
168
|
+
(getVariableByName('accordion/icon/color', resolvedModes) as string | null) ?? '#141414'
|
|
169
|
+
const iconSize = (getVariableByName('accordion/icon/size', resolvedModes) as number | null) ?? 24
|
|
170
|
+
|
|
171
|
+
const headerGap = (getVariableByName('accordion/header/gap', resolvedModes) as number | null) ?? 12
|
|
172
|
+
const headerPaddingVertical =
|
|
173
|
+
(getVariableByName('accordion/header/padding/vertical', resolvedModes) as number | null) ?? 8
|
|
174
|
+
const headerBackground =
|
|
175
|
+
(getVariableByName('accordion/header/background', resolvedModes) as string | null) ??
|
|
176
|
+
'transparent'
|
|
177
|
+
|
|
178
|
+
const contentGap = (getVariableByName('accordion/content/gap', resolvedModes) as number | null) ?? 12
|
|
179
|
+
const contentPaddingTop =
|
|
180
|
+
(getVariableByName('accordion/content/padding/top', resolvedModes) as number | null) ?? 8
|
|
181
|
+
const contentPaddingBottom =
|
|
182
|
+
(getVariableByName('accordion/content/padding/bottom', resolvedModes) as number | null) ?? 8
|
|
183
|
+
|
|
184
|
+
const borderColor =
|
|
185
|
+
(getVariableByName('accordion/border/color', resolvedModes) as string | null) ?? '#e6e6e6'
|
|
186
|
+
|
|
143
187
|
const containerStyle: ViewStyle = {
|
|
144
188
|
borderBottomWidth: 1,
|
|
145
189
|
borderBottomColor: borderColor,
|
|
146
190
|
}
|
|
147
|
-
|
|
191
|
+
|
|
148
192
|
const headerStyle: ViewStyle = {
|
|
149
193
|
flexDirection: 'row',
|
|
150
194
|
alignItems: 'center',
|
|
@@ -154,16 +198,16 @@ function Accordion({
|
|
|
154
198
|
backgroundColor: headerBackground,
|
|
155
199
|
overflow: 'hidden',
|
|
156
200
|
}
|
|
157
|
-
|
|
201
|
+
|
|
158
202
|
const titleStyle: TextStyle = {
|
|
159
203
|
flex: 1,
|
|
160
204
|
color: titleColor,
|
|
161
205
|
fontSize: titleFontSize,
|
|
162
206
|
lineHeight: titleLineHeight,
|
|
163
207
|
fontFamily: titleFontFamily,
|
|
164
|
-
fontWeight:
|
|
208
|
+
fontWeight: titleFontWeight,
|
|
165
209
|
}
|
|
166
|
-
|
|
210
|
+
|
|
167
211
|
const contentStyle: ViewStyle = {
|
|
168
212
|
backgroundColor: 'transparent',
|
|
169
213
|
gap: contentGap,
|
|
@@ -172,11 +216,9 @@ function Accordion({
|
|
|
172
216
|
paddingHorizontal: 0,
|
|
173
217
|
overflow: 'hidden',
|
|
174
218
|
}
|
|
175
|
-
|
|
176
|
-
// Generate default accessibility label
|
|
219
|
+
|
|
177
220
|
const defaultAccessibilityLabel = accessibilityLabel || title
|
|
178
|
-
|
|
179
|
-
// Web platform support
|
|
221
|
+
|
|
180
222
|
const webProps = usePressableWebSupport({
|
|
181
223
|
restProps: {},
|
|
182
224
|
onPress: handleToggle,
|
|
@@ -184,12 +226,11 @@ function Accordion({
|
|
|
184
226
|
accessibilityLabel: defaultAccessibilityLabel,
|
|
185
227
|
webAccessibilityProps,
|
|
186
228
|
})
|
|
187
|
-
|
|
188
|
-
// Process children to pass modes
|
|
229
|
+
|
|
189
230
|
const processedChildren = children
|
|
190
|
-
? cloneChildrenWithModes(React.Children.toArray(children),
|
|
231
|
+
? cloneChildrenWithModes(React.Children.toArray(children), resolvedModes)
|
|
191
232
|
: null
|
|
192
|
-
|
|
233
|
+
|
|
193
234
|
return (
|
|
194
235
|
<View style={[containerStyle, style]} {...rest}>
|
|
195
236
|
<Pressable
|
|
@@ -217,12 +258,12 @@ function Accordion({
|
|
|
217
258
|
<Icon
|
|
218
259
|
name={isExpanded ? 'ic_minus' : 'ic_add'}
|
|
219
260
|
size={iconSize}
|
|
220
|
-
color={
|
|
261
|
+
color={iconColor}
|
|
221
262
|
accessibilityElementsHidden={true}
|
|
222
263
|
importantForAccessibility="no"
|
|
223
264
|
/>
|
|
224
265
|
</Pressable>
|
|
225
|
-
|
|
266
|
+
|
|
226
267
|
{isExpanded && processedChildren && (
|
|
227
268
|
<View style={contentStyle}>
|
|
228
269
|
{processedChildren}
|
|
@@ -233,4 +274,3 @@ function Accordion({
|
|
|
233
274
|
}
|
|
234
275
|
|
|
235
276
|
export default Accordion
|
|
236
|
-
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import React, { useMemo } from 'react'
|
|
1
|
+
import React, { useEffect, useMemo, useRef } from 'react'
|
|
2
2
|
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Keyboard,
|
|
3
5
|
View,
|
|
4
6
|
Platform,
|
|
7
|
+
type KeyboardEvent,
|
|
5
8
|
type ViewStyle,
|
|
6
9
|
type StyleProp,
|
|
7
10
|
} from 'react-native'
|
|
@@ -133,6 +136,47 @@ function ActionFooter({
|
|
|
133
136
|
style,
|
|
134
137
|
accessibilityLabel,
|
|
135
138
|
}: ActionFooterProps) {
|
|
139
|
+
// -------------------------------------------------------------------------
|
|
140
|
+
// Keep the footer locked in place behind the software keyboard (Android).
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
//
|
|
143
|
+
// The Android activity is configured with `windowSoftInputMode="adjustResize"`,
|
|
144
|
+
// which shrinks the app window by the keyboard height when the keyboard
|
|
145
|
+
// opens. A bottom-anchored footer therefore gets lifted UP by the keyboard
|
|
146
|
+
// height — exactly the jump the design does not want.
|
|
147
|
+
//
|
|
148
|
+
// To counteract that, we translate the footer back DOWN by the same keyboard
|
|
149
|
+
// height so it visually stays exactly where it was (now sitting behind the
|
|
150
|
+
// keyboard). iOS does not resize the window for the keyboard, so the footer
|
|
151
|
+
// already stays put there; we only run this on Android to avoid pushing the
|
|
152
|
+
// footer off-screen on platforms that don't lift it in the first place.
|
|
153
|
+
const keyboardOffset = useRef(new Animated.Value(0)).current
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (Platform.OS !== 'android') return undefined
|
|
156
|
+
|
|
157
|
+
const animateTo = (toValue: number, duration?: number) => {
|
|
158
|
+
Animated.timing(keyboardOffset, {
|
|
159
|
+
toValue,
|
|
160
|
+
// Match the OS keyboard animation so the resize and our counter-shift
|
|
161
|
+
// cancel out smoothly with no visible footer movement.
|
|
162
|
+
duration: typeof duration === 'number' && duration > 0 ? duration : 150,
|
|
163
|
+
useNativeDriver: true,
|
|
164
|
+
}).start()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const showSub = Keyboard.addListener('keyboardDidShow', (e: KeyboardEvent) => {
|
|
168
|
+
animateTo(e?.endCoordinates?.height ?? 0, e?.duration)
|
|
169
|
+
})
|
|
170
|
+
const hideSub = Keyboard.addListener('keyboardDidHide', (e: KeyboardEvent) => {
|
|
171
|
+
animateTo(0, e?.duration)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
showSub.remove()
|
|
176
|
+
hideSub.remove()
|
|
177
|
+
}
|
|
178
|
+
}, [keyboardOffset])
|
|
179
|
+
|
|
136
180
|
// All token reads collapsed into a single useMemo keyed on `modes`. With
|
|
137
181
|
// the shared `EMPTY_MODES` default this resolves once for the common path
|
|
138
182
|
// and never re-allocates the container/slot style objects between renders.
|
|
@@ -192,13 +236,21 @@ function ActionFooter({
|
|
|
192
236
|
}, [children, modes])
|
|
193
237
|
|
|
194
238
|
return (
|
|
195
|
-
<View
|
|
196
|
-
style={[
|
|
239
|
+
<Animated.View
|
|
240
|
+
style={[
|
|
241
|
+
containerStyle,
|
|
242
|
+
WEB_SHADOW,
|
|
243
|
+
style,
|
|
244
|
+
// Counter-translate by the keyboard height on Android so `adjustResize`
|
|
245
|
+
// can't lift the footer above the keyboard (no-op on iOS/web where the
|
|
246
|
+
// value stays at 0).
|
|
247
|
+
{ transform: [{ translateY: keyboardOffset }] },
|
|
248
|
+
]}
|
|
197
249
|
accessibilityRole="toolbar"
|
|
198
250
|
accessibilityLabel={accessibilityLabel}
|
|
199
251
|
>
|
|
200
252
|
<View style={slotStyle}>{enhancedChildren}</View>
|
|
201
|
-
</View>
|
|
253
|
+
</Animated.View>
|
|
202
254
|
)
|
|
203
255
|
}
|
|
204
256
|
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
Pressable,
|
|
4
4
|
Platform,
|
|
5
|
+
View,
|
|
5
6
|
type StyleProp,
|
|
6
7
|
type ViewStyle,
|
|
7
8
|
} from 'react-native'
|
|
@@ -50,6 +51,16 @@ function useFocusVisible() {
|
|
|
50
51
|
return { isFocusVisible, focusHandlers: { onFocus, onBlur } }
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/** Minimum touch target per iOS HIG / Material accessibility guidance. */
|
|
55
|
+
const MIN_TOUCH_TARGET = 44
|
|
56
|
+
|
|
57
|
+
const touchTargetStyle: ViewStyle = {
|
|
58
|
+
minWidth: MIN_TOUCH_TARGET,
|
|
59
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
justifyContent: 'center',
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
export interface CheckboxProps {
|
|
54
65
|
/** Whether the checkbox is checked (controlled) */
|
|
55
66
|
checked?: boolean
|
|
@@ -207,7 +218,7 @@ function Checkbox({
|
|
|
207
218
|
|
|
208
219
|
return (
|
|
209
220
|
<Pressable
|
|
210
|
-
style={[
|
|
221
|
+
style={[touchTargetStyle, style]}
|
|
211
222
|
onPress={handlePress}
|
|
212
223
|
disabled={disabled}
|
|
213
224
|
onHoverIn={() => setIsHovered(true)}
|
|
@@ -217,14 +228,16 @@ function Checkbox({
|
|
|
217
228
|
accessibilityState={{ checked: isChecked, disabled }}
|
|
218
229
|
accessibilityLabel={accessibilityLabel}
|
|
219
230
|
>
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
<View style={resolveStyle()}>
|
|
232
|
+
{isChecked && (
|
|
233
|
+
<Svg width={12} height={9} viewBox="0 0 12 9" fill="none">
|
|
234
|
+
<Path
|
|
235
|
+
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"
|
|
236
|
+
fill={markColor}
|
|
237
|
+
/>
|
|
238
|
+
</Svg>
|
|
239
|
+
)}
|
|
240
|
+
</View>
|
|
228
241
|
</Pressable>
|
|
229
242
|
)
|
|
230
243
|
}
|
|
@@ -162,51 +162,60 @@ function useChevronTokens(modes: Record<string, any>) {
|
|
|
162
162
|
}, [modes])
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function toNumber(value: unknown, fallback: number): number {
|
|
166
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
167
|
+
if (typeof value === 'string') {
|
|
168
|
+
const parsed = parseFloat(value)
|
|
169
|
+
if (Number.isFinite(parsed)) return parsed
|
|
170
|
+
}
|
|
171
|
+
return fallback
|
|
172
|
+
}
|
|
173
|
+
|
|
165
174
|
function useFormFieldTokens(modes: Record<string, any>) {
|
|
166
175
|
return useMemo(() => {
|
|
167
176
|
const labelColor =
|
|
168
177
|
(getVariableByName('formField/label/color', modes) as string) ||
|
|
169
|
-
'#
|
|
178
|
+
'#000000'
|
|
170
179
|
const labelFontFamily =
|
|
171
180
|
(getVariableByName('formField/label/fontFamily', modes) as string) ||
|
|
172
181
|
'JioType Var'
|
|
173
|
-
const labelFontSize =
|
|
174
|
-
|
|
182
|
+
const labelFontSize = toNumber(
|
|
183
|
+
getVariableByName('formField/label/fontSize', modes),
|
|
175
184
|
14
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
)
|
|
186
|
+
const labelLineHeight = toNumber(
|
|
187
|
+
getVariableByName('formField/label/lineHeight', modes),
|
|
188
|
+
17
|
|
189
|
+
)
|
|
181
190
|
const labelFontWeight =
|
|
182
191
|
(getVariableByName('formField/label/fontWeight', modes) as string) ||
|
|
183
192
|
'500'
|
|
184
193
|
|
|
185
|
-
const gap =
|
|
186
|
-
|
|
187
|
-
const inputPaddingH =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
parseInt(getVariableByName('formField/input/gap', modes), 10) || 8
|
|
194
|
-
const inputRadius =
|
|
195
|
-
parseInt(getVariableByName('formField/input/radius', modes), 10) ||
|
|
194
|
+
const gap = toNumber(getVariableByName('formField/gap', modes), 8)
|
|
195
|
+
|
|
196
|
+
const inputPaddingH = toNumber(
|
|
197
|
+
getVariableByName('formField/input/padding/horizontal', modes),
|
|
198
|
+
12
|
|
199
|
+
)
|
|
200
|
+
const inputGap = toNumber(
|
|
201
|
+
getVariableByName('formField/input/gap', modes),
|
|
196
202
|
8
|
|
203
|
+
)
|
|
204
|
+
const inputRadius = toNumber(
|
|
205
|
+
getVariableByName('formField/input/radius', modes),
|
|
206
|
+
8
|
|
207
|
+
)
|
|
197
208
|
const inputBackground =
|
|
198
209
|
(getVariableByName('formField/input/background', modes) as string) ||
|
|
199
210
|
'#ffffff'
|
|
200
|
-
const inputFontSize =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
10
|
|
209
|
-
) || 45
|
|
211
|
+
const inputFontSize = toNumber(
|
|
212
|
+
getVariableByName('formField/input/label/fontSize', modes),
|
|
213
|
+
16
|
|
214
|
+
)
|
|
215
|
+
const inputLineHeight = toNumber(
|
|
216
|
+
getVariableByName('formField/input/label/lineHeight', modes),
|
|
217
|
+
45
|
|
218
|
+
)
|
|
210
219
|
const inputFontFamily =
|
|
211
220
|
(getVariableByName(
|
|
212
221
|
'formField/input/label/fontFamily',
|
|
@@ -231,11 +240,13 @@ function useFormFieldTokens(modes: Record<string, any>) {
|
|
|
231
240
|
) as string) ||
|
|
232
241
|
(getVariableByName('formField/input/border/color', modes) as string) ||
|
|
233
242
|
'#b5b6b7'
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
243
|
+
// Figma spec: 1.5px. Using parseFloat (via toNumber) preserves the
|
|
244
|
+
// fractional value — parseInt was truncating it to 1, leaving the
|
|
245
|
+
// resolved row height ~1px shorter than the Figma reference.
|
|
246
|
+
const inputBorderSize = toNumber(
|
|
247
|
+
getVariableByName('formField/input/border/size', modes),
|
|
248
|
+
1.5
|
|
249
|
+
)
|
|
239
250
|
|
|
240
251
|
return {
|
|
241
252
|
labelColor,
|
|
@@ -314,7 +325,7 @@ function DropdownInput({
|
|
|
314
325
|
supportText,
|
|
315
326
|
errorMessage,
|
|
316
327
|
menuMaxHeight = 240,
|
|
317
|
-
menuOffset =
|
|
328
|
+
menuOffset = 6,
|
|
318
329
|
matchTriggerWidth = true,
|
|
319
330
|
closeOnBackdropPress = true,
|
|
320
331
|
modes: propModes = EMPTY_MODES,
|
|
@@ -594,19 +605,23 @@ function DropdownInput({
|
|
|
594
605
|
}
|
|
595
606
|
|
|
596
607
|
// Focus ring uses the resolved input border color from FormField States so
|
|
597
|
-
// active/error look consistent with TextInput-based FormField.
|
|
598
|
-
//
|
|
608
|
+
// active/error look consistent with TextInput-based FormField. Only the
|
|
609
|
+
// color changes between states — width stays constant to avoid layout
|
|
610
|
+
// shift when opening the menu (a shift would invalidate the measured
|
|
611
|
+
// trigger rect and visually shove the popup).
|
|
599
612
|
const inputRowStyle: ViewStyle = {
|
|
600
613
|
flexDirection: 'row',
|
|
601
614
|
alignItems: 'center',
|
|
602
615
|
backgroundColor: tokens.inputBackground,
|
|
603
616
|
borderColor: tokens.inputBorderColor,
|
|
604
|
-
borderWidth:
|
|
617
|
+
borderWidth: tokens.inputBorderSize,
|
|
618
|
+
borderStyle: 'solid',
|
|
605
619
|
borderRadius: tokens.inputRadius,
|
|
606
620
|
paddingHorizontal: tokens.inputPaddingH,
|
|
607
621
|
paddingVertical: 0,
|
|
608
622
|
gap: tokens.inputGap,
|
|
609
623
|
minHeight: tokens.inputLineHeight,
|
|
624
|
+
width: '100%',
|
|
610
625
|
}
|
|
611
626
|
|
|
612
627
|
const valueTextStyle: TextStyle = {
|
|
@@ -763,12 +778,25 @@ function DropdownInput({
|
|
|
763
778
|
/>
|
|
764
779
|
)}
|
|
765
780
|
|
|
781
|
+
{/*
|
|
782
|
+
IMPORTANT: do NOT pass `statusBarTranslucent` to this Modal.
|
|
783
|
+
On Android, a `statusBarTranslucent` Modal opens its own window
|
|
784
|
+
that spans the entire screen (origin at screen-top, including
|
|
785
|
+
the status bar), but `measureInWindow` on the trigger returns
|
|
786
|
+
coordinates relative to the *activity* window — which on a
|
|
787
|
+
default Android setup starts BELOW the status bar. The two
|
|
788
|
+
coordinate spaces then differ by `StatusBar.currentHeight`, so
|
|
789
|
+
`triggerRect.y + triggerRect.height + menuOffset` lands roughly
|
|
790
|
+
one status-bar-height ABOVE the visible input, making the
|
|
791
|
+
popup overlap the input row. Leaving `statusBarTranslucent`
|
|
792
|
+
off keeps the Modal's window aligned with the activity
|
|
793
|
+
window, which is what every measurement here assumes.
|
|
794
|
+
*/}
|
|
766
795
|
<Modal
|
|
767
796
|
visible={isOpen}
|
|
768
797
|
transparent
|
|
769
798
|
animationType="fade"
|
|
770
799
|
onRequestClose={closeMenu}
|
|
771
|
-
statusBarTranslucent
|
|
772
800
|
>
|
|
773
801
|
<Pressable
|
|
774
802
|
style={StyleSheet.absoluteFill}
|