jfs-components 0.0.73 → 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 +23 -6
- package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
- package/lib/commonjs/components/AppBar/AppBar.js +17 -11
- package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
- 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/LottieIntroBlock/LottieIntroBlock.js +150 -0
- 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/Text/Text.js +9 -2
- package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
- package/lib/commonjs/components/index.js +60 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/AccountCard/AccountCard.js +241 -0
- package/lib/module/components/AppBar/AppBar.js +17 -11
- package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
- package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
- 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/LottieIntroBlock/LottieIntroBlock.js +144 -0
- 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/Text/Text.js +9 -2
- package/lib/module/components/Tooltip/Tooltip.js +34 -27
- package/lib/module/components/index.js +7 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
- package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
- 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/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
- 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/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 +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -3
- package/src/components/AccountCard/AccountCard.tsx +376 -0
- package/src/components/AppBar/AppBar.tsx +25 -14
- package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
- package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
- 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/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
- 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/Text/Text.tsx +24 -3
- package/src/components/Tooltip/Tooltip.tsx +50 -25
- package/src/components/index.ts +15 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
|
@@ -23,9 +23,21 @@ export type CheckboxItemProps = {
|
|
|
23
23
|
/** The label text rendered next to the checkbox. */
|
|
24
24
|
label?: React.ReactNode
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Position of the checkbox control relative to the label.
|
|
27
|
+
* - `'leading'` (default): checkbox is rendered on the left, then label, then optional `endSlot` on the right.
|
|
28
|
+
* - `'trailing'`: optional `endSlot` is rendered on the left, then label, then checkbox on the right.
|
|
29
|
+
*
|
|
30
|
+
* Matches the Figma "Control" variant on the Checkbox Item component.
|
|
31
|
+
*/
|
|
32
|
+
control?: 'leading' | 'trailing'
|
|
33
|
+
/**
|
|
34
|
+
* Optional content rendered in the row's end slot (e.g. a `Button`). When provided,
|
|
27
35
|
* it receives the same `modes` as the parent. Wrapped in a fixed-width container
|
|
28
36
|
* matching the Figma `endSlot` design.
|
|
37
|
+
*
|
|
38
|
+
* Placement depends on `control`:
|
|
39
|
+
* - With `control="leading"` the slot appears on the right edge (after the label).
|
|
40
|
+
* - With `control="trailing"` the slot appears on the left edge (before the label).
|
|
29
41
|
*/
|
|
30
42
|
endSlot?: React.ReactNode
|
|
31
43
|
/** Width of the end slot container in pixels. Defaults to 80 to match the Figma reference. */
|
|
@@ -45,6 +57,9 @@ export type CheckboxItemProps = {
|
|
|
45
57
|
* single horizontal pressable row. Pressing anywhere on the row (outside of the
|
|
46
58
|
* `endSlot`) toggles the checkbox, mirroring the typical native form pattern.
|
|
47
59
|
*
|
|
60
|
+
* Use the `control` prop to swap the checkbox between the leading (left, default)
|
|
61
|
+
* and trailing (right) edge of the row. The `endSlot` flips to the opposite edge.
|
|
62
|
+
*
|
|
48
63
|
* Mirrors the Figma "Checkbox Item" component and uses the `checkboxItem/*`
|
|
49
64
|
* design tokens for typography and spacing.
|
|
50
65
|
*
|
|
@@ -59,6 +74,7 @@ export type CheckboxItemProps = {
|
|
|
59
74
|
* label="Fixed deposit • 0245"
|
|
60
75
|
* checked={checked}
|
|
61
76
|
* onValueChange={setChecked}
|
|
77
|
+
* control="leading"
|
|
62
78
|
* modes={{ 'Color Mode': 'Light' }}
|
|
63
79
|
* />
|
|
64
80
|
* ```
|
|
@@ -69,6 +85,7 @@ function CheckboxItem({
|
|
|
69
85
|
onValueChange,
|
|
70
86
|
disabled = false,
|
|
71
87
|
label = 'Fixed deposit • 0245',
|
|
88
|
+
control = 'leading',
|
|
72
89
|
endSlot,
|
|
73
90
|
endSlotWidth = 80,
|
|
74
91
|
modes = EMPTY_MODES,
|
|
@@ -76,6 +93,7 @@ function CheckboxItem({
|
|
|
76
93
|
labelStyle,
|
|
77
94
|
accessibilityLabel,
|
|
78
95
|
}: CheckboxItemProps) {
|
|
96
|
+
const isTrailing = control === 'trailing'
|
|
79
97
|
const isControlled = controlledChecked !== undefined
|
|
80
98
|
const [internalChecked, setInternalChecked] = useState(defaultChecked)
|
|
81
99
|
const isChecked = isControlled ? controlledChecked : internalChecked
|
|
@@ -129,6 +147,39 @@ function CheckboxItem({
|
|
|
129
147
|
const a11yLabel =
|
|
130
148
|
accessibilityLabel ?? (typeof label === 'string' ? label : undefined)
|
|
131
149
|
|
|
150
|
+
const checkboxNode = (
|
|
151
|
+
<Checkbox
|
|
152
|
+
checked={isChecked}
|
|
153
|
+
disabled={disabled}
|
|
154
|
+
onValueChange={handleToggle}
|
|
155
|
+
modes={modes}
|
|
156
|
+
{...(a11yLabel !== undefined ? { accessibilityLabel: a11yLabel } : {})}
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const labelNode =
|
|
161
|
+
label != null && label !== false ? (
|
|
162
|
+
typeof label === 'string' || typeof label === 'number' ? (
|
|
163
|
+
<Text style={[resolvedLabelStyle, labelStyle]} selectable={false}>
|
|
164
|
+
{label}
|
|
165
|
+
</Text>
|
|
166
|
+
) : (
|
|
167
|
+
<View style={{ flex: 1, minWidth: 0 }}>{label}</View>
|
|
168
|
+
)
|
|
169
|
+
) : null
|
|
170
|
+
|
|
171
|
+
const endSlotNode = endSlot ? (
|
|
172
|
+
<View
|
|
173
|
+
style={{
|
|
174
|
+
width: endSlotWidth,
|
|
175
|
+
flexShrink: 0,
|
|
176
|
+
alignItems: 'stretch',
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
{cloneChildrenWithModes(endSlot, modes)}
|
|
180
|
+
</View>
|
|
181
|
+
) : null
|
|
182
|
+
|
|
132
183
|
return (
|
|
133
184
|
<Pressable
|
|
134
185
|
style={[containerStyle, style]}
|
|
@@ -138,35 +189,19 @@ function CheckboxItem({
|
|
|
138
189
|
accessibilityState={{ checked: isChecked, disabled }}
|
|
139
190
|
accessibilityLabel={a11yLabel}
|
|
140
191
|
>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
) : (
|
|
155
|
-
<View style={{ flex: 1, minWidth: 0 }}>{label}</View>
|
|
156
|
-
)
|
|
157
|
-
) : null}
|
|
158
|
-
|
|
159
|
-
{endSlot ? (
|
|
160
|
-
<View
|
|
161
|
-
style={{
|
|
162
|
-
width: endSlotWidth,
|
|
163
|
-
flexShrink: 0,
|
|
164
|
-
alignItems: 'stretch',
|
|
165
|
-
}}
|
|
166
|
-
>
|
|
167
|
-
{cloneChildrenWithModes(endSlot, modes)}
|
|
168
|
-
</View>
|
|
169
|
-
) : null}
|
|
192
|
+
{isTrailing ? (
|
|
193
|
+
<>
|
|
194
|
+
{endSlotNode}
|
|
195
|
+
{labelNode}
|
|
196
|
+
{checkboxNode}
|
|
197
|
+
</>
|
|
198
|
+
) : (
|
|
199
|
+
<>
|
|
200
|
+
{checkboxNode}
|
|
201
|
+
{labelNode}
|
|
202
|
+
{endSlotNode}
|
|
203
|
+
</>
|
|
204
|
+
)}
|
|
170
205
|
</Pressable>
|
|
171
206
|
)
|
|
172
207
|
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
Pressable,
|
|
5
|
+
ScrollView,
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type AccessibilityProps,
|
|
9
|
+
type PressableStateCallbackType,
|
|
10
|
+
type StyleProp,
|
|
11
|
+
type TextStyle,
|
|
12
|
+
type ViewStyle,
|
|
13
|
+
} from 'react-native'
|
|
14
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
15
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
16
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
17
|
+
import Icon from '../../icons/Icon'
|
|
18
|
+
|
|
19
|
+
const IS_WEB = Platform.OS === 'web'
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// DropdownItem
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export type DropdownItemProps = {
|
|
26
|
+
/** Display text for the item. Ignored when `children` is provided. */
|
|
27
|
+
label?: string
|
|
28
|
+
/**
|
|
29
|
+
* Value associated with the item. Used by the parent `Dropdown` /
|
|
30
|
+
* `DropdownInput` to identify the selected item. Optional when the item
|
|
31
|
+
* is purely informational or used outside of a selection context.
|
|
32
|
+
*/
|
|
33
|
+
value?: string | number | null
|
|
34
|
+
/** Whether the item is currently selected (renders the trailing check). */
|
|
35
|
+
selected?: boolean
|
|
36
|
+
/** Whether the item is disabled and non-interactive. */
|
|
37
|
+
disabled?: boolean
|
|
38
|
+
/** Optional element rendered before the label (icon, avatar, etc.). */
|
|
39
|
+
leading?: React.ReactNode
|
|
40
|
+
/**
|
|
41
|
+
* Optional element rendered after the label. When omitted and `selected`
|
|
42
|
+
* is true, a check icon is rendered automatically.
|
|
43
|
+
*/
|
|
44
|
+
trailing?: React.ReactNode
|
|
45
|
+
/** Press handler. Receives the item's `value` if provided. */
|
|
46
|
+
onPress?: (value: DropdownItemProps['value']) => void
|
|
47
|
+
/** Optional custom child content (overrides `label`). */
|
|
48
|
+
children?: React.ReactNode
|
|
49
|
+
/** Modes for design token resolution. */
|
|
50
|
+
modes?: Record<string, any>
|
|
51
|
+
/** Style overrides for the item container. */
|
|
52
|
+
style?: StyleProp<ViewStyle>
|
|
53
|
+
/** Style overrides for the item label text. */
|
|
54
|
+
labelStyle?: StyleProp<TextStyle>
|
|
55
|
+
/** Accessibility label (defaults to label / inferred from children). */
|
|
56
|
+
accessibilityLabel?: string
|
|
57
|
+
/** Accessibility hint. */
|
|
58
|
+
accessibilityHint?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function useDropdownItemTokens(modes: Record<string, any>) {
|
|
62
|
+
return useMemo(() => {
|
|
63
|
+
// The `dropdownItem/background` token aliases through the
|
|
64
|
+
// `Dropdown Item State` collection (Idle | Selected), so we resolve
|
|
65
|
+
// both possibilities up-front and pick at render time.
|
|
66
|
+
const idleBackground =
|
|
67
|
+
(getVariableByName('dropdownItem/background', {
|
|
68
|
+
...modes,
|
|
69
|
+
'Dropdown Item State': 'Idle',
|
|
70
|
+
}) as string) || '#ffffff'
|
|
71
|
+
const selectedBackground =
|
|
72
|
+
(getVariableByName('dropdownItem/background', {
|
|
73
|
+
...modes,
|
|
74
|
+
'Dropdown Item State': 'Selected',
|
|
75
|
+
}) as string) || '#f5f5f5'
|
|
76
|
+
const foreground =
|
|
77
|
+
(getVariableByName('dropdownItem/foreground', modes) as string) ||
|
|
78
|
+
'#000000'
|
|
79
|
+
const fontFamily =
|
|
80
|
+
(getVariableByName('dropdownItem/fontFamily', modes) as string) ||
|
|
81
|
+
'JioType Var'
|
|
82
|
+
const fontSize =
|
|
83
|
+
parseInt(getVariableByName('dropdownItem/fontSize', modes), 10) || 16
|
|
84
|
+
const fontWeight =
|
|
85
|
+
(getVariableByName('dropdownItem/fontWeight', modes) as string) ||
|
|
86
|
+
'400'
|
|
87
|
+
const lineHeight =
|
|
88
|
+
parseInt(getVariableByName('dropdownItem/lineHeight', modes), 10) || 19
|
|
89
|
+
const gap = parseInt(getVariableByName('dropdownItem/gap', modes), 10) || 8
|
|
90
|
+
const paddingHorizontal =
|
|
91
|
+
parseInt(
|
|
92
|
+
getVariableByName('dropdownItem/padding/horizontal', modes),
|
|
93
|
+
10
|
|
94
|
+
) || 12
|
|
95
|
+
const paddingVertical =
|
|
96
|
+
parseInt(
|
|
97
|
+
getVariableByName('dropdownItem/padding/vertical', modes),
|
|
98
|
+
10
|
|
99
|
+
) || 12
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
idleBackground,
|
|
103
|
+
selectedBackground,
|
|
104
|
+
foreground,
|
|
105
|
+
fontFamily,
|
|
106
|
+
fontSize,
|
|
107
|
+
fontWeight,
|
|
108
|
+
lineHeight,
|
|
109
|
+
gap,
|
|
110
|
+
paddingHorizontal,
|
|
111
|
+
paddingVertical,
|
|
112
|
+
}
|
|
113
|
+
}, [modes])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function DropdownItem({
|
|
117
|
+
label,
|
|
118
|
+
value = null,
|
|
119
|
+
selected = false,
|
|
120
|
+
disabled = false,
|
|
121
|
+
leading,
|
|
122
|
+
trailing,
|
|
123
|
+
onPress,
|
|
124
|
+
children,
|
|
125
|
+
modes: propModes = EMPTY_MODES,
|
|
126
|
+
style,
|
|
127
|
+
labelStyle,
|
|
128
|
+
accessibilityLabel,
|
|
129
|
+
accessibilityHint,
|
|
130
|
+
}: DropdownItemProps) {
|
|
131
|
+
const { modes: globalModes } = useTokens()
|
|
132
|
+
const modes = useMemo(
|
|
133
|
+
() => ({ ...globalModes, ...propModes }),
|
|
134
|
+
[globalModes, propModes]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const tokens = useDropdownItemTokens(modes)
|
|
138
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
139
|
+
|
|
140
|
+
const handlePress = useCallback(() => {
|
|
141
|
+
if (disabled) return
|
|
142
|
+
onPress?.(value)
|
|
143
|
+
}, [disabled, onPress, value])
|
|
144
|
+
|
|
145
|
+
const containerStyle = useCallback(
|
|
146
|
+
({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => {
|
|
147
|
+
const showSelected =
|
|
148
|
+
pressed || (isHovered && IS_WEB) || selected
|
|
149
|
+
const base: ViewStyle = {
|
|
150
|
+
flexDirection: 'row',
|
|
151
|
+
alignItems: 'center',
|
|
152
|
+
gap: tokens.gap,
|
|
153
|
+
paddingHorizontal: tokens.paddingHorizontal,
|
|
154
|
+
paddingVertical: tokens.paddingVertical,
|
|
155
|
+
backgroundColor: showSelected
|
|
156
|
+
? tokens.selectedBackground
|
|
157
|
+
: tokens.idleBackground,
|
|
158
|
+
opacity: disabled ? 0.4 : 1,
|
|
159
|
+
width: '100%',
|
|
160
|
+
}
|
|
161
|
+
return [base, style]
|
|
162
|
+
},
|
|
163
|
+
[
|
|
164
|
+
tokens.gap,
|
|
165
|
+
tokens.paddingHorizontal,
|
|
166
|
+
tokens.paddingVertical,
|
|
167
|
+
tokens.idleBackground,
|
|
168
|
+
tokens.selectedBackground,
|
|
169
|
+
isHovered,
|
|
170
|
+
selected,
|
|
171
|
+
disabled,
|
|
172
|
+
style,
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const textStyle: TextStyle = {
|
|
177
|
+
color: tokens.foreground,
|
|
178
|
+
fontFamily: tokens.fontFamily,
|
|
179
|
+
fontSize: tokens.fontSize,
|
|
180
|
+
fontWeight: tokens.fontWeight as TextStyle['fontWeight'],
|
|
181
|
+
lineHeight: tokens.lineHeight,
|
|
182
|
+
flexShrink: 1,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const processedLeading = leading
|
|
186
|
+
? cloneChildrenWithModes(React.Children.toArray(leading), modes)
|
|
187
|
+
: null
|
|
188
|
+
|
|
189
|
+
const customTrailing = trailing
|
|
190
|
+
? cloneChildrenWithModes(React.Children.toArray(trailing), modes)
|
|
191
|
+
: null
|
|
192
|
+
|
|
193
|
+
const showDefaultCheck = !trailing && selected
|
|
194
|
+
|
|
195
|
+
const fallbackA11yLabel =
|
|
196
|
+
accessibilityLabel ||
|
|
197
|
+
(typeof label === 'string' ? label : 'Dropdown item')
|
|
198
|
+
|
|
199
|
+
const a11yProps: AccessibilityProps = {
|
|
200
|
+
accessibilityRole: 'menuitem',
|
|
201
|
+
accessibilityLabel: fallbackA11yLabel,
|
|
202
|
+
accessibilityState: { selected, disabled },
|
|
203
|
+
}
|
|
204
|
+
if (accessibilityHint) {
|
|
205
|
+
a11yProps.accessibilityHint = accessibilityHint
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const handleHoverIn = useCallback(() => {
|
|
209
|
+
if (IS_WEB && !disabled) setIsHovered(true)
|
|
210
|
+
}, [disabled])
|
|
211
|
+
const handleHoverOut = useCallback(() => {
|
|
212
|
+
if (IS_WEB) setIsHovered(false)
|
|
213
|
+
}, [])
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Pressable
|
|
217
|
+
onPress={handlePress}
|
|
218
|
+
disabled={disabled}
|
|
219
|
+
onHoverIn={handleHoverIn}
|
|
220
|
+
onHoverOut={handleHoverOut}
|
|
221
|
+
style={containerStyle}
|
|
222
|
+
{...a11yProps}
|
|
223
|
+
>
|
|
224
|
+
{processedLeading}
|
|
225
|
+
{children != null ? (
|
|
226
|
+
children
|
|
227
|
+
) : (
|
|
228
|
+
<Text style={[textStyle, labelStyle]} numberOfLines={1}>
|
|
229
|
+
{label}
|
|
230
|
+
</Text>
|
|
231
|
+
)}
|
|
232
|
+
{customTrailing}
|
|
233
|
+
{showDefaultCheck && (
|
|
234
|
+
<Icon name="ic_confirm" size={16} color={tokens.foreground} />
|
|
235
|
+
)}
|
|
236
|
+
</Pressable>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Dropdown (popup surface)
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
export type DropdownProps = {
|
|
245
|
+
/** Dropdown items, typically `<DropdownItem />` children. */
|
|
246
|
+
children?: React.ReactNode
|
|
247
|
+
/**
|
|
248
|
+
* Maximum height of the popup before its content becomes scrollable.
|
|
249
|
+
* Defaults to no limit.
|
|
250
|
+
*/
|
|
251
|
+
maxHeight?: number
|
|
252
|
+
/** Modes for design token resolution. */
|
|
253
|
+
modes?: Record<string, any>
|
|
254
|
+
/** Style overrides for the popup container. */
|
|
255
|
+
style?: StyleProp<ViewStyle>
|
|
256
|
+
/** Accessibility label for the menu surface. */
|
|
257
|
+
accessibilityLabel?: string
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* `Dropdown` is the visual surface (popup) that contains a list of
|
|
262
|
+
* `DropdownItem`s. It is responsible for the background, rounded corners,
|
|
263
|
+
* elevation/shadow, and clipping. Use it standalone for menu UIs, or rely on
|
|
264
|
+
* `DropdownInput` which composes it into a form-field experience.
|
|
265
|
+
*/
|
|
266
|
+
export function Dropdown({
|
|
267
|
+
children,
|
|
268
|
+
maxHeight,
|
|
269
|
+
modes: propModes = EMPTY_MODES,
|
|
270
|
+
style,
|
|
271
|
+
accessibilityLabel,
|
|
272
|
+
}: DropdownProps) {
|
|
273
|
+
const { modes: globalModes } = useTokens()
|
|
274
|
+
const modes = useMemo(
|
|
275
|
+
() => ({ ...globalModes, ...propModes }),
|
|
276
|
+
[globalModes, propModes]
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
const radius =
|
|
280
|
+
parseInt(getVariableByName('dropdown/radius', modes), 10) || 8
|
|
281
|
+
const background =
|
|
282
|
+
(getVariableByName('dropdown/background', modes) as string) || '#ffffff'
|
|
283
|
+
const shadowColor =
|
|
284
|
+
(getVariableByName('dropdown/shadow/color', modes) as string) ||
|
|
285
|
+
'rgba(0, 0, 0, 0.08)'
|
|
286
|
+
const shadowOffsetX =
|
|
287
|
+
parseInt(getVariableByName('dropdown/shadow/offsetX', modes), 10) || 0
|
|
288
|
+
const shadowOffsetY =
|
|
289
|
+
parseInt(getVariableByName('dropdown/shadow/offsetY', modes), 10) || 4
|
|
290
|
+
const shadowBlur =
|
|
291
|
+
parseInt(getVariableByName('dropdown/shadow/blur', modes), 10) || 16
|
|
292
|
+
|
|
293
|
+
const containerStyle: ViewStyle = {
|
|
294
|
+
backgroundColor: background,
|
|
295
|
+
borderRadius: radius,
|
|
296
|
+
overflow: 'hidden',
|
|
297
|
+
shadowColor,
|
|
298
|
+
shadowOffset: { width: shadowOffsetX, height: shadowOffsetY },
|
|
299
|
+
shadowOpacity: 1,
|
|
300
|
+
shadowRadius: shadowBlur / 2,
|
|
301
|
+
elevation: 4,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const content = (
|
|
305
|
+
<View style={{ flexDirection: 'column' }}>
|
|
306
|
+
{cloneChildrenWithModes(children, modes)}
|
|
307
|
+
</View>
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<View
|
|
312
|
+
style={[containerStyle, style]}
|
|
313
|
+
accessibilityRole="menu"
|
|
314
|
+
accessibilityLabel={accessibilityLabel || 'Dropdown menu'}
|
|
315
|
+
>
|
|
316
|
+
{maxHeight != null ? (
|
|
317
|
+
<ScrollView
|
|
318
|
+
style={{ maxHeight }}
|
|
319
|
+
showsVerticalScrollIndicator={true}
|
|
320
|
+
keyboardShouldPersistTaps="handled"
|
|
321
|
+
>
|
|
322
|
+
{content}
|
|
323
|
+
</ScrollView>
|
|
324
|
+
) : (
|
|
325
|
+
content
|
|
326
|
+
)}
|
|
327
|
+
</View>
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export default Dropdown
|