jfs-components 0.0.74 → 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 +109 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +193 -82
- package/lib/commonjs/components/Avatar/Avatar.js +20 -0
- package/lib/commonjs/components/Badge/Badge.js +23 -0
- package/lib/commonjs/components/Button/Button.js +37 -0
- 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/IconButton/IconButton.js +20 -0
- package/lib/commonjs/components/Image/Image.js +26 -1
- package/lib/commonjs/components/ListItem/ListItem.js +25 -10
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
- package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
- package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/PageHero/PageHero.js +41 -5
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -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/Text/Text.js +31 -1
- package/lib/commonjs/components/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/Icon.js +16 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/skeleton/Skeleton.js +234 -0
- package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
- package/lib/commonjs/skeleton/index.js +58 -0
- package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
- package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +193 -83
- package/lib/module/components/Avatar/Avatar.js +19 -0
- package/lib/module/components/Badge/Badge.js +23 -0
- package/lib/module/components/Button/Button.js +37 -0
- 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/IconButton/IconButton.js +20 -0
- package/lib/module/components/Image/Image.js +25 -1
- package/lib/module/components/ListItem/ListItem.js +25 -10
- package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
- package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
- package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/PageHero/PageHero.js +41 -5
- package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
- 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/Text/Text.js +31 -1
- package/lib/module/components/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +5 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/Icon.js +16 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/skeleton/Skeleton.js +229 -0
- package/lib/module/skeleton/SkeletonGroup.js +133 -0
- package/lib/module/skeleton/index.js +6 -0
- package/lib/module/skeleton/shimmer-tokens.js +181 -0
- package/lib/module/skeleton/useReducedMotion.js +61 -0
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
- package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
- package/lib/typescript/src/components/Button/Button.d.ts +8 -1
- 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/IconButton/IconButton.d.ts +7 -1
- package/lib/typescript/src/components/Image/Image.d.ts +8 -1
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
- package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
- package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -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/PageHero/PageHero.d.ts +31 -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/Text/Text.d.ts +20 -1
- package/lib/typescript/src/components/index.d.ts +8 -3
- package/lib/typescript/src/icons/Icon.d.ts +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
- package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
- package/lib/typescript/src/skeleton/index.d.ts +5 -0
- package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
- package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
- package/package.json +11 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +210 -92
- package/src/components/Avatar/Avatar.tsx +26 -0
- package/src/components/Badge/Badge.tsx +27 -0
- package/src/components/Button/Button.tsx +40 -0
- 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/IconButton/IconButton.tsx +27 -0
- package/src/components/Image/Image.tsx +25 -0
- package/src/components/ListItem/ListItem.tsx +21 -10
- package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
- package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
- package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
- package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/PageHero/PageHero.tsx +61 -4
- package/src/components/RechargeCard/RechargeCard.tsx +32 -24
- 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/Text/Text.tsx +54 -0
- package/src/components/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +8 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/Icon.tsx +17 -0
- package/src/icons/registry.ts +1 -1
- package/src/index.ts +1 -0
- package/src/skeleton/Skeleton.tsx +298 -0
- package/src/skeleton/SkeletonGroup.tsx +193 -0
- package/src/skeleton/index.ts +10 -0
- package/src/skeleton/shimmer-tokens.ts +221 -0
- package/src/skeleton/useReducedMotion.ts +72 -0
|
@@ -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,9 +1,12 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useEffect, useMemo, useRef } from 'react'
|
|
2
2
|
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Keyboard,
|
|
3
5
|
View,
|
|
6
|
+
Platform,
|
|
7
|
+
type KeyboardEvent,
|
|
4
8
|
type ViewStyle,
|
|
5
9
|
type StyleProp,
|
|
6
|
-
Platform,
|
|
7
10
|
} from 'react-native'
|
|
8
11
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
9
12
|
import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils'
|
|
@@ -12,48 +15,118 @@ import IconButton from '../IconButton/IconButton'
|
|
|
12
15
|
export type ActionFooterProps = {
|
|
13
16
|
/**
|
|
14
17
|
* Content to render inside the action footer slot.
|
|
15
|
-
* Typically includes IconButton and Button components.
|
|
18
|
+
* Typically includes `IconButton` and `Button` components.
|
|
19
|
+
* `IconButton` children keep their intrinsic square size; everything else
|
|
20
|
+
* is auto-stretched to share the remaining horizontal space equally.
|
|
16
21
|
*/
|
|
17
22
|
children?: React.ReactNode
|
|
18
23
|
/**
|
|
19
24
|
* Mode configuration passed to the token resolver.
|
|
20
|
-
*
|
|
25
|
+
* Automatically merged into every slot child via {@link cloneChildrenWithModes}
|
|
26
|
+
* so callers don't have to thread modes down by hand.
|
|
21
27
|
*/
|
|
22
28
|
modes?: Record<string, any>
|
|
23
29
|
/**
|
|
24
|
-
* Optional style overrides for the container
|
|
30
|
+
* Optional style overrides for the outer container.
|
|
25
31
|
*/
|
|
26
32
|
style?: StyleProp<ViewStyle>
|
|
27
33
|
/**
|
|
28
|
-
* Accessibility label for the footer region
|
|
34
|
+
* Accessibility label for the footer region (announced for the toolbar).
|
|
29
35
|
*/
|
|
30
36
|
accessibilityLabel?: string
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
const IS_WEB = Platform.OS === 'web'
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Yoga-safe stretch
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
//
|
|
45
|
+
// React Native (Yoga) interprets the `flex: 1` shorthand as
|
|
46
|
+
// { flexGrow: 1, flexShrink: 1, flexBasis: 0 }
|
|
47
|
+
// which is the *equal-share* variant. That is the correct math for what we
|
|
48
|
+
// want here (equal-width action buttons), BUT Yoga has a well-known foot-gun
|
|
49
|
+
// when this child sits inside a parent whose main-axis size hasn't been
|
|
50
|
+
// resolved yet on the first layout pass: the child collapses to 0 and the
|
|
51
|
+
// inner text gets clipped to "" before the parent ever measures.
|
|
52
|
+
//
|
|
53
|
+
// The defensive incantation used elsewhere in this codebase (see
|
|
54
|
+
// `CardCTA.leftWrap` and the `MediaCard.Header` fix in CHANGELOG.md) is to
|
|
55
|
+
// keep the equal-share math but explicitly clamp `minWidth` to 0 so Yoga
|
|
56
|
+
// always allows the child to participate in the shrink algorithm, even when
|
|
57
|
+
// the parent itself is in an undetermined state. Combined with explicit
|
|
58
|
+
// `flexGrow`/`flexShrink`/`flexBasis` (NOT the `flex` shorthand) this
|
|
59
|
+
// renders correctly on iOS, Android, and Web — and crucially never produces
|
|
60
|
+
// the "buttons render as empty pills" failure mode the previous version had
|
|
61
|
+
// on iOS dev clients.
|
|
62
|
+
const STRETCH_STYLE: ViewStyle = {
|
|
63
|
+
flexGrow: 1,
|
|
64
|
+
flexShrink: 1,
|
|
65
|
+
flexBasis: 0,
|
|
66
|
+
minWidth: 0,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Platform-specific drop shadow. Web boxShadow can't go through
|
|
70
|
+
// Platform.select (RN's typed surface doesn't include it) so we keep it as a
|
|
71
|
+
// separate constant and append it below.
|
|
72
|
+
const NATIVE_SHADOW = Platform.select<ViewStyle>({
|
|
73
|
+
ios: {
|
|
74
|
+
shadowColor: '#0c0d10',
|
|
75
|
+
shadowOffset: { width: 0, height: -12 },
|
|
76
|
+
shadowOpacity: 0.16,
|
|
77
|
+
shadowRadius: 24,
|
|
78
|
+
},
|
|
79
|
+
android: {
|
|
80
|
+
elevation: 16,
|
|
81
|
+
},
|
|
82
|
+
default: {},
|
|
83
|
+
}) as ViewStyle
|
|
84
|
+
|
|
85
|
+
const WEB_SHADOW = IS_WEB
|
|
86
|
+
? ({
|
|
87
|
+
boxShadow:
|
|
88
|
+
'0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)',
|
|
89
|
+
} as ViewStyle)
|
|
90
|
+
: null
|
|
91
|
+
|
|
92
|
+
// The runtime token a slot child must equal (by reference) to be treated as
|
|
93
|
+
// an IconButton. `IconButton` is exported wrapped in `React.memo`, so the
|
|
94
|
+
// element.type identity comparison works for both `<IconButton />` from the
|
|
95
|
+
// same module and any `React.memo`-wrapped re-export. The fallback check
|
|
96
|
+
// (`type.type === IconButton`) catches one extra layer of `forwardRef` /
|
|
97
|
+
// `memo` wrapping which can happen when consumers re-export the component.
|
|
98
|
+
function isIconButtonElement(element: React.ReactElement<any>): boolean {
|
|
99
|
+
const t: any = element.type
|
|
100
|
+
if (t === IconButton) return true
|
|
101
|
+
if (t && typeof t === 'object' && t.type === IconButton) return true
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
33
105
|
/**
|
|
34
|
-
* ActionFooter
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
106
|
+
* ActionFooter — a sticky bottom container for primary screen actions.
|
|
107
|
+
*
|
|
108
|
+
* Layout contract:
|
|
109
|
+
* - The outer container stretches horizontally (`alignSelf: 'stretch'`) so
|
|
110
|
+
* it fills the parent regardless of whether the parent is a flex column,
|
|
111
|
+
* a ScrollView contentContainer, or a plain View.
|
|
112
|
+
* - The inner slot is a single row sized by its tallest child. It does NOT
|
|
113
|
+
* use `flex: 1` — that previously caused the row to collapse to zero on
|
|
114
|
+
* the first Yoga pass on native, taking the button labels with it.
|
|
115
|
+
* - `IconButton` children keep their intrinsic square size.
|
|
116
|
+
* - Every other child is auto-stretched with the Yoga-safe stretch style
|
|
117
|
+
* above so two `<Button>` siblings render at equal width on iOS, Android,
|
|
118
|
+
* and Web.
|
|
119
|
+
*
|
|
120
|
+
* The `modes` prop is automatically pushed down to every slot child via
|
|
121
|
+
* {@link cloneChildrenWithModes}; explicit child-level modes win over the
|
|
122
|
+
* parent's modes.
|
|
123
|
+
*
|
|
49
124
|
* @example
|
|
50
125
|
* ```tsx
|
|
51
|
-
* // Basic usage - modes are automatically passed to all children.
|
|
52
|
-
* // Non-IconButton children (e.g., Button) are auto-stretched to fill.
|
|
53
126
|
* <ActionFooter modes={modes}>
|
|
54
127
|
* <IconButton iconName="ic_split" />
|
|
55
|
-
* <Button label="Request" />
|
|
56
|
-
* <Button label="Pay" />
|
|
128
|
+
* <Button label="Request" modes={{ AppearanceBrand: 'Secondary' }} />
|
|
129
|
+
* <Button label="Pay" modes={{ AppearanceBrand: 'Primary' }} />
|
|
57
130
|
* </ActionFooter>
|
|
58
131
|
* ```
|
|
59
132
|
*/
|
|
@@ -61,79 +134,124 @@ function ActionFooter({
|
|
|
61
134
|
children,
|
|
62
135
|
modes = EMPTY_MODES,
|
|
63
136
|
style,
|
|
64
|
-
accessibilityLabel
|
|
137
|
+
accessibilityLabel,
|
|
65
138
|
}: ActionFooterProps) {
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
|
|
180
|
+
// All token reads collapsed into a single useMemo keyed on `modes`. With
|
|
181
|
+
// the shared `EMPTY_MODES` default this resolves once for the common path
|
|
182
|
+
// and never re-allocates the container/slot style objects between renders.
|
|
183
|
+
const { containerStyle, slotStyle } = useMemo(() => {
|
|
184
|
+
const backgroundColor =
|
|
185
|
+
(getVariableByName('actionFooter/background', modes) ?? '#ffffff') as string
|
|
186
|
+
const gap = (getVariableByName('actionFooter/gap', modes) ?? 8) as number
|
|
187
|
+
const paddingHorizontal =
|
|
188
|
+
(getVariableByName('actionFooter/padding/horizontal', modes) ?? 16) as number
|
|
189
|
+
const paddingTop = (getVariableByName('actionFooter/padding/top', modes) ?? 10) as number
|
|
190
|
+
const paddingBottom = (getVariableByName('actionFooter/padding/bottom', modes) ?? 41) as number
|
|
191
|
+
|
|
192
|
+
const container: ViewStyle = {
|
|
193
|
+
// `alignSelf: 'stretch'` is the cross-platform way to ask "fill the
|
|
194
|
+
// parent's cross axis" — in the common case (column parent) this gives
|
|
195
|
+
// us full-width without the caller needing to pass `width: '100%'`.
|
|
196
|
+
alignSelf: 'stretch',
|
|
197
|
+
backgroundColor,
|
|
198
|
+
paddingLeft: paddingHorizontal,
|
|
199
|
+
paddingRight: paddingHorizontal,
|
|
200
|
+
paddingTop,
|
|
201
|
+
paddingBottom,
|
|
202
|
+
...NATIVE_SHADOW,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const slot: ViewStyle = {
|
|
206
|
+
flexDirection: 'row',
|
|
207
|
+
// Vertically center the IconButton against the slightly taller Buttons
|
|
208
|
+
// so the row reads as a single optical baseline.
|
|
209
|
+
alignItems: 'center',
|
|
210
|
+
gap,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { containerStyle: container, slotStyle: slot }
|
|
214
|
+
}, [modes])
|
|
215
|
+
|
|
216
|
+
// Process children once per (children, modes) tuple:
|
|
217
|
+
// 1. Flatten Fragments so each action is its own keyed sibling.
|
|
218
|
+
// 2. Push `modes` down so callers don't have to thread it manually.
|
|
219
|
+
// 3. Auto-stretch every non-IconButton with the Yoga-safe stretch style.
|
|
220
|
+
//
|
|
221
|
+
// The result identity is stable across re-renders when the inputs don't
|
|
222
|
+
// change, which keeps the `React.memo`-wrapped Button/IconButton children
|
|
223
|
+
// from re-rendering for no reason.
|
|
224
|
+
const enhancedChildren = useMemo(() => {
|
|
225
|
+
const flat = flattenChildren(children)
|
|
226
|
+
const withModes = cloneChildrenWithModes(flat, modes) as React.ReactNode[]
|
|
227
|
+
return withModes.map((child, index) => {
|
|
228
|
+
if (!React.isValidElement(child)) return child
|
|
229
|
+
const element = child as React.ReactElement<any>
|
|
230
|
+
if (isIconButtonElement(element)) return element
|
|
231
|
+
return React.cloneElement(element, {
|
|
232
|
+
key: element.key ?? `action-footer-item-${index}`,
|
|
233
|
+
style: [STRETCH_STYLE, element.props.style],
|
|
234
|
+
})
|
|
122
235
|
})
|
|
123
|
-
})
|
|
236
|
+
}, [children, modes])
|
|
124
237
|
|
|
125
238
|
return (
|
|
126
|
-
<View
|
|
127
|
-
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
|
+
]}
|
|
128
249
|
accessibilityRole="toolbar"
|
|
129
|
-
accessibilityLabel={
|
|
250
|
+
accessibilityLabel={accessibilityLabel}
|
|
130
251
|
>
|
|
131
|
-
<View style={slotStyle}>
|
|
132
|
-
|
|
133
|
-
</View>
|
|
134
|
-
</View>
|
|
252
|
+
<View style={slotStyle}>{enhancedChildren}</View>
|
|
253
|
+
</Animated.View>
|
|
135
254
|
)
|
|
136
255
|
}
|
|
137
256
|
|
|
138
|
-
export default ActionFooter
|
|
139
|
-
|
|
257
|
+
export default React.memo(ActionFooter)
|