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,819 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react'
|
|
8
|
+
import {
|
|
9
|
+
Dimensions,
|
|
10
|
+
Modal,
|
|
11
|
+
Platform,
|
|
12
|
+
Pressable,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
Text,
|
|
15
|
+
View,
|
|
16
|
+
type AccessibilityProps,
|
|
17
|
+
type LayoutChangeEvent,
|
|
18
|
+
type StyleProp,
|
|
19
|
+
type TextStyle,
|
|
20
|
+
type ViewStyle,
|
|
21
|
+
} from 'react-native'
|
|
22
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
23
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
24
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
25
|
+
import { EMPTY_MODES, flattenChildren } from '../../utils/react-utils'
|
|
26
|
+
import Icon from '../../icons/Icon'
|
|
27
|
+
import SupportText from '../SupportText/SupportText'
|
|
28
|
+
import type { SupportTextStatus } from '../SupportText/SupportTextIcon'
|
|
29
|
+
import Dropdown, { DropdownItem, type DropdownItemProps } from '../Dropdown/Dropdown'
|
|
30
|
+
|
|
31
|
+
const IS_WEB = Platform.OS === 'web'
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Types
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export type DropdownInputOptionValue = string | number
|
|
38
|
+
|
|
39
|
+
export type DropdownInputOption = {
|
|
40
|
+
/** Stable, unique value used to identify the option. */
|
|
41
|
+
value: DropdownInputOptionValue
|
|
42
|
+
/** Human-readable label rendered in the menu and selected display. */
|
|
43
|
+
label: string
|
|
44
|
+
/** Optional element rendered before the label inside the item. */
|
|
45
|
+
leading?: React.ReactNode
|
|
46
|
+
/** Optional element rendered after the label inside the item. */
|
|
47
|
+
trailing?: React.ReactNode
|
|
48
|
+
/** Whether the option is non-selectable. */
|
|
49
|
+
disabled?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Rect = { x: number; y: number; width: number; height: number }
|
|
53
|
+
|
|
54
|
+
export type DropdownInputProps = {
|
|
55
|
+
/** Label rendered above the input. */
|
|
56
|
+
label?: string
|
|
57
|
+
/** Placeholder text shown when no value is selected. */
|
|
58
|
+
placeholder?: string
|
|
59
|
+
/**
|
|
60
|
+
* Data-driven list of options. Mutually compatible with `children`; if
|
|
61
|
+
* both are provided the `items` are rendered first.
|
|
62
|
+
*/
|
|
63
|
+
items?: DropdownInputOption[]
|
|
64
|
+
/**
|
|
65
|
+
* Currently selected value (controlled). When `value` is `undefined` the
|
|
66
|
+
* component operates in uncontrolled mode and keeps its own state.
|
|
67
|
+
*/
|
|
68
|
+
value?: DropdownInputOptionValue | null
|
|
69
|
+
/** Initial selected value for uncontrolled mode. */
|
|
70
|
+
defaultValue?: DropdownInputOptionValue | null
|
|
71
|
+
/** Called when the selected value changes. */
|
|
72
|
+
onValueChange?: (
|
|
73
|
+
value: DropdownInputOptionValue | null,
|
|
74
|
+
option?: DropdownInputOption
|
|
75
|
+
) => void
|
|
76
|
+
/**
|
|
77
|
+
* Custom slot of `<DropdownItem />` children. Used when finer-grained
|
|
78
|
+
* control is needed (icons, custom layouts, etc.). When provided alongside
|
|
79
|
+
* `items`, both are rendered (items first).
|
|
80
|
+
*/
|
|
81
|
+
children?: React.ReactNode
|
|
82
|
+
/**
|
|
83
|
+
* Custom renderer for the trigger label. Receives the currently-selected
|
|
84
|
+
* option (if any) and a boolean indicating whether the field has a value.
|
|
85
|
+
*/
|
|
86
|
+
renderValue?: (
|
|
87
|
+
option: DropdownInputOption | undefined,
|
|
88
|
+
hasValue: boolean
|
|
89
|
+
) => React.ReactNode
|
|
90
|
+
/** Controlled open state. */
|
|
91
|
+
open?: boolean
|
|
92
|
+
/** Initial open state for uncontrolled mode. */
|
|
93
|
+
defaultOpen?: boolean
|
|
94
|
+
/** Called whenever the open state changes. */
|
|
95
|
+
onOpenChange?: (open: boolean) => void
|
|
96
|
+
/**
|
|
97
|
+
* Preferred placement for the popup. The component falls back to the
|
|
98
|
+
* opposite side automatically when there isn't enough room.
|
|
99
|
+
* @default 'bottom'
|
|
100
|
+
*/
|
|
101
|
+
placement?: 'bottom' | 'top' | 'auto'
|
|
102
|
+
/** Renders a required asterisk next to the label. */
|
|
103
|
+
isRequired?: boolean
|
|
104
|
+
/** Disables interaction and dims the field. */
|
|
105
|
+
isDisabled?: boolean
|
|
106
|
+
/** Marks the field as invalid and shows `errorMessage`. */
|
|
107
|
+
isInvalid?: boolean
|
|
108
|
+
/** Renders the field in read-only mode (non-interactive but not disabled). */
|
|
109
|
+
isReadOnly?: boolean
|
|
110
|
+
/** Helper text displayed below the input. */
|
|
111
|
+
supportText?: string
|
|
112
|
+
/** Replaces `supportText` when `isInvalid` is true. */
|
|
113
|
+
errorMessage?: string
|
|
114
|
+
/**
|
|
115
|
+
* Maximum height of the popup before it becomes scrollable. Helpful for
|
|
116
|
+
* long lists. Defaults to 240 (roughly 5 items).
|
|
117
|
+
*/
|
|
118
|
+
menuMaxHeight?: number
|
|
119
|
+
/**
|
|
120
|
+
* Pixel offset between the trigger and the popup. Defaults to 4 so the
|
|
121
|
+
* popup visually peeks below the input.
|
|
122
|
+
*/
|
|
123
|
+
menuOffset?: number
|
|
124
|
+
/**
|
|
125
|
+
* When true, the popup width matches the trigger width. When false, the
|
|
126
|
+
* popup uses its intrinsic content width but never exceeds the trigger.
|
|
127
|
+
* @default true
|
|
128
|
+
*/
|
|
129
|
+
matchTriggerWidth?: boolean
|
|
130
|
+
/** Whether tapping the backdrop closes the menu. */
|
|
131
|
+
closeOnBackdropPress?: boolean
|
|
132
|
+
/** Modes for design token resolution. */
|
|
133
|
+
modes?: Record<string, any>
|
|
134
|
+
/** Style overrides for the outermost wrapper. */
|
|
135
|
+
style?: StyleProp<ViewStyle>
|
|
136
|
+
/** Style overrides for the input row. */
|
|
137
|
+
inputStyle?: StyleProp<ViewStyle>
|
|
138
|
+
/** Style overrides for the popup container. */
|
|
139
|
+
menuStyle?: StyleProp<ViewStyle>
|
|
140
|
+
/** Accessibility label. Defaults to the visible label / placeholder. */
|
|
141
|
+
accessibilityLabel?: string
|
|
142
|
+
/** Accessibility hint. */
|
|
143
|
+
accessibilityHint?: string
|
|
144
|
+
/** Called when the trigger receives focus (web only). */
|
|
145
|
+
onFocus?: (e: any) => void
|
|
146
|
+
/** Called when the trigger loses focus (web only). */
|
|
147
|
+
onBlur?: (e: any) => void
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Token resolution
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
function useChevronTokens(modes: Record<string, any>) {
|
|
155
|
+
return useMemo(() => {
|
|
156
|
+
const iconSize =
|
|
157
|
+
parseInt(getVariableByName('input/iconSize', modes), 10) || 32
|
|
158
|
+
const iconColor =
|
|
159
|
+
(getVariableByName('iconButton/icon/color', modes) as string) ||
|
|
160
|
+
'#0f0d0a'
|
|
161
|
+
return { iconSize, iconColor }
|
|
162
|
+
}, [modes])
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function useFormFieldTokens(modes: Record<string, any>) {
|
|
166
|
+
return useMemo(() => {
|
|
167
|
+
const labelColor =
|
|
168
|
+
(getVariableByName('formField/label/color', modes) as string) ||
|
|
169
|
+
'#0c0d10'
|
|
170
|
+
const labelFontFamily =
|
|
171
|
+
(getVariableByName('formField/label/fontFamily', modes) as string) ||
|
|
172
|
+
'JioType Var'
|
|
173
|
+
const labelFontSize =
|
|
174
|
+
parseInt(getVariableByName('formField/label/fontSize', modes), 10) ||
|
|
175
|
+
14
|
|
176
|
+
const labelLineHeight =
|
|
177
|
+
parseInt(
|
|
178
|
+
getVariableByName('formField/label/lineHeight', modes),
|
|
179
|
+
10
|
|
180
|
+
) || 17
|
|
181
|
+
const labelFontWeight =
|
|
182
|
+
(getVariableByName('formField/label/fontWeight', modes) as string) ||
|
|
183
|
+
'500'
|
|
184
|
+
|
|
185
|
+
const gap = parseInt(getVariableByName('formField/gap', modes), 10) || 8
|
|
186
|
+
|
|
187
|
+
const inputPaddingH =
|
|
188
|
+
parseInt(
|
|
189
|
+
getVariableByName('formField/input/padding/horizontal', modes),
|
|
190
|
+
10
|
|
191
|
+
) || 12
|
|
192
|
+
const inputGap =
|
|
193
|
+
parseInt(getVariableByName('formField/input/gap', modes), 10) || 8
|
|
194
|
+
const inputRadius =
|
|
195
|
+
parseInt(getVariableByName('formField/input/radius', modes), 10) ||
|
|
196
|
+
8
|
|
197
|
+
const inputBackground =
|
|
198
|
+
(getVariableByName('formField/input/background', modes) as string) ||
|
|
199
|
+
'#ffffff'
|
|
200
|
+
const inputFontSize =
|
|
201
|
+
parseInt(
|
|
202
|
+
getVariableByName('formField/input/label/fontSize', modes),
|
|
203
|
+
10
|
|
204
|
+
) || 16
|
|
205
|
+
const inputLineHeight =
|
|
206
|
+
parseInt(
|
|
207
|
+
getVariableByName('formField/input/label/lineHeight', modes),
|
|
208
|
+
10
|
|
209
|
+
) || 45
|
|
210
|
+
const inputFontFamily =
|
|
211
|
+
(getVariableByName(
|
|
212
|
+
'formField/input/label/fontFamily',
|
|
213
|
+
modes
|
|
214
|
+
) as string) || 'JioType Var'
|
|
215
|
+
const inputFontWeight =
|
|
216
|
+
(getVariableByName(
|
|
217
|
+
'formField/input/label/fontWeight',
|
|
218
|
+
modes
|
|
219
|
+
) as string) || '400'
|
|
220
|
+
const inputTextColor =
|
|
221
|
+
(getVariableByName(
|
|
222
|
+
'states/formField/input/label/color',
|
|
223
|
+
modes
|
|
224
|
+
) as string) ||
|
|
225
|
+
(getVariableByName('formField/input/label/color', modes) as string) ||
|
|
226
|
+
'#24262b'
|
|
227
|
+
const inputBorderColor =
|
|
228
|
+
(getVariableByName(
|
|
229
|
+
'states/formField/input/border/color',
|
|
230
|
+
modes
|
|
231
|
+
) as string) ||
|
|
232
|
+
(getVariableByName('formField/input/border/color', modes) as string) ||
|
|
233
|
+
'#b5b6b7'
|
|
234
|
+
const inputBorderSize =
|
|
235
|
+
parseInt(
|
|
236
|
+
getVariableByName('formField/input/border/size', modes),
|
|
237
|
+
10
|
|
238
|
+
) || 1
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
labelColor,
|
|
242
|
+
labelFontFamily,
|
|
243
|
+
labelFontSize,
|
|
244
|
+
labelLineHeight,
|
|
245
|
+
labelFontWeight,
|
|
246
|
+
gap,
|
|
247
|
+
inputPaddingH,
|
|
248
|
+
inputGap,
|
|
249
|
+
inputRadius,
|
|
250
|
+
inputBackground,
|
|
251
|
+
inputFontSize,
|
|
252
|
+
inputLineHeight,
|
|
253
|
+
inputFontFamily,
|
|
254
|
+
inputFontWeight,
|
|
255
|
+
inputTextColor,
|
|
256
|
+
inputBorderColor,
|
|
257
|
+
inputBorderSize,
|
|
258
|
+
}
|
|
259
|
+
}, [modes])
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Helpers
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Collect every option this DropdownInput knows about, in render order, from
|
|
268
|
+
* both `items` and `children` slots. Used for keyboard navigation, lookups
|
|
269
|
+
* of the selected option, and accessibility labels.
|
|
270
|
+
*/
|
|
271
|
+
function collectOptionsFromChildren(
|
|
272
|
+
children: React.ReactNode
|
|
273
|
+
): DropdownInputOption[] {
|
|
274
|
+
const out: DropdownInputOption[] = []
|
|
275
|
+
flattenChildren(children).forEach((child) => {
|
|
276
|
+
if (!React.isValidElement(child)) return
|
|
277
|
+
if ((child.type as any) !== DropdownItem) return
|
|
278
|
+
const childProps = child.props as DropdownItemProps
|
|
279
|
+
const { value, label, disabled } = childProps
|
|
280
|
+
if (value == null) return
|
|
281
|
+
if (typeof value !== 'string' && typeof value !== 'number') return
|
|
282
|
+
if (typeof label !== 'string') return
|
|
283
|
+
const opt: DropdownInputOption = {
|
|
284
|
+
value: value as DropdownInputOptionValue,
|
|
285
|
+
label,
|
|
286
|
+
}
|
|
287
|
+
if (disabled != null) opt.disabled = disabled
|
|
288
|
+
out.push(opt)
|
|
289
|
+
})
|
|
290
|
+
return out
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Component
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
function DropdownInput({
|
|
298
|
+
label,
|
|
299
|
+
placeholder = 'Select an option',
|
|
300
|
+
items,
|
|
301
|
+
value,
|
|
302
|
+
defaultValue = null,
|
|
303
|
+
onValueChange,
|
|
304
|
+
children,
|
|
305
|
+
renderValue,
|
|
306
|
+
open,
|
|
307
|
+
defaultOpen = false,
|
|
308
|
+
onOpenChange,
|
|
309
|
+
placement = 'bottom',
|
|
310
|
+
isRequired = false,
|
|
311
|
+
isDisabled = false,
|
|
312
|
+
isInvalid = false,
|
|
313
|
+
isReadOnly = false,
|
|
314
|
+
supportText,
|
|
315
|
+
errorMessage,
|
|
316
|
+
menuMaxHeight = 240,
|
|
317
|
+
menuOffset = 4,
|
|
318
|
+
matchTriggerWidth = true,
|
|
319
|
+
closeOnBackdropPress = true,
|
|
320
|
+
modes: propModes = EMPTY_MODES,
|
|
321
|
+
style,
|
|
322
|
+
inputStyle,
|
|
323
|
+
menuStyle,
|
|
324
|
+
accessibilityLabel,
|
|
325
|
+
accessibilityHint,
|
|
326
|
+
onFocus,
|
|
327
|
+
onBlur,
|
|
328
|
+
}: DropdownInputProps) {
|
|
329
|
+
// ---------------- Modes ----------------
|
|
330
|
+
const { modes: globalModes } = useTokens()
|
|
331
|
+
const baseModes = useMemo(
|
|
332
|
+
() => ({ ...globalModes, ...propModes }),
|
|
333
|
+
[globalModes, propModes]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
// ---------------- Open state ----------------
|
|
337
|
+
const isControlledOpen = open !== undefined
|
|
338
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen)
|
|
339
|
+
const isOpen = (isControlledOpen ? open : internalOpen) && !isDisabled && !isReadOnly
|
|
340
|
+
|
|
341
|
+
const setOpenState = useCallback(
|
|
342
|
+
(next: boolean) => {
|
|
343
|
+
if (!isControlledOpen) setInternalOpen(next)
|
|
344
|
+
onOpenChange?.(next)
|
|
345
|
+
},
|
|
346
|
+
[isControlledOpen, onOpenChange]
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
const closeMenu = useCallback(() => setOpenState(false), [setOpenState])
|
|
350
|
+
const toggleMenu = useCallback(() => setOpenState(!isOpen), [isOpen, setOpenState])
|
|
351
|
+
|
|
352
|
+
// ---------------- Value state ----------------
|
|
353
|
+
const isControlledValue = value !== undefined
|
|
354
|
+
const [internalValue, setInternalValue] = useState<
|
|
355
|
+
DropdownInputOptionValue | null
|
|
356
|
+
>(defaultValue)
|
|
357
|
+
const currentValue: DropdownInputOptionValue | null = isControlledValue
|
|
358
|
+
? (value as DropdownInputOptionValue | null)
|
|
359
|
+
: internalValue
|
|
360
|
+
|
|
361
|
+
// Combine items + children-derived options into a single lookup table so
|
|
362
|
+
// selecting via either API surfaces the same option metadata.
|
|
363
|
+
const childOptions = useMemo(
|
|
364
|
+
() => collectOptionsFromChildren(children),
|
|
365
|
+
[children]
|
|
366
|
+
)
|
|
367
|
+
const allOptions = useMemo<DropdownInputOption[]>(
|
|
368
|
+
() => [...(items ?? []), ...childOptions],
|
|
369
|
+
[items, childOptions]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
const selectedOption = useMemo(
|
|
373
|
+
() => allOptions.find((o) => o.value === currentValue),
|
|
374
|
+
[allOptions, currentValue]
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
const handleSelect = useCallback(
|
|
378
|
+
(selectedValue: DropdownItemProps['value']) => {
|
|
379
|
+
if (
|
|
380
|
+
typeof selectedValue !== 'string' &&
|
|
381
|
+
typeof selectedValue !== 'number'
|
|
382
|
+
) {
|
|
383
|
+
// Items without a meaningful value just close the menu.
|
|
384
|
+
closeMenu()
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
const option = allOptions.find((o) => o.value === selectedValue)
|
|
388
|
+
if (option?.disabled) return
|
|
389
|
+
if (!isControlledValue) setInternalValue(selectedValue)
|
|
390
|
+
onValueChange?.(selectedValue, option)
|
|
391
|
+
closeMenu()
|
|
392
|
+
},
|
|
393
|
+
[allOptions, closeMenu, isControlledValue, onValueChange]
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
// ---------------- Token modes (with state cascade) ----------------
|
|
397
|
+
const modes = useMemo(
|
|
398
|
+
() => ({
|
|
399
|
+
...baseModes,
|
|
400
|
+
'FormField States': isInvalid
|
|
401
|
+
? 'Error'
|
|
402
|
+
: isReadOnly
|
|
403
|
+
? 'Read Only'
|
|
404
|
+
: isOpen
|
|
405
|
+
? 'Active'
|
|
406
|
+
: (baseModes['FormField States'] as string) || 'Idle',
|
|
407
|
+
}),
|
|
408
|
+
[baseModes, isInvalid, isReadOnly, isOpen]
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
const tokens = useFormFieldTokens(modes)
|
|
412
|
+
const chevron = useChevronTokens(modes)
|
|
413
|
+
|
|
414
|
+
// ---------------- Layout / measurement ----------------
|
|
415
|
+
const triggerRef = useRef<View>(null)
|
|
416
|
+
const [triggerRect, setTriggerRect] = useState<Rect | null>(null)
|
|
417
|
+
const insets = useSafeAreaInsets()
|
|
418
|
+
|
|
419
|
+
const measure = useCallback(() => {
|
|
420
|
+
if (!triggerRef.current) return
|
|
421
|
+
triggerRef.current.measureInWindow((x, y, width, height) => {
|
|
422
|
+
if (
|
|
423
|
+
!Number.isFinite(x) ||
|
|
424
|
+
!Number.isFinite(y) ||
|
|
425
|
+
!Number.isFinite(width) ||
|
|
426
|
+
!Number.isFinite(height)
|
|
427
|
+
) {
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
setTriggerRect((prev) => {
|
|
431
|
+
if (
|
|
432
|
+
!prev ||
|
|
433
|
+
Math.abs(prev.x - x) > 0.5 ||
|
|
434
|
+
Math.abs(prev.y - y) > 0.5 ||
|
|
435
|
+
prev.width !== width ||
|
|
436
|
+
prev.height !== height
|
|
437
|
+
) {
|
|
438
|
+
return { x, y, width, height }
|
|
439
|
+
}
|
|
440
|
+
return prev
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
}, [])
|
|
444
|
+
|
|
445
|
+
// Keep the trigger rect in sync while the menu is open (handles scroll,
|
|
446
|
+
// window resize, etc.). One rAF tick per frame is enough; we bail early
|
|
447
|
+
// if the rect hasn't changed so React doesn't re-render unnecessarily.
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (!isOpen) return
|
|
450
|
+
let raf = 0
|
|
451
|
+
const loop = () => {
|
|
452
|
+
measure()
|
|
453
|
+
raf = requestAnimationFrame(loop)
|
|
454
|
+
}
|
|
455
|
+
loop()
|
|
456
|
+
return () => {
|
|
457
|
+
if (raf) cancelAnimationFrame(raf)
|
|
458
|
+
}
|
|
459
|
+
}, [isOpen, measure])
|
|
460
|
+
|
|
461
|
+
const handleTriggerLayout = useCallback(
|
|
462
|
+
(_e: LayoutChangeEvent) => {
|
|
463
|
+
measure()
|
|
464
|
+
},
|
|
465
|
+
[measure]
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
// ---------------- Popup positioning ----------------
|
|
469
|
+
const [menuSize, setMenuSize] = useState<{
|
|
470
|
+
width: number
|
|
471
|
+
height: number
|
|
472
|
+
} | null>(null)
|
|
473
|
+
|
|
474
|
+
const handleMenuLayout = useCallback((e: LayoutChangeEvent) => {
|
|
475
|
+
const { width, height } = e.nativeEvent.layout
|
|
476
|
+
setMenuSize((prev) => {
|
|
477
|
+
if (!prev || prev.width !== width || prev.height !== height) {
|
|
478
|
+
return { width, height }
|
|
479
|
+
}
|
|
480
|
+
return prev
|
|
481
|
+
})
|
|
482
|
+
}, [])
|
|
483
|
+
|
|
484
|
+
const { width: windowWidth, height: windowHeight } = Dimensions.get('window')
|
|
485
|
+
|
|
486
|
+
const computedPlacement = useMemo<'top' | 'bottom'>(() => {
|
|
487
|
+
if (!triggerRect) return placement === 'top' ? 'top' : 'bottom'
|
|
488
|
+
const spaceBelow =
|
|
489
|
+
windowHeight - (triggerRect.y + triggerRect.height) - insets.bottom
|
|
490
|
+
const spaceAbove = triggerRect.y - insets.top
|
|
491
|
+
const desiredHeight = Math.min(
|
|
492
|
+
menuSize?.height ?? menuMaxHeight,
|
|
493
|
+
menuMaxHeight
|
|
494
|
+
)
|
|
495
|
+
const needed = desiredHeight + menuOffset + 8
|
|
496
|
+
if (placement === 'top') {
|
|
497
|
+
return spaceAbove >= needed || spaceAbove >= spaceBelow
|
|
498
|
+
? 'top'
|
|
499
|
+
: 'bottom'
|
|
500
|
+
}
|
|
501
|
+
if (placement === 'bottom') {
|
|
502
|
+
return spaceBelow >= needed || spaceBelow >= spaceAbove
|
|
503
|
+
? 'bottom'
|
|
504
|
+
: 'top'
|
|
505
|
+
}
|
|
506
|
+
return spaceBelow >= needed || spaceBelow >= spaceAbove
|
|
507
|
+
? 'bottom'
|
|
508
|
+
: 'top'
|
|
509
|
+
}, [
|
|
510
|
+
triggerRect,
|
|
511
|
+
placement,
|
|
512
|
+
windowHeight,
|
|
513
|
+
menuSize?.height,
|
|
514
|
+
menuMaxHeight,
|
|
515
|
+
menuOffset,
|
|
516
|
+
insets.top,
|
|
517
|
+
insets.bottom,
|
|
518
|
+
])
|
|
519
|
+
|
|
520
|
+
const popupStyle = useMemo<ViewStyle>(() => {
|
|
521
|
+
if (!triggerRect) {
|
|
522
|
+
return { position: 'absolute', opacity: 0, top: 0, left: 0 }
|
|
523
|
+
}
|
|
524
|
+
const screenPadding = 8
|
|
525
|
+
const width = matchTriggerWidth ? triggerRect.width : undefined
|
|
526
|
+
const intrinsicWidth = menuSize?.width ?? triggerRect.width
|
|
527
|
+
const finalWidth = width ?? intrinsicWidth
|
|
528
|
+
|
|
529
|
+
let leftPos = triggerRect.x
|
|
530
|
+
const maxLeft =
|
|
531
|
+
windowWidth - insets.right - finalWidth - screenPadding
|
|
532
|
+
const minLeft = insets.left + screenPadding
|
|
533
|
+
if (leftPos > maxLeft) leftPos = maxLeft
|
|
534
|
+
if (leftPos < minLeft) leftPos = minLeft
|
|
535
|
+
|
|
536
|
+
let topPos: number
|
|
537
|
+
if (computedPlacement === 'top') {
|
|
538
|
+
const desiredHeight = menuSize?.height ?? menuMaxHeight
|
|
539
|
+
topPos = triggerRect.y - desiredHeight - menuOffset
|
|
540
|
+
if (topPos < insets.top + screenPadding) {
|
|
541
|
+
topPos = insets.top + screenPadding
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
topPos = triggerRect.y + triggerRect.height + menuOffset
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const style: ViewStyle = {
|
|
548
|
+
position: 'absolute',
|
|
549
|
+
top: topPos,
|
|
550
|
+
left: leftPos,
|
|
551
|
+
}
|
|
552
|
+
if (width != null) style.width = width
|
|
553
|
+
// Hide first frame before measurement to avoid the popup flashing in
|
|
554
|
+
// the wrong place. menuSize becomes truthy after the first layout.
|
|
555
|
+
if (menuSize == null) style.opacity = 0
|
|
556
|
+
return style
|
|
557
|
+
}, [
|
|
558
|
+
triggerRect,
|
|
559
|
+
computedPlacement,
|
|
560
|
+
menuSize,
|
|
561
|
+
menuOffset,
|
|
562
|
+
menuMaxHeight,
|
|
563
|
+
matchTriggerWidth,
|
|
564
|
+
windowWidth,
|
|
565
|
+
insets.top,
|
|
566
|
+
insets.left,
|
|
567
|
+
insets.right,
|
|
568
|
+
])
|
|
569
|
+
|
|
570
|
+
// Reset menu size when closing so the next open re-measures (handles items
|
|
571
|
+
// changing while the menu was closed).
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (!isOpen) setMenuSize(null)
|
|
574
|
+
}, [isOpen])
|
|
575
|
+
|
|
576
|
+
// ---------------- Styles ----------------
|
|
577
|
+
const labelTextStyle: TextStyle = {
|
|
578
|
+
color: tokens.labelColor,
|
|
579
|
+
fontFamily: tokens.labelFontFamily,
|
|
580
|
+
fontSize: tokens.labelFontSize,
|
|
581
|
+
lineHeight: tokens.labelLineHeight,
|
|
582
|
+
fontWeight: tokens.labelFontWeight as TextStyle['fontWeight'],
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const requiredIndicatorStyle: TextStyle = {
|
|
586
|
+
...labelTextStyle,
|
|
587
|
+
color: '#d93d3d',
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const wrapperStyle: ViewStyle = {
|
|
591
|
+
gap: tokens.gap,
|
|
592
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
593
|
+
width: '100%',
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Focus ring uses the resolved input border color from FormField States so
|
|
597
|
+
// active/error look consistent with TextInput-based FormField. We also lift
|
|
598
|
+
// border weight to 2 when "Active" to read as a focus ring.
|
|
599
|
+
const inputRowStyle: ViewStyle = {
|
|
600
|
+
flexDirection: 'row',
|
|
601
|
+
alignItems: 'center',
|
|
602
|
+
backgroundColor: tokens.inputBackground,
|
|
603
|
+
borderColor: tokens.inputBorderColor,
|
|
604
|
+
borderWidth: isOpen ? Math.max(tokens.inputBorderSize, 1) : tokens.inputBorderSize,
|
|
605
|
+
borderRadius: tokens.inputRadius,
|
|
606
|
+
paddingHorizontal: tokens.inputPaddingH,
|
|
607
|
+
paddingVertical: 0,
|
|
608
|
+
gap: tokens.inputGap,
|
|
609
|
+
minHeight: tokens.inputLineHeight,
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const valueTextStyle: TextStyle = {
|
|
613
|
+
flex: 1,
|
|
614
|
+
color: tokens.inputTextColor,
|
|
615
|
+
fontFamily: tokens.inputFontFamily,
|
|
616
|
+
fontSize: tokens.inputFontSize,
|
|
617
|
+
lineHeight: tokens.inputLineHeight,
|
|
618
|
+
fontWeight: tokens.inputFontWeight as TextStyle['fontWeight'],
|
|
619
|
+
paddingVertical: 0,
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const placeholderColor = '#888a8d'
|
|
623
|
+
|
|
624
|
+
// ---------------- Support text ----------------
|
|
625
|
+
const supportStatus: SupportTextStatus = isInvalid ? 'Error' : 'Neutral'
|
|
626
|
+
const supportLabel = isInvalid && errorMessage ? errorMessage : supportText
|
|
627
|
+
|
|
628
|
+
// ---------------- Accessibility ----------------
|
|
629
|
+
const resolvedA11yLabel =
|
|
630
|
+
accessibilityLabel || label || placeholder || 'Dropdown'
|
|
631
|
+
const a11yProps: AccessibilityProps & { [key: string]: any } = {
|
|
632
|
+
accessibilityRole: 'combobox',
|
|
633
|
+
accessibilityLabel: resolvedA11yLabel,
|
|
634
|
+
accessibilityState: {
|
|
635
|
+
disabled: isDisabled,
|
|
636
|
+
expanded: isOpen,
|
|
637
|
+
},
|
|
638
|
+
}
|
|
639
|
+
if (accessibilityHint) a11yProps.accessibilityHint = accessibilityHint
|
|
640
|
+
|
|
641
|
+
// ---------------- Items rendering ----------------
|
|
642
|
+
const renderItems = useCallback(() => {
|
|
643
|
+
const itemNodes: React.ReactNode[] = []
|
|
644
|
+
if (items && items.length > 0) {
|
|
645
|
+
items.forEach((opt) => {
|
|
646
|
+
const isSelected = opt.value === currentValue
|
|
647
|
+
itemNodes.push(
|
|
648
|
+
<DropdownItem
|
|
649
|
+
key={`item-${opt.value}`}
|
|
650
|
+
value={opt.value}
|
|
651
|
+
label={opt.label}
|
|
652
|
+
selected={isSelected}
|
|
653
|
+
disabled={opt.disabled ?? false}
|
|
654
|
+
leading={opt.leading}
|
|
655
|
+
trailing={opt.trailing}
|
|
656
|
+
onPress={handleSelect}
|
|
657
|
+
modes={modes}
|
|
658
|
+
/>
|
|
659
|
+
)
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
if (children) {
|
|
663
|
+
// Inject `selected` and `onPress` into child DropdownItems so the
|
|
664
|
+
// consumer doesn't have to wire selection by hand. Existing
|
|
665
|
+
// `onPress` handlers on a child are preserved and called after our
|
|
666
|
+
// selection logic runs.
|
|
667
|
+
flattenChildren(children).forEach((child, idx) => {
|
|
668
|
+
if (!React.isValidElement(child)) {
|
|
669
|
+
itemNodes.push(child)
|
|
670
|
+
return
|
|
671
|
+
}
|
|
672
|
+
if ((child.type as any) === DropdownItem) {
|
|
673
|
+
const original = child.props as DropdownItemProps
|
|
674
|
+
const isSelected = original.value === currentValue
|
|
675
|
+
const composedOnPress = (
|
|
676
|
+
v: DropdownItemProps['value']
|
|
677
|
+
) => {
|
|
678
|
+
original.onPress?.(v)
|
|
679
|
+
handleSelect(v)
|
|
680
|
+
}
|
|
681
|
+
itemNodes.push(
|
|
682
|
+
React.cloneElement(child, {
|
|
683
|
+
key: child.key ?? `child-${idx}`,
|
|
684
|
+
selected: isSelected,
|
|
685
|
+
onPress: composedOnPress,
|
|
686
|
+
modes: { ...modes, ...(original.modes || {}) },
|
|
687
|
+
} as Partial<DropdownItemProps>)
|
|
688
|
+
)
|
|
689
|
+
} else {
|
|
690
|
+
itemNodes.push(child)
|
|
691
|
+
}
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
return itemNodes
|
|
695
|
+
}, [items, children, currentValue, handleSelect, modes])
|
|
696
|
+
|
|
697
|
+
// ---------------- Render ----------------
|
|
698
|
+
const hasValue = selectedOption != null
|
|
699
|
+
const displayLabel = hasValue ? selectedOption!.label : placeholder
|
|
700
|
+
|
|
701
|
+
return (
|
|
702
|
+
<View style={[wrapperStyle, style]} pointerEvents={isDisabled ? 'none' : 'auto'}>
|
|
703
|
+
{label != null && (
|
|
704
|
+
<View style={styles.labelRow}>
|
|
705
|
+
<Text style={labelTextStyle}>{label}</Text>
|
|
706
|
+
{isRequired && <Text style={requiredIndicatorStyle}> *</Text>}
|
|
707
|
+
</View>
|
|
708
|
+
)}
|
|
709
|
+
|
|
710
|
+
<Pressable
|
|
711
|
+
ref={triggerRef}
|
|
712
|
+
onLayout={handleTriggerLayout}
|
|
713
|
+
onPress={() => {
|
|
714
|
+
if (isDisabled || isReadOnly) return
|
|
715
|
+
measure()
|
|
716
|
+
toggleMenu()
|
|
717
|
+
}}
|
|
718
|
+
{...(onFocus ? { onFocus } : {})}
|
|
719
|
+
{...(onBlur ? { onBlur } : {})}
|
|
720
|
+
style={[inputRowStyle, inputStyle, IS_WEB && webNoOutline]}
|
|
721
|
+
{...a11yProps}
|
|
722
|
+
{...(IS_WEB
|
|
723
|
+
? {
|
|
724
|
+
accessibilityRole: 'combobox' as const,
|
|
725
|
+
'aria-haspopup': 'listbox' as const,
|
|
726
|
+
'aria-expanded': isOpen,
|
|
727
|
+
}
|
|
728
|
+
: {})}
|
|
729
|
+
>
|
|
730
|
+
{renderValue ? (
|
|
731
|
+
<View style={{ flex: 1, justifyContent: 'center' }}>
|
|
732
|
+
{renderValue(selectedOption, hasValue)}
|
|
733
|
+
</View>
|
|
734
|
+
) : (
|
|
735
|
+
<Text
|
|
736
|
+
style={[
|
|
737
|
+
valueTextStyle,
|
|
738
|
+
!hasValue && { color: placeholderColor },
|
|
739
|
+
]}
|
|
740
|
+
numberOfLines={1}
|
|
741
|
+
>
|
|
742
|
+
{displayLabel}
|
|
743
|
+
</Text>
|
|
744
|
+
)}
|
|
745
|
+
<View
|
|
746
|
+
accessibilityElementsHidden
|
|
747
|
+
importantForAccessibility="no"
|
|
748
|
+
pointerEvents="none"
|
|
749
|
+
>
|
|
750
|
+
<Icon
|
|
751
|
+
name={isOpen ? 'ic_chevron_up' : 'ic_chevron_down'}
|
|
752
|
+
size={chevron.iconSize}
|
|
753
|
+
color={chevron.iconColor}
|
|
754
|
+
/>
|
|
755
|
+
</View>
|
|
756
|
+
</Pressable>
|
|
757
|
+
|
|
758
|
+
{supportLabel != null && (
|
|
759
|
+
<SupportText
|
|
760
|
+
label={supportLabel}
|
|
761
|
+
status={supportStatus}
|
|
762
|
+
modes={modes}
|
|
763
|
+
/>
|
|
764
|
+
)}
|
|
765
|
+
|
|
766
|
+
<Modal
|
|
767
|
+
visible={isOpen}
|
|
768
|
+
transparent
|
|
769
|
+
animationType="fade"
|
|
770
|
+
onRequestClose={closeMenu}
|
|
771
|
+
statusBarTranslucent
|
|
772
|
+
>
|
|
773
|
+
<Pressable
|
|
774
|
+
style={StyleSheet.absoluteFill}
|
|
775
|
+
onPress={closeOnBackdropPress ? closeMenu : undefined}
|
|
776
|
+
accessibilityRole="button"
|
|
777
|
+
accessibilityLabel="Close options"
|
|
778
|
+
accessible={false}
|
|
779
|
+
>
|
|
780
|
+
<View
|
|
781
|
+
style={StyleSheet.absoluteFill}
|
|
782
|
+
pointerEvents="box-none"
|
|
783
|
+
>
|
|
784
|
+
<View
|
|
785
|
+
style={popupStyle}
|
|
786
|
+
onLayout={handleMenuLayout}
|
|
787
|
+
pointerEvents="auto"
|
|
788
|
+
>
|
|
789
|
+
<Dropdown
|
|
790
|
+
modes={modes}
|
|
791
|
+
maxHeight={menuMaxHeight}
|
|
792
|
+
style={menuStyle}
|
|
793
|
+
accessibilityLabel={`${resolvedA11yLabel} options`}
|
|
794
|
+
>
|
|
795
|
+
{renderItems()}
|
|
796
|
+
</Dropdown>
|
|
797
|
+
</View>
|
|
798
|
+
</View>
|
|
799
|
+
</Pressable>
|
|
800
|
+
</Modal>
|
|
801
|
+
</View>
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const webNoOutline: any = {
|
|
806
|
+
outlineStyle: 'none',
|
|
807
|
+
outlineWidth: 0,
|
|
808
|
+
outlineColor: 'transparent',
|
|
809
|
+
cursor: 'pointer',
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const styles = StyleSheet.create({
|
|
813
|
+
labelRow: {
|
|
814
|
+
flexDirection: 'row',
|
|
815
|
+
alignItems: 'baseline',
|
|
816
|
+
},
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
export default DropdownInput
|