jfs-components 0.0.72 → 0.0.74
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 +28 -0
- package/lib/commonjs/components/AccordionCheckbox/AccordionCheckbox.js +239 -0
- package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
- package/lib/commonjs/components/AppBar/AppBar.js +17 -11
- package/lib/commonjs/components/BrandChip/BrandChip.js +149 -0
- package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +229 -0
- package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
- package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +140 -0
- package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
- package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
- package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
- package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
- package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
- package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
- package/lib/commonjs/components/FormField/FormField.js +328 -178
- package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
- package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
- package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
- package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
- package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
- package/lib/commonjs/components/OTP/OTP.js +381 -37
- package/lib/commonjs/components/PageHero/PageHero.js +153 -0
- package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
- package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
- package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
- package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
- package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
- package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
- package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
- package/lib/commonjs/components/StatItem/StatItem.js +65 -35
- package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
- package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
- package/lib/commonjs/components/Text/Text.js +9 -2
- package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
- package/lib/commonjs/components/index.js +231 -1
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/utils/index.js +7 -0
- package/lib/commonjs/utils/number-utils.js +57 -0
- package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
- package/lib/module/components/AccountCard/AccountCard.js +241 -0
- package/lib/module/components/AppBar/AppBar.js +17 -11
- package/lib/module/components/BrandChip/BrandChip.js +143 -0
- package/lib/module/components/CardBankAccount/CardBankAccount.js +223 -0
- package/lib/module/components/CardInsight/CardInsight.js +161 -0
- package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
- package/lib/module/components/CheckboxItem/CheckboxItem.js +134 -0
- package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
- package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
- package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
- package/lib/module/components/DonutChart/DonutChart.js +303 -0
- package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
- package/lib/module/components/Dropdown/Dropdown.js +206 -0
- package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
- package/lib/module/components/FormField/FormField.js +330 -180
- package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
- package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
- package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
- package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
- package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
- package/lib/module/components/OTP/OTP.js +381 -38
- package/lib/module/components/PageHero/PageHero.js +147 -0
- package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
- package/lib/module/components/PoweredByLabel/finvu.png +0 -0
- package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
- package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
- package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
- package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
- package/lib/module/components/StatGroup/StatGroup.js +123 -0
- package/lib/module/components/StatItem/StatItem.js +66 -36
- package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
- package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
- package/lib/module/components/Text/Text.js +9 -2
- package/lib/module/components/Tooltip/Tooltip.js +34 -27
- package/lib/module/components/index.js +28 -2
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/utils/index.js +2 -1
- package/lib/module/utils/number-utils.js +53 -0
- package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
- package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
- package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
- package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +86 -0
- package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
- package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +72 -0
- package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
- package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
- package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
- package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
- package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
- package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
- package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
- package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
- package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
- package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
- package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
- package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +53 -0
- package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
- package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
- package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
- package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
- package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
- package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
- package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
- package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
- package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
- package/lib/typescript/src/components/Text/Text.d.ts +12 -2
- package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
- package/lib/typescript/src/components/index.d.ts +29 -3
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/utils/index.d.ts +1 -0
- package/lib/typescript/src/utils/number-utils.d.ts +29 -0
- package/package.json +1 -3
- package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
- package/src/components/AccountCard/AccountCard.tsx +376 -0
- package/src/components/AppBar/AppBar.tsx +25 -14
- package/src/components/BrandChip/BrandChip.tsx +235 -0
- package/src/components/CardBankAccount/CardBankAccount.tsx +321 -0
- package/src/components/CardInsight/CardInsight.tsx +239 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
- package/src/components/CheckboxItem/CheckboxItem.tsx +209 -0
- package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
- package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
- package/src/components/CoverageRing/CoverageRing.tsx +225 -0
- package/src/components/DonutChart/DonutChart.tsx +503 -0
- package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
- package/src/components/Dropdown/Dropdown.tsx +331 -0
- package/src/components/DropdownInput/DropdownInput.tsx +819 -0
- package/src/components/FormField/FormField.tsx +542 -215
- package/src/components/LinearMeter/LinearMeter.tsx +9 -39
- package/src/components/LinearProgress/LinearProgress.tsx +92 -0
- package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
- package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
- package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
- package/src/components/OTP/OTP.tsx +476 -29
- package/src/components/PageHero/PageHero.tsx +200 -0
- package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
- package/src/components/PoweredByLabel/finvu.png +0 -0
- package/src/components/ProductOverview/ProductOverview.tsx +236 -0
- package/src/components/RangeTrack/RangeTrack.tsx +394 -0
- package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
- package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
- package/src/components/StatGroup/StatGroup.tsx +169 -0
- package/src/components/StatItem/StatItem.tsx +117 -40
- package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
- package/src/components/SummaryTile/SummaryTile.tsx +251 -0
- package/src/components/Text/Text.tsx +24 -3
- package/src/components/Tooltip/Tooltip.tsx +50 -25
- package/src/components/index.ts +47 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/number-utils.ts +60 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Dimensions, Modal, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
5
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
7
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
8
|
+
import { EMPTY_MODES, flattenChildren } from '../../utils/react-utils';
|
|
9
|
+
import Icon from '../../icons/Icon';
|
|
10
|
+
import SupportText from '../SupportText/SupportText';
|
|
11
|
+
import Dropdown, { DropdownItem } from '../Dropdown/Dropdown';
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
const IS_WEB = Platform.OS === 'web';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Token resolution
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function useChevronTokens(modes) {
|
|
24
|
+
return useMemo(() => {
|
|
25
|
+
const iconSize = parseInt(getVariableByName('input/iconSize', modes), 10) || 32;
|
|
26
|
+
const iconColor = getVariableByName('iconButton/icon/color', modes) || '#0f0d0a';
|
|
27
|
+
return {
|
|
28
|
+
iconSize,
|
|
29
|
+
iconColor
|
|
30
|
+
};
|
|
31
|
+
}, [modes]);
|
|
32
|
+
}
|
|
33
|
+
function useFormFieldTokens(modes) {
|
|
34
|
+
return useMemo(() => {
|
|
35
|
+
const labelColor = getVariableByName('formField/label/color', modes) || '#0c0d10';
|
|
36
|
+
const labelFontFamily = getVariableByName('formField/label/fontFamily', modes) || 'JioType Var';
|
|
37
|
+
const labelFontSize = parseInt(getVariableByName('formField/label/fontSize', modes), 10) || 14;
|
|
38
|
+
const labelLineHeight = parseInt(getVariableByName('formField/label/lineHeight', modes), 10) || 17;
|
|
39
|
+
const labelFontWeight = getVariableByName('formField/label/fontWeight', modes) || '500';
|
|
40
|
+
const gap = parseInt(getVariableByName('formField/gap', modes), 10) || 8;
|
|
41
|
+
const inputPaddingH = parseInt(getVariableByName('formField/input/padding/horizontal', modes), 10) || 12;
|
|
42
|
+
const inputGap = parseInt(getVariableByName('formField/input/gap', modes), 10) || 8;
|
|
43
|
+
const inputRadius = parseInt(getVariableByName('formField/input/radius', modes), 10) || 8;
|
|
44
|
+
const inputBackground = getVariableByName('formField/input/background', modes) || '#ffffff';
|
|
45
|
+
const inputFontSize = parseInt(getVariableByName('formField/input/label/fontSize', modes), 10) || 16;
|
|
46
|
+
const inputLineHeight = parseInt(getVariableByName('formField/input/label/lineHeight', modes), 10) || 45;
|
|
47
|
+
const inputFontFamily = getVariableByName('formField/input/label/fontFamily', modes) || 'JioType Var';
|
|
48
|
+
const inputFontWeight = getVariableByName('formField/input/label/fontWeight', modes) || '400';
|
|
49
|
+
const inputTextColor = getVariableByName('states/formField/input/label/color', modes) || getVariableByName('formField/input/label/color', modes) || '#24262b';
|
|
50
|
+
const inputBorderColor = getVariableByName('states/formField/input/border/color', modes) || getVariableByName('formField/input/border/color', modes) || '#b5b6b7';
|
|
51
|
+
const inputBorderSize = parseInt(getVariableByName('formField/input/border/size', modes), 10) || 1;
|
|
52
|
+
return {
|
|
53
|
+
labelColor,
|
|
54
|
+
labelFontFamily,
|
|
55
|
+
labelFontSize,
|
|
56
|
+
labelLineHeight,
|
|
57
|
+
labelFontWeight,
|
|
58
|
+
gap,
|
|
59
|
+
inputPaddingH,
|
|
60
|
+
inputGap,
|
|
61
|
+
inputRadius,
|
|
62
|
+
inputBackground,
|
|
63
|
+
inputFontSize,
|
|
64
|
+
inputLineHeight,
|
|
65
|
+
inputFontFamily,
|
|
66
|
+
inputFontWeight,
|
|
67
|
+
inputTextColor,
|
|
68
|
+
inputBorderColor,
|
|
69
|
+
inputBorderSize
|
|
70
|
+
};
|
|
71
|
+
}, [modes]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Collect every option this DropdownInput knows about, in render order, from
|
|
80
|
+
* both `items` and `children` slots. Used for keyboard navigation, lookups
|
|
81
|
+
* of the selected option, and accessibility labels.
|
|
82
|
+
*/
|
|
83
|
+
function collectOptionsFromChildren(children) {
|
|
84
|
+
const out = [];
|
|
85
|
+
flattenChildren(children).forEach(child => {
|
|
86
|
+
if (! /*#__PURE__*/React.isValidElement(child)) return;
|
|
87
|
+
if (child.type !== DropdownItem) return;
|
|
88
|
+
const childProps = child.props;
|
|
89
|
+
const {
|
|
90
|
+
value,
|
|
91
|
+
label,
|
|
92
|
+
disabled
|
|
93
|
+
} = childProps;
|
|
94
|
+
if (value == null) return;
|
|
95
|
+
if (typeof value !== 'string' && typeof value !== 'number') return;
|
|
96
|
+
if (typeof label !== 'string') return;
|
|
97
|
+
const opt = {
|
|
98
|
+
value: value,
|
|
99
|
+
label
|
|
100
|
+
};
|
|
101
|
+
if (disabled != null) opt.disabled = disabled;
|
|
102
|
+
out.push(opt);
|
|
103
|
+
});
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Component
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
function DropdownInput({
|
|
112
|
+
label,
|
|
113
|
+
placeholder = 'Select an option',
|
|
114
|
+
items,
|
|
115
|
+
value,
|
|
116
|
+
defaultValue = null,
|
|
117
|
+
onValueChange,
|
|
118
|
+
children,
|
|
119
|
+
renderValue,
|
|
120
|
+
open,
|
|
121
|
+
defaultOpen = false,
|
|
122
|
+
onOpenChange,
|
|
123
|
+
placement = 'bottom',
|
|
124
|
+
isRequired = false,
|
|
125
|
+
isDisabled = false,
|
|
126
|
+
isInvalid = false,
|
|
127
|
+
isReadOnly = false,
|
|
128
|
+
supportText,
|
|
129
|
+
errorMessage,
|
|
130
|
+
menuMaxHeight = 240,
|
|
131
|
+
menuOffset = 4,
|
|
132
|
+
matchTriggerWidth = true,
|
|
133
|
+
closeOnBackdropPress = true,
|
|
134
|
+
modes: propModes = EMPTY_MODES,
|
|
135
|
+
style,
|
|
136
|
+
inputStyle,
|
|
137
|
+
menuStyle,
|
|
138
|
+
accessibilityLabel,
|
|
139
|
+
accessibilityHint,
|
|
140
|
+
onFocus,
|
|
141
|
+
onBlur
|
|
142
|
+
}) {
|
|
143
|
+
// ---------------- Modes ----------------
|
|
144
|
+
const {
|
|
145
|
+
modes: globalModes
|
|
146
|
+
} = useTokens();
|
|
147
|
+
const baseModes = useMemo(() => ({
|
|
148
|
+
...globalModes,
|
|
149
|
+
...propModes
|
|
150
|
+
}), [globalModes, propModes]);
|
|
151
|
+
|
|
152
|
+
// ---------------- Open state ----------------
|
|
153
|
+
const isControlledOpen = open !== undefined;
|
|
154
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
155
|
+
const isOpen = (isControlledOpen ? open : internalOpen) && !isDisabled && !isReadOnly;
|
|
156
|
+
const setOpenState = useCallback(next => {
|
|
157
|
+
if (!isControlledOpen) setInternalOpen(next);
|
|
158
|
+
onOpenChange?.(next);
|
|
159
|
+
}, [isControlledOpen, onOpenChange]);
|
|
160
|
+
const closeMenu = useCallback(() => setOpenState(false), [setOpenState]);
|
|
161
|
+
const toggleMenu = useCallback(() => setOpenState(!isOpen), [isOpen, setOpenState]);
|
|
162
|
+
|
|
163
|
+
// ---------------- Value state ----------------
|
|
164
|
+
const isControlledValue = value !== undefined;
|
|
165
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
166
|
+
const currentValue = isControlledValue ? value : internalValue;
|
|
167
|
+
|
|
168
|
+
// Combine items + children-derived options into a single lookup table so
|
|
169
|
+
// selecting via either API surfaces the same option metadata.
|
|
170
|
+
const childOptions = useMemo(() => collectOptionsFromChildren(children), [children]);
|
|
171
|
+
const allOptions = useMemo(() => [...(items ?? []), ...childOptions], [items, childOptions]);
|
|
172
|
+
const selectedOption = useMemo(() => allOptions.find(o => o.value === currentValue), [allOptions, currentValue]);
|
|
173
|
+
const handleSelect = useCallback(selectedValue => {
|
|
174
|
+
if (typeof selectedValue !== 'string' && typeof selectedValue !== 'number') {
|
|
175
|
+
// Items without a meaningful value just close the menu.
|
|
176
|
+
closeMenu();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const option = allOptions.find(o => o.value === selectedValue);
|
|
180
|
+
if (option?.disabled) return;
|
|
181
|
+
if (!isControlledValue) setInternalValue(selectedValue);
|
|
182
|
+
onValueChange?.(selectedValue, option);
|
|
183
|
+
closeMenu();
|
|
184
|
+
}, [allOptions, closeMenu, isControlledValue, onValueChange]);
|
|
185
|
+
|
|
186
|
+
// ---------------- Token modes (with state cascade) ----------------
|
|
187
|
+
const modes = useMemo(() => ({
|
|
188
|
+
...baseModes,
|
|
189
|
+
'FormField States': isInvalid ? 'Error' : isReadOnly ? 'Read Only' : isOpen ? 'Active' : baseModes['FormField States'] || 'Idle'
|
|
190
|
+
}), [baseModes, isInvalid, isReadOnly, isOpen]);
|
|
191
|
+
const tokens = useFormFieldTokens(modes);
|
|
192
|
+
const chevron = useChevronTokens(modes);
|
|
193
|
+
|
|
194
|
+
// ---------------- Layout / measurement ----------------
|
|
195
|
+
const triggerRef = useRef(null);
|
|
196
|
+
const [triggerRect, setTriggerRect] = useState(null);
|
|
197
|
+
const insets = useSafeAreaInsets();
|
|
198
|
+
const measure = useCallback(() => {
|
|
199
|
+
if (!triggerRef.current) return;
|
|
200
|
+
triggerRef.current.measureInWindow((x, y, width, height) => {
|
|
201
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
setTriggerRect(prev => {
|
|
205
|
+
if (!prev || Math.abs(prev.x - x) > 0.5 || Math.abs(prev.y - y) > 0.5 || prev.width !== width || prev.height !== height) {
|
|
206
|
+
return {
|
|
207
|
+
x,
|
|
208
|
+
y,
|
|
209
|
+
width,
|
|
210
|
+
height
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return prev;
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
218
|
+
// Keep the trigger rect in sync while the menu is open (handles scroll,
|
|
219
|
+
// window resize, etc.). One rAF tick per frame is enough; we bail early
|
|
220
|
+
// if the rect hasn't changed so React doesn't re-render unnecessarily.
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!isOpen) return;
|
|
223
|
+
let raf = 0;
|
|
224
|
+
const loop = () => {
|
|
225
|
+
measure();
|
|
226
|
+
raf = requestAnimationFrame(loop);
|
|
227
|
+
};
|
|
228
|
+
loop();
|
|
229
|
+
return () => {
|
|
230
|
+
if (raf) cancelAnimationFrame(raf);
|
|
231
|
+
};
|
|
232
|
+
}, [isOpen, measure]);
|
|
233
|
+
const handleTriggerLayout = useCallback(_e => {
|
|
234
|
+
measure();
|
|
235
|
+
}, [measure]);
|
|
236
|
+
|
|
237
|
+
// ---------------- Popup positioning ----------------
|
|
238
|
+
const [menuSize, setMenuSize] = useState(null);
|
|
239
|
+
const handleMenuLayout = useCallback(e => {
|
|
240
|
+
const {
|
|
241
|
+
width,
|
|
242
|
+
height
|
|
243
|
+
} = e.nativeEvent.layout;
|
|
244
|
+
setMenuSize(prev => {
|
|
245
|
+
if (!prev || prev.width !== width || prev.height !== height) {
|
|
246
|
+
return {
|
|
247
|
+
width,
|
|
248
|
+
height
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return prev;
|
|
252
|
+
});
|
|
253
|
+
}, []);
|
|
254
|
+
const {
|
|
255
|
+
width: windowWidth,
|
|
256
|
+
height: windowHeight
|
|
257
|
+
} = Dimensions.get('window');
|
|
258
|
+
const computedPlacement = useMemo(() => {
|
|
259
|
+
if (!triggerRect) return placement === 'top' ? 'top' : 'bottom';
|
|
260
|
+
const spaceBelow = windowHeight - (triggerRect.y + triggerRect.height) - insets.bottom;
|
|
261
|
+
const spaceAbove = triggerRect.y - insets.top;
|
|
262
|
+
const desiredHeight = Math.min(menuSize?.height ?? menuMaxHeight, menuMaxHeight);
|
|
263
|
+
const needed = desiredHeight + menuOffset + 8;
|
|
264
|
+
if (placement === 'top') {
|
|
265
|
+
return spaceAbove >= needed || spaceAbove >= spaceBelow ? 'top' : 'bottom';
|
|
266
|
+
}
|
|
267
|
+
if (placement === 'bottom') {
|
|
268
|
+
return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
|
|
269
|
+
}
|
|
270
|
+
return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
|
|
271
|
+
}, [triggerRect, placement, windowHeight, menuSize?.height, menuMaxHeight, menuOffset, insets.top, insets.bottom]);
|
|
272
|
+
const popupStyle = useMemo(() => {
|
|
273
|
+
if (!triggerRect) {
|
|
274
|
+
return {
|
|
275
|
+
position: 'absolute',
|
|
276
|
+
opacity: 0,
|
|
277
|
+
top: 0,
|
|
278
|
+
left: 0
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const screenPadding = 8;
|
|
282
|
+
const width = matchTriggerWidth ? triggerRect.width : undefined;
|
|
283
|
+
const intrinsicWidth = menuSize?.width ?? triggerRect.width;
|
|
284
|
+
const finalWidth = width ?? intrinsicWidth;
|
|
285
|
+
let leftPos = triggerRect.x;
|
|
286
|
+
const maxLeft = windowWidth - insets.right - finalWidth - screenPadding;
|
|
287
|
+
const minLeft = insets.left + screenPadding;
|
|
288
|
+
if (leftPos > maxLeft) leftPos = maxLeft;
|
|
289
|
+
if (leftPos < minLeft) leftPos = minLeft;
|
|
290
|
+
let topPos;
|
|
291
|
+
if (computedPlacement === 'top') {
|
|
292
|
+
const desiredHeight = menuSize?.height ?? menuMaxHeight;
|
|
293
|
+
topPos = triggerRect.y - desiredHeight - menuOffset;
|
|
294
|
+
if (topPos < insets.top + screenPadding) {
|
|
295
|
+
topPos = insets.top + screenPadding;
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
topPos = triggerRect.y + triggerRect.height + menuOffset;
|
|
299
|
+
}
|
|
300
|
+
const style = {
|
|
301
|
+
position: 'absolute',
|
|
302
|
+
top: topPos,
|
|
303
|
+
left: leftPos
|
|
304
|
+
};
|
|
305
|
+
if (width != null) style.width = width;
|
|
306
|
+
// Hide first frame before measurement to avoid the popup flashing in
|
|
307
|
+
// the wrong place. menuSize becomes truthy after the first layout.
|
|
308
|
+
if (menuSize == null) style.opacity = 0;
|
|
309
|
+
return style;
|
|
310
|
+
}, [triggerRect, computedPlacement, menuSize, menuOffset, menuMaxHeight, matchTriggerWidth, windowWidth, insets.top, insets.left, insets.right]);
|
|
311
|
+
|
|
312
|
+
// Reset menu size when closing so the next open re-measures (handles items
|
|
313
|
+
// changing while the menu was closed).
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (!isOpen) setMenuSize(null);
|
|
316
|
+
}, [isOpen]);
|
|
317
|
+
|
|
318
|
+
// ---------------- Styles ----------------
|
|
319
|
+
const labelTextStyle = {
|
|
320
|
+
color: tokens.labelColor,
|
|
321
|
+
fontFamily: tokens.labelFontFamily,
|
|
322
|
+
fontSize: tokens.labelFontSize,
|
|
323
|
+
lineHeight: tokens.labelLineHeight,
|
|
324
|
+
fontWeight: tokens.labelFontWeight
|
|
325
|
+
};
|
|
326
|
+
const requiredIndicatorStyle = {
|
|
327
|
+
...labelTextStyle,
|
|
328
|
+
color: '#d93d3d'
|
|
329
|
+
};
|
|
330
|
+
const wrapperStyle = {
|
|
331
|
+
gap: tokens.gap,
|
|
332
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
333
|
+
width: '100%'
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Focus ring uses the resolved input border color from FormField States so
|
|
337
|
+
// active/error look consistent with TextInput-based FormField. We also lift
|
|
338
|
+
// border weight to 2 when "Active" to read as a focus ring.
|
|
339
|
+
const inputRowStyle = {
|
|
340
|
+
flexDirection: 'row',
|
|
341
|
+
alignItems: 'center',
|
|
342
|
+
backgroundColor: tokens.inputBackground,
|
|
343
|
+
borderColor: tokens.inputBorderColor,
|
|
344
|
+
borderWidth: isOpen ? Math.max(tokens.inputBorderSize, 1) : tokens.inputBorderSize,
|
|
345
|
+
borderRadius: tokens.inputRadius,
|
|
346
|
+
paddingHorizontal: tokens.inputPaddingH,
|
|
347
|
+
paddingVertical: 0,
|
|
348
|
+
gap: tokens.inputGap,
|
|
349
|
+
minHeight: tokens.inputLineHeight
|
|
350
|
+
};
|
|
351
|
+
const valueTextStyle = {
|
|
352
|
+
flex: 1,
|
|
353
|
+
color: tokens.inputTextColor,
|
|
354
|
+
fontFamily: tokens.inputFontFamily,
|
|
355
|
+
fontSize: tokens.inputFontSize,
|
|
356
|
+
lineHeight: tokens.inputLineHeight,
|
|
357
|
+
fontWeight: tokens.inputFontWeight,
|
|
358
|
+
paddingVertical: 0
|
|
359
|
+
};
|
|
360
|
+
const placeholderColor = '#888a8d';
|
|
361
|
+
|
|
362
|
+
// ---------------- Support text ----------------
|
|
363
|
+
const supportStatus = isInvalid ? 'Error' : 'Neutral';
|
|
364
|
+
const supportLabel = isInvalid && errorMessage ? errorMessage : supportText;
|
|
365
|
+
|
|
366
|
+
// ---------------- Accessibility ----------------
|
|
367
|
+
const resolvedA11yLabel = accessibilityLabel || label || placeholder || 'Dropdown';
|
|
368
|
+
const a11yProps = {
|
|
369
|
+
accessibilityRole: 'combobox',
|
|
370
|
+
accessibilityLabel: resolvedA11yLabel,
|
|
371
|
+
accessibilityState: {
|
|
372
|
+
disabled: isDisabled,
|
|
373
|
+
expanded: isOpen
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
if (accessibilityHint) a11yProps.accessibilityHint = accessibilityHint;
|
|
377
|
+
|
|
378
|
+
// ---------------- Items rendering ----------------
|
|
379
|
+
const renderItems = useCallback(() => {
|
|
380
|
+
const itemNodes = [];
|
|
381
|
+
if (items && items.length > 0) {
|
|
382
|
+
items.forEach(opt => {
|
|
383
|
+
const isSelected = opt.value === currentValue;
|
|
384
|
+
itemNodes.push(/*#__PURE__*/_jsx(DropdownItem, {
|
|
385
|
+
value: opt.value,
|
|
386
|
+
label: opt.label,
|
|
387
|
+
selected: isSelected,
|
|
388
|
+
disabled: opt.disabled ?? false,
|
|
389
|
+
leading: opt.leading,
|
|
390
|
+
trailing: opt.trailing,
|
|
391
|
+
onPress: handleSelect,
|
|
392
|
+
modes: modes
|
|
393
|
+
}, `item-${opt.value}`));
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (children) {
|
|
397
|
+
// Inject `selected` and `onPress` into child DropdownItems so the
|
|
398
|
+
// consumer doesn't have to wire selection by hand. Existing
|
|
399
|
+
// `onPress` handlers on a child are preserved and called after our
|
|
400
|
+
// selection logic runs.
|
|
401
|
+
flattenChildren(children).forEach((child, idx) => {
|
|
402
|
+
if (! /*#__PURE__*/React.isValidElement(child)) {
|
|
403
|
+
itemNodes.push(child);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (child.type === DropdownItem) {
|
|
407
|
+
const original = child.props;
|
|
408
|
+
const isSelected = original.value === currentValue;
|
|
409
|
+
const composedOnPress = v => {
|
|
410
|
+
original.onPress?.(v);
|
|
411
|
+
handleSelect(v);
|
|
412
|
+
};
|
|
413
|
+
itemNodes.push(/*#__PURE__*/React.cloneElement(child, {
|
|
414
|
+
key: child.key ?? `child-${idx}`,
|
|
415
|
+
selected: isSelected,
|
|
416
|
+
onPress: composedOnPress,
|
|
417
|
+
modes: {
|
|
418
|
+
...modes,
|
|
419
|
+
...(original.modes || {})
|
|
420
|
+
}
|
|
421
|
+
}));
|
|
422
|
+
} else {
|
|
423
|
+
itemNodes.push(child);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return itemNodes;
|
|
428
|
+
}, [items, children, currentValue, handleSelect, modes]);
|
|
429
|
+
|
|
430
|
+
// ---------------- Render ----------------
|
|
431
|
+
const hasValue = selectedOption != null;
|
|
432
|
+
const displayLabel = hasValue ? selectedOption.label : placeholder;
|
|
433
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
434
|
+
style: [wrapperStyle, style],
|
|
435
|
+
pointerEvents: isDisabled ? 'none' : 'auto',
|
|
436
|
+
children: [label != null && /*#__PURE__*/_jsxs(View, {
|
|
437
|
+
style: styles.labelRow,
|
|
438
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
439
|
+
style: labelTextStyle,
|
|
440
|
+
children: label
|
|
441
|
+
}), isRequired && /*#__PURE__*/_jsx(Text, {
|
|
442
|
+
style: requiredIndicatorStyle,
|
|
443
|
+
children: " *"
|
|
444
|
+
})]
|
|
445
|
+
}), /*#__PURE__*/_jsxs(Pressable, {
|
|
446
|
+
ref: triggerRef,
|
|
447
|
+
onLayout: handleTriggerLayout,
|
|
448
|
+
onPress: () => {
|
|
449
|
+
if (isDisabled || isReadOnly) return;
|
|
450
|
+
measure();
|
|
451
|
+
toggleMenu();
|
|
452
|
+
},
|
|
453
|
+
...(onFocus ? {
|
|
454
|
+
onFocus
|
|
455
|
+
} : {}),
|
|
456
|
+
...(onBlur ? {
|
|
457
|
+
onBlur
|
|
458
|
+
} : {}),
|
|
459
|
+
style: [inputRowStyle, inputStyle, IS_WEB && webNoOutline],
|
|
460
|
+
...a11yProps,
|
|
461
|
+
...(IS_WEB ? {
|
|
462
|
+
accessibilityRole: 'combobox',
|
|
463
|
+
'aria-haspopup': 'listbox',
|
|
464
|
+
'aria-expanded': isOpen
|
|
465
|
+
} : {}),
|
|
466
|
+
children: [renderValue ? /*#__PURE__*/_jsx(View, {
|
|
467
|
+
style: {
|
|
468
|
+
flex: 1,
|
|
469
|
+
justifyContent: 'center'
|
|
470
|
+
},
|
|
471
|
+
children: renderValue(selectedOption, hasValue)
|
|
472
|
+
}) : /*#__PURE__*/_jsx(Text, {
|
|
473
|
+
style: [valueTextStyle, !hasValue && {
|
|
474
|
+
color: placeholderColor
|
|
475
|
+
}],
|
|
476
|
+
numberOfLines: 1,
|
|
477
|
+
children: displayLabel
|
|
478
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
479
|
+
accessibilityElementsHidden: true,
|
|
480
|
+
importantForAccessibility: "no",
|
|
481
|
+
pointerEvents: "none",
|
|
482
|
+
children: /*#__PURE__*/_jsx(Icon, {
|
|
483
|
+
name: isOpen ? 'ic_chevron_up' : 'ic_chevron_down',
|
|
484
|
+
size: chevron.iconSize,
|
|
485
|
+
color: chevron.iconColor
|
|
486
|
+
})
|
|
487
|
+
})]
|
|
488
|
+
}), supportLabel != null && /*#__PURE__*/_jsx(SupportText, {
|
|
489
|
+
label: supportLabel,
|
|
490
|
+
status: supportStatus,
|
|
491
|
+
modes: modes
|
|
492
|
+
}), /*#__PURE__*/_jsx(Modal, {
|
|
493
|
+
visible: isOpen,
|
|
494
|
+
transparent: true,
|
|
495
|
+
animationType: "fade",
|
|
496
|
+
onRequestClose: closeMenu,
|
|
497
|
+
statusBarTranslucent: true,
|
|
498
|
+
children: /*#__PURE__*/_jsx(Pressable, {
|
|
499
|
+
style: StyleSheet.absoluteFill,
|
|
500
|
+
onPress: closeOnBackdropPress ? closeMenu : undefined,
|
|
501
|
+
accessibilityRole: "button",
|
|
502
|
+
accessibilityLabel: "Close options",
|
|
503
|
+
accessible: false,
|
|
504
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
505
|
+
style: StyleSheet.absoluteFill,
|
|
506
|
+
pointerEvents: "box-none",
|
|
507
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
508
|
+
style: popupStyle,
|
|
509
|
+
onLayout: handleMenuLayout,
|
|
510
|
+
pointerEvents: "auto",
|
|
511
|
+
children: /*#__PURE__*/_jsx(Dropdown, {
|
|
512
|
+
modes: modes,
|
|
513
|
+
maxHeight: menuMaxHeight,
|
|
514
|
+
style: menuStyle,
|
|
515
|
+
accessibilityLabel: `${resolvedA11yLabel} options`,
|
|
516
|
+
children: renderItems()
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
})]
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
const webNoOutline = {
|
|
525
|
+
outlineStyle: 'none',
|
|
526
|
+
outlineWidth: 0,
|
|
527
|
+
outlineColor: 'transparent',
|
|
528
|
+
cursor: 'pointer'
|
|
529
|
+
};
|
|
530
|
+
const styles = StyleSheet.create({
|
|
531
|
+
labelRow: {
|
|
532
|
+
flexDirection: 'row',
|
|
533
|
+
alignItems: 'baseline'
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
export default DropdownInput;
|