jfs-components 0.0.74 → 0.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +193 -82
- package/lib/commonjs/components/Avatar/Avatar.js +20 -0
- package/lib/commonjs/components/Badge/Badge.js +23 -0
- package/lib/commonjs/components/Button/Button.js +37 -0
- package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
- package/lib/commonjs/components/FormField/FormField.js +14 -1
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
- package/lib/commonjs/components/IconButton/IconButton.js +20 -0
- package/lib/commonjs/components/Image/Image.js +26 -1
- package/lib/commonjs/components/ListItem/ListItem.js +25 -10
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
- package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
- package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/PageHero/PageHero.js +41 -5
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
- package/lib/commonjs/components/Stepper/Step.js +47 -60
- package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
- package/lib/commonjs/components/Stepper/Stepper.js +15 -17
- package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
- package/lib/commonjs/components/Text/Text.js +31 -1
- package/lib/commonjs/components/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/Icon.js +16 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/skeleton/Skeleton.js +234 -0
- package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
- package/lib/commonjs/skeleton/index.js +58 -0
- package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
- package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +193 -83
- package/lib/module/components/Avatar/Avatar.js +19 -0
- package/lib/module/components/Badge/Badge.js +23 -0
- package/lib/module/components/Button/Button.js +37 -0
- package/lib/module/components/Checkbox/Checkbox.js +22 -10
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
- package/lib/module/components/FormField/FormField.js +16 -3
- package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
- package/lib/module/components/IconButton/IconButton.js +20 -0
- package/lib/module/components/Image/Image.js +25 -1
- package/lib/module/components/ListItem/ListItem.js +25 -10
- package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
- package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
- package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/PageHero/PageHero.js +41 -5
- package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
- package/lib/module/components/Stepper/Step.js +48 -61
- package/lib/module/components/Stepper/StepLabel.js +40 -10
- package/lib/module/components/Stepper/Stepper.js +15 -17
- package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
- package/lib/module/components/Text/Text.js +31 -1
- package/lib/module/components/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +5 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/Icon.js +16 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/skeleton/Skeleton.js +229 -0
- package/lib/module/skeleton/SkeletonGroup.js +133 -0
- package/lib/module/skeleton/index.js +6 -0
- package/lib/module/skeleton/shimmer-tokens.js +181 -0
- package/lib/module/skeleton/useReducedMotion.js +61 -0
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
- package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
- package/lib/typescript/src/components/Button/Button.d.ts +8 -1
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
- package/lib/typescript/src/components/Image/Image.d.ts +8 -1
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
- package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
- package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
- package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
- package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +31 -5
- package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
- package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
- package/lib/typescript/src/components/Text/Text.d.ts +20 -1
- package/lib/typescript/src/components/index.d.ts +8 -3
- package/lib/typescript/src/icons/Icon.d.ts +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
- package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
- package/lib/typescript/src/skeleton/index.d.ts +5 -0
- package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
- package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
- package/package.json +11 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +210 -92
- package/src/components/Avatar/Avatar.tsx +26 -0
- package/src/components/Badge/Badge.tsx +27 -0
- package/src/components/Button/Button.tsx +40 -0
- package/src/components/Checkbox/Checkbox.tsx +22 -9
- package/src/components/DropdownInput/DropdownInput.tsx +67 -39
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
- package/src/components/FormField/FormField.tsx +19 -3
- package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
- package/src/components/IconButton/IconButton.tsx +27 -0
- package/src/components/Image/Image.tsx +25 -0
- package/src/components/ListItem/ListItem.tsx +21 -10
- package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
- package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
- package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
- package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/PageHero/PageHero.tsx +61 -4
- package/src/components/RechargeCard/RechargeCard.tsx +32 -24
- package/src/components/Stepper/Step.tsx +52 -51
- package/src/components/Stepper/StepLabel.tsx +46 -9
- package/src/components/Stepper/Stepper.tsx +20 -15
- package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
- package/src/components/Text/Text.tsx +54 -0
- package/src/components/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +8 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/Icon.tsx +17 -0
- package/src/icons/registry.ts +1 -1
- package/src/index.ts +1 -0
- package/src/skeleton/Skeleton.tsx +298 -0
- package/src/skeleton/SkeletonGroup.tsx +193 -0
- package/src/skeleton/index.ts +10 -0
- package/src/skeleton/shimmer-tokens.ts +221 -0
- package/src/skeleton/useReducedMotion.ts +72 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react'
|
|
8
|
+
import {
|
|
9
|
+
Platform,
|
|
10
|
+
Pressable,
|
|
11
|
+
Text,
|
|
12
|
+
TextInput as RNTextInput,
|
|
13
|
+
View,
|
|
14
|
+
type AccessibilityProps,
|
|
15
|
+
type StyleProp,
|
|
16
|
+
type TextInputProps as RNTextInputProps,
|
|
17
|
+
type TextStyle,
|
|
18
|
+
type ViewStyle,
|
|
19
|
+
} from 'react-native'
|
|
20
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
21
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
22
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
23
|
+
import SupportText from '../SupportText/SupportText'
|
|
24
|
+
import type { SupportTextStatus } from '../SupportText/SupportTextIcon'
|
|
25
|
+
import Dropdown, { DropdownItem } from '../Dropdown/Dropdown'
|
|
26
|
+
|
|
27
|
+
const IS_WEB = Platform.OS === 'web'
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Types
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export type SuggestiveSearchOptionValue = string | number
|
|
34
|
+
|
|
35
|
+
export type SuggestiveSearchOption = {
|
|
36
|
+
/** Stable, unique value used to identify the suggestion. */
|
|
37
|
+
value: SuggestiveSearchOptionValue
|
|
38
|
+
/** Human-readable label shown in the suggestion list and the input. */
|
|
39
|
+
label: string
|
|
40
|
+
/** Whether the suggestion is non-selectable. */
|
|
41
|
+
disabled?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Suggestions accept either a bare string (used as both value and label) or a
|
|
46
|
+
* full `{ value, label }` option object for richer data.
|
|
47
|
+
*/
|
|
48
|
+
export type SuggestiveSearchItem = string | SuggestiveSearchOption
|
|
49
|
+
|
|
50
|
+
export type SuggestiveSearchProps = {
|
|
51
|
+
/** Label rendered above the input. */
|
|
52
|
+
label?: string
|
|
53
|
+
/** Placeholder text shown when the query is empty. */
|
|
54
|
+
placeholder?: string
|
|
55
|
+
/**
|
|
56
|
+
* Suggestions to filter against the current query. May be bare strings or
|
|
57
|
+
* `{ value, label }` objects.
|
|
58
|
+
*/
|
|
59
|
+
items?: SuggestiveSearchItem[]
|
|
60
|
+
/**
|
|
61
|
+
* Current query text (controlled). When `undefined` the component manages
|
|
62
|
+
* its own query state internally.
|
|
63
|
+
*/
|
|
64
|
+
inputValue?: string
|
|
65
|
+
/** Initial query text for uncontrolled mode. */
|
|
66
|
+
defaultInputValue?: string
|
|
67
|
+
/** Called whenever the query text changes (typing or selection). */
|
|
68
|
+
onInputChange?: (text: string) => void
|
|
69
|
+
/**
|
|
70
|
+
* Currently selected suggestion value (controlled). When `undefined` the
|
|
71
|
+
* component tracks the selection internally.
|
|
72
|
+
*/
|
|
73
|
+
value?: SuggestiveSearchOptionValue | null
|
|
74
|
+
/** Initial selected value for uncontrolled mode. */
|
|
75
|
+
defaultValue?: SuggestiveSearchOptionValue | null
|
|
76
|
+
/** Called when a suggestion is chosen. */
|
|
77
|
+
onValueChange?: (
|
|
78
|
+
value: SuggestiveSearchOptionValue | null,
|
|
79
|
+
option?: SuggestiveSearchOption
|
|
80
|
+
) => void
|
|
81
|
+
/**
|
|
82
|
+
* Custom predicate deciding whether an option matches the current query.
|
|
83
|
+
* Defaults to a case-insensitive substring match on the label.
|
|
84
|
+
*/
|
|
85
|
+
filter?: (query: string, option: SuggestiveSearchOption) => boolean
|
|
86
|
+
/**
|
|
87
|
+
* Minimum number of characters required before suggestions are shown.
|
|
88
|
+
* @default 1
|
|
89
|
+
*/
|
|
90
|
+
minChars?: number
|
|
91
|
+
/** Caps the number of suggestions rendered. Defaults to no limit. */
|
|
92
|
+
maxResults?: number
|
|
93
|
+
/**
|
|
94
|
+
* Highlights the matched substring of each suggestion in bold.
|
|
95
|
+
* @default true
|
|
96
|
+
*/
|
|
97
|
+
highlightMatch?: boolean
|
|
98
|
+
/**
|
|
99
|
+
* Message shown when the query has matched no suggestions. When omitted,
|
|
100
|
+
* the dropdown simply stays hidden on an empty result set.
|
|
101
|
+
*/
|
|
102
|
+
emptyMessage?: string
|
|
103
|
+
/** Custom renderer for a suggestion row (overrides the default label). */
|
|
104
|
+
renderItem?: (
|
|
105
|
+
option: SuggestiveSearchOption,
|
|
106
|
+
meta: { query: string; isSelected: boolean }
|
|
107
|
+
) => React.ReactNode
|
|
108
|
+
/** Controlled open state of the suggestion dropdown. */
|
|
109
|
+
open?: boolean
|
|
110
|
+
/** Initial open state for uncontrolled mode. */
|
|
111
|
+
defaultOpen?: boolean
|
|
112
|
+
/** Called whenever the open state changes. */
|
|
113
|
+
onOpenChange?: (open: boolean) => void
|
|
114
|
+
/**
|
|
115
|
+
* Maximum height of the suggestion list before it becomes scrollable.
|
|
116
|
+
* @default 240
|
|
117
|
+
*/
|
|
118
|
+
menuMaxHeight?: number
|
|
119
|
+
/**
|
|
120
|
+
* Vertical gap between the input and the suggestion dropdown.
|
|
121
|
+
* @default 6
|
|
122
|
+
*/
|
|
123
|
+
menuOffset?: number
|
|
124
|
+
/** Renders a required asterisk next to the label. */
|
|
125
|
+
isRequired?: boolean
|
|
126
|
+
/** Disables interaction and dims the field. */
|
|
127
|
+
isDisabled?: boolean
|
|
128
|
+
/** Marks the field as invalid and shows `errorMessage`. */
|
|
129
|
+
isInvalid?: boolean
|
|
130
|
+
/** Renders the field as read-only (non-interactive, not dimmed). */
|
|
131
|
+
isReadOnly?: boolean
|
|
132
|
+
/** Helper text displayed below the input. */
|
|
133
|
+
supportText?: string
|
|
134
|
+
/** Replaces `supportText` when `isInvalid` is true. */
|
|
135
|
+
errorMessage?: string
|
|
136
|
+
/** Modes for design token resolution. */
|
|
137
|
+
modes?: Record<string, any>
|
|
138
|
+
/** Style overrides for the outermost wrapper. */
|
|
139
|
+
style?: StyleProp<ViewStyle>
|
|
140
|
+
/** Style overrides for the input row. */
|
|
141
|
+
inputStyle?: StyleProp<ViewStyle>
|
|
142
|
+
/** Style overrides for the input text. */
|
|
143
|
+
inputTextStyle?: StyleProp<TextStyle>
|
|
144
|
+
/** Style overrides for the suggestion dropdown container. */
|
|
145
|
+
menuStyle?: StyleProp<ViewStyle>
|
|
146
|
+
/** Accessibility label. Defaults to the visible label / placeholder. */
|
|
147
|
+
accessibilityLabel?: string
|
|
148
|
+
/** Accessibility hint. */
|
|
149
|
+
accessibilityHint?: string
|
|
150
|
+
/** Called when the input receives focus. */
|
|
151
|
+
onFocus?: RNTextInputProps['onFocus']
|
|
152
|
+
/** Called when the input loses focus. */
|
|
153
|
+
onBlur?: RNTextInputProps['onBlur']
|
|
154
|
+
/** Test identifier. */
|
|
155
|
+
testID?: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Helpers
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
function toNumber(value: unknown, fallback: number): number {
|
|
163
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
164
|
+
if (typeof value === 'string') {
|
|
165
|
+
const parsed = parseFloat(value)
|
|
166
|
+
if (Number.isFinite(parsed)) return parsed
|
|
167
|
+
}
|
|
168
|
+
return fallback
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeItem(item: SuggestiveSearchItem): SuggestiveSearchOption {
|
|
172
|
+
if (typeof item === 'string') return { value: item, label: item }
|
|
173
|
+
return item
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const defaultFilter = (query: string, option: SuggestiveSearchOption) =>
|
|
177
|
+
option.label.toLowerCase().includes(query.toLowerCase())
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Token resolution
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
function useFormFieldTokens(modes: Record<string, any>) {
|
|
184
|
+
return useMemo(() => {
|
|
185
|
+
const labelColor =
|
|
186
|
+
(getVariableByName('formField/label/color', modes) as string) ||
|
|
187
|
+
'#000000'
|
|
188
|
+
const labelFontFamily =
|
|
189
|
+
(getVariableByName('formField/label/fontFamily', modes) as string) ||
|
|
190
|
+
'JioType Var'
|
|
191
|
+
const labelFontSize = toNumber(
|
|
192
|
+
getVariableByName('formField/label/fontSize', modes),
|
|
193
|
+
14
|
|
194
|
+
)
|
|
195
|
+
const labelLineHeight = toNumber(
|
|
196
|
+
getVariableByName('formField/label/lineHeight', modes),
|
|
197
|
+
17
|
|
198
|
+
)
|
|
199
|
+
const labelFontWeight =
|
|
200
|
+
(getVariableByName('formField/label/fontWeight', modes) as string) ||
|
|
201
|
+
'500'
|
|
202
|
+
|
|
203
|
+
const gap = toNumber(getVariableByName('formField/gap', modes), 8)
|
|
204
|
+
|
|
205
|
+
const inputPaddingH = toNumber(
|
|
206
|
+
getVariableByName('formField/input/padding/horizontal', modes),
|
|
207
|
+
12
|
|
208
|
+
)
|
|
209
|
+
const inputGap = toNumber(getVariableByName('formField/input/gap', modes), 8)
|
|
210
|
+
const inputRadius = toNumber(
|
|
211
|
+
getVariableByName('formField/input/radius', modes),
|
|
212
|
+
8
|
|
213
|
+
)
|
|
214
|
+
const inputBorderSize = toNumber(
|
|
215
|
+
getVariableByName('formField/input/border/size', modes),
|
|
216
|
+
1.5
|
|
217
|
+
)
|
|
218
|
+
const inputBackground =
|
|
219
|
+
(getVariableByName('formField/input/background', modes) as string) ||
|
|
220
|
+
'#ffffff'
|
|
221
|
+
const inputBorderColor =
|
|
222
|
+
(getVariableByName('formField/input/border/color', modes) as string) ||
|
|
223
|
+
'#b5b6b7'
|
|
224
|
+
const inputFontSize = toNumber(
|
|
225
|
+
getVariableByName('formField/input/label/fontSize', modes),
|
|
226
|
+
16
|
|
227
|
+
)
|
|
228
|
+
const inputLineHeight = toNumber(
|
|
229
|
+
getVariableByName('formField/input/label/lineHeight', modes),
|
|
230
|
+
45
|
|
231
|
+
)
|
|
232
|
+
const inputFontFamily =
|
|
233
|
+
(getVariableByName('formField/input/label/fontFamily', modes) as string) ||
|
|
234
|
+
'JioType Var'
|
|
235
|
+
const inputFontWeight =
|
|
236
|
+
(getVariableByName('formField/input/label/fontWeight', modes) as string) ||
|
|
237
|
+
'400'
|
|
238
|
+
const inputTextColor =
|
|
239
|
+
(getVariableByName('formField/input/label/color', modes) as string) ||
|
|
240
|
+
'#24262b'
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
labelColor,
|
|
244
|
+
labelFontFamily,
|
|
245
|
+
labelFontSize,
|
|
246
|
+
labelLineHeight,
|
|
247
|
+
labelFontWeight,
|
|
248
|
+
gap,
|
|
249
|
+
inputPaddingH,
|
|
250
|
+
inputGap,
|
|
251
|
+
inputRadius,
|
|
252
|
+
inputBorderSize,
|
|
253
|
+
inputBackground,
|
|
254
|
+
inputBorderColor,
|
|
255
|
+
inputFontSize,
|
|
256
|
+
inputLineHeight,
|
|
257
|
+
inputFontFamily,
|
|
258
|
+
inputFontWeight,
|
|
259
|
+
inputTextColor,
|
|
260
|
+
}
|
|
261
|
+
}, [modes])
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function useDropdownItemTextTokens(modes: Record<string, any>) {
|
|
265
|
+
return useMemo(() => {
|
|
266
|
+
const foreground =
|
|
267
|
+
(getVariableByName('dropdownItem/foreground', modes) as string) ||
|
|
268
|
+
'#000000'
|
|
269
|
+
const fontFamily =
|
|
270
|
+
(getVariableByName('dropdownItem/fontFamily', modes) as string) ||
|
|
271
|
+
'JioType Var'
|
|
272
|
+
const fontSize = toNumber(
|
|
273
|
+
getVariableByName('dropdownItem/fontSize', modes),
|
|
274
|
+
16
|
|
275
|
+
)
|
|
276
|
+
const lineHeight = toNumber(
|
|
277
|
+
getVariableByName('dropdownItem/lineHeight', modes),
|
|
278
|
+
19
|
|
279
|
+
)
|
|
280
|
+
return { foreground, fontFamily, fontSize, lineHeight }
|
|
281
|
+
}, [modes])
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Component
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
function SuggestiveSearch({
|
|
289
|
+
label,
|
|
290
|
+
placeholder = 'Search',
|
|
291
|
+
items,
|
|
292
|
+
inputValue,
|
|
293
|
+
defaultInputValue = '',
|
|
294
|
+
onInputChange,
|
|
295
|
+
value,
|
|
296
|
+
defaultValue = null,
|
|
297
|
+
onValueChange,
|
|
298
|
+
filter = defaultFilter,
|
|
299
|
+
minChars = 1,
|
|
300
|
+
maxResults,
|
|
301
|
+
highlightMatch = true,
|
|
302
|
+
emptyMessage,
|
|
303
|
+
renderItem,
|
|
304
|
+
open,
|
|
305
|
+
defaultOpen = false,
|
|
306
|
+
onOpenChange,
|
|
307
|
+
menuMaxHeight = 240,
|
|
308
|
+
menuOffset = 6,
|
|
309
|
+
isRequired = false,
|
|
310
|
+
isDisabled = false,
|
|
311
|
+
isInvalid = false,
|
|
312
|
+
isReadOnly = false,
|
|
313
|
+
supportText,
|
|
314
|
+
errorMessage,
|
|
315
|
+
modes: propModes = EMPTY_MODES,
|
|
316
|
+
style,
|
|
317
|
+
inputStyle,
|
|
318
|
+
inputTextStyle,
|
|
319
|
+
menuStyle,
|
|
320
|
+
accessibilityLabel,
|
|
321
|
+
accessibilityHint,
|
|
322
|
+
onFocus,
|
|
323
|
+
onBlur,
|
|
324
|
+
testID,
|
|
325
|
+
}: SuggestiveSearchProps) {
|
|
326
|
+
// ---------------- Modes ----------------
|
|
327
|
+
const { modes: globalModes } = useTokens()
|
|
328
|
+
const baseModes = useMemo(
|
|
329
|
+
() => ({ ...globalModes, ...propModes }),
|
|
330
|
+
[globalModes, propModes]
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
const interactive = !isDisabled && !isReadOnly
|
|
334
|
+
|
|
335
|
+
// ---------------- Query state ----------------
|
|
336
|
+
const isControlledInput = inputValue !== undefined
|
|
337
|
+
const [internalInput, setInternalInput] = useState(defaultInputValue)
|
|
338
|
+
const query = isControlledInput ? (inputValue as string) : internalInput
|
|
339
|
+
|
|
340
|
+
// ---------------- Selected value state ----------------
|
|
341
|
+
const isControlledValue = value !== undefined
|
|
342
|
+
const [internalValue, setInternalValue] = useState<
|
|
343
|
+
SuggestiveSearchOptionValue | null
|
|
344
|
+
>(defaultValue)
|
|
345
|
+
const currentValue = isControlledValue
|
|
346
|
+
? (value as SuggestiveSearchOptionValue | null)
|
|
347
|
+
: internalValue
|
|
348
|
+
|
|
349
|
+
// ---------------- Open state ----------------
|
|
350
|
+
const isControlledOpen = open !== undefined
|
|
351
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen)
|
|
352
|
+
const [isFocused, setIsFocused] = useState(false)
|
|
353
|
+
|
|
354
|
+
const setOpenState = useCallback(
|
|
355
|
+
(next: boolean) => {
|
|
356
|
+
if (!isControlledOpen) setInternalOpen(next)
|
|
357
|
+
onOpenChange?.(next)
|
|
358
|
+
},
|
|
359
|
+
[isControlledOpen, onOpenChange]
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
// ---------------- Suggestions ----------------
|
|
363
|
+
const normalizedItems = useMemo(
|
|
364
|
+
() => (items ?? []).map(normalizeItem),
|
|
365
|
+
[items]
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const suggestions = useMemo(() => {
|
|
369
|
+
const trimmed = query.trim()
|
|
370
|
+
if (trimmed.length < minChars) return []
|
|
371
|
+
const matched = normalizedItems.filter((opt) => filter(query, opt))
|
|
372
|
+
return maxResults != null ? matched.slice(0, maxResults) : matched
|
|
373
|
+
}, [normalizedItems, query, minChars, filter, maxResults])
|
|
374
|
+
|
|
375
|
+
const hasSuggestions = suggestions.length > 0
|
|
376
|
+
const showEmpty = Boolean(
|
|
377
|
+
emptyMessage && query.trim().length >= minChars && !hasSuggestions
|
|
378
|
+
)
|
|
379
|
+
const hasMenuContent = hasSuggestions || showEmpty
|
|
380
|
+
|
|
381
|
+
// Resolved open state: an explicit `open` prop wins; otherwise the dropdown
|
|
382
|
+
// tracks the internal "wants suggestions" flag (set on focus / typing,
|
|
383
|
+
// cleared on blur / select). Blur and outside-press handle dismissal, so we
|
|
384
|
+
// intentionally do NOT gate on `isFocused` here — that would suppress
|
|
385
|
+
// `defaultOpen` on mount.
|
|
386
|
+
const isOpen =
|
|
387
|
+
interactive &&
|
|
388
|
+
(isControlledOpen ? Boolean(open) : internalOpen) &&
|
|
389
|
+
hasMenuContent
|
|
390
|
+
|
|
391
|
+
// ---------------- Token modes (state cascade) ----------------
|
|
392
|
+
const modes = useMemo(
|
|
393
|
+
() => ({
|
|
394
|
+
...baseModes,
|
|
395
|
+
'FormField States': isInvalid
|
|
396
|
+
? 'Error'
|
|
397
|
+
: isReadOnly || isDisabled
|
|
398
|
+
? 'Read Only'
|
|
399
|
+
: isFocused
|
|
400
|
+
? 'Active'
|
|
401
|
+
: (baseModes['FormField States'] as string) || 'Idle',
|
|
402
|
+
}),
|
|
403
|
+
[baseModes, isInvalid, isReadOnly, isDisabled, isFocused]
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
const tokens = useFormFieldTokens(modes)
|
|
407
|
+
const itemTextTokens = useDropdownItemTextTokens(modes)
|
|
408
|
+
|
|
409
|
+
// ---------------- Handlers ----------------
|
|
410
|
+
const inputRef = useRef<RNTextInput>(null)
|
|
411
|
+
const blurTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
412
|
+
|
|
413
|
+
const clearBlurTimer = useCallback(() => {
|
|
414
|
+
if (blurTimer.current) {
|
|
415
|
+
clearTimeout(blurTimer.current)
|
|
416
|
+
blurTimer.current = null
|
|
417
|
+
}
|
|
418
|
+
}, [])
|
|
419
|
+
|
|
420
|
+
useEffect(() => () => clearBlurTimer(), [clearBlurTimer])
|
|
421
|
+
|
|
422
|
+
const setQuery = useCallback(
|
|
423
|
+
(text: string) => {
|
|
424
|
+
if (!isControlledInput) setInternalInput(text)
|
|
425
|
+
onInputChange?.(text)
|
|
426
|
+
},
|
|
427
|
+
[isControlledInput, onInputChange]
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
const handleChangeText = useCallback(
|
|
431
|
+
(text: string) => {
|
|
432
|
+
setQuery(text)
|
|
433
|
+
// Typing invalidates a prior selection unless the text still
|
|
434
|
+
// exactly matches the selected option's label.
|
|
435
|
+
if (currentValue != null) {
|
|
436
|
+
const selected = normalizedItems.find(
|
|
437
|
+
(o) => o.value === currentValue
|
|
438
|
+
)
|
|
439
|
+
if (!selected || selected.label !== text) {
|
|
440
|
+
if (!isControlledValue) setInternalValue(null)
|
|
441
|
+
onValueChange?.(null)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (!isControlledOpen) setInternalOpen(true)
|
|
445
|
+
},
|
|
446
|
+
[
|
|
447
|
+
setQuery,
|
|
448
|
+
currentValue,
|
|
449
|
+
normalizedItems,
|
|
450
|
+
isControlledValue,
|
|
451
|
+
onValueChange,
|
|
452
|
+
isControlledOpen,
|
|
453
|
+
]
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
const handleSelect = useCallback(
|
|
457
|
+
(selectedValue: SuggestiveSearchOptionValue | null) => {
|
|
458
|
+
clearBlurTimer()
|
|
459
|
+
const option = normalizedItems.find((o) => o.value === selectedValue)
|
|
460
|
+
if (!option || option.disabled) return
|
|
461
|
+
setQuery(option.label)
|
|
462
|
+
if (!isControlledValue) setInternalValue(option.value)
|
|
463
|
+
onValueChange?.(option.value, option)
|
|
464
|
+
setOpenState(false)
|
|
465
|
+
inputRef.current?.blur()
|
|
466
|
+
},
|
|
467
|
+
[
|
|
468
|
+
clearBlurTimer,
|
|
469
|
+
normalizedItems,
|
|
470
|
+
setQuery,
|
|
471
|
+
isControlledValue,
|
|
472
|
+
onValueChange,
|
|
473
|
+
setOpenState,
|
|
474
|
+
]
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
const handleFocus = useCallback<NonNullable<RNTextInputProps['onFocus']>>(
|
|
478
|
+
(e) => {
|
|
479
|
+
clearBlurTimer()
|
|
480
|
+
setIsFocused(true)
|
|
481
|
+
if (!isControlledOpen) setInternalOpen(true)
|
|
482
|
+
onFocus?.(e)
|
|
483
|
+
},
|
|
484
|
+
[clearBlurTimer, isControlledOpen, onFocus]
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
const handleBlur = useCallback<NonNullable<RNTextInputProps['onBlur']>>(
|
|
488
|
+
(e) => {
|
|
489
|
+
// Delay closing so a suggestion press (which blurs the input first
|
|
490
|
+
// on web) still registers before the list unmounts.
|
|
491
|
+
clearBlurTimer()
|
|
492
|
+
blurTimer.current = setTimeout(() => {
|
|
493
|
+
setIsFocused(false)
|
|
494
|
+
if (!isControlledOpen) setInternalOpen(false)
|
|
495
|
+
}, 120)
|
|
496
|
+
onBlur?.(e)
|
|
497
|
+
},
|
|
498
|
+
[clearBlurTimer, isControlledOpen, onBlur]
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
// ---------------- Web outside-press to close ----------------
|
|
502
|
+
const rootRef = useRef<View>(null)
|
|
503
|
+
useEffect(() => {
|
|
504
|
+
if (!IS_WEB || !isOpen) return
|
|
505
|
+
const handler = (e: any) => {
|
|
506
|
+
const node = rootRef.current as unknown as {
|
|
507
|
+
contains?: (t: EventTarget | null) => boolean
|
|
508
|
+
} | null
|
|
509
|
+
if (node?.contains && !node.contains(e.target)) {
|
|
510
|
+
clearBlurTimer()
|
|
511
|
+
setIsFocused(false)
|
|
512
|
+
if (!isControlledOpen) setInternalOpen(false)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
document.addEventListener('mousedown', handler)
|
|
516
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
517
|
+
}, [isOpen, isControlledOpen, clearBlurTimer])
|
|
518
|
+
|
|
519
|
+
// ---------------- Styles ----------------
|
|
520
|
+
const labelTextStyle: TextStyle = {
|
|
521
|
+
color: tokens.labelColor,
|
|
522
|
+
fontFamily: tokens.labelFontFamily,
|
|
523
|
+
fontSize: tokens.labelFontSize,
|
|
524
|
+
lineHeight: tokens.labelLineHeight,
|
|
525
|
+
fontWeight: tokens.labelFontWeight as TextStyle['fontWeight'],
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const requiredIndicatorStyle: TextStyle = {
|
|
529
|
+
...labelTextStyle,
|
|
530
|
+
color: '#d93d3d',
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const wrapperStyle: ViewStyle = {
|
|
534
|
+
gap: tokens.gap,
|
|
535
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
536
|
+
width: '100%',
|
|
537
|
+
position: 'relative',
|
|
538
|
+
// Keep the dropdown above sibling content when it overflows the field.
|
|
539
|
+
zIndex: isOpen ? 1000 : undefined,
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const inputRowStyle: ViewStyle = {
|
|
543
|
+
flexDirection: 'row',
|
|
544
|
+
alignItems: 'center',
|
|
545
|
+
backgroundColor: tokens.inputBackground,
|
|
546
|
+
borderColor: tokens.inputBorderColor,
|
|
547
|
+
borderWidth: tokens.inputBorderSize,
|
|
548
|
+
borderStyle: 'solid',
|
|
549
|
+
borderRadius: tokens.inputRadius,
|
|
550
|
+
paddingHorizontal: tokens.inputPaddingH,
|
|
551
|
+
paddingVertical: 0,
|
|
552
|
+
gap: tokens.inputGap,
|
|
553
|
+
minHeight: tokens.inputLineHeight,
|
|
554
|
+
width: '100%',
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const inputTextStyles: TextStyle = {
|
|
558
|
+
flex: 1,
|
|
559
|
+
color: tokens.inputTextColor,
|
|
560
|
+
fontFamily: tokens.inputFontFamily,
|
|
561
|
+
fontSize: tokens.inputFontSize,
|
|
562
|
+
lineHeight: tokens.inputLineHeight,
|
|
563
|
+
fontWeight: tokens.inputFontWeight as TextStyle['fontWeight'],
|
|
564
|
+
padding: 0,
|
|
565
|
+
margin: 0,
|
|
566
|
+
...(IS_WEB
|
|
567
|
+
? {
|
|
568
|
+
outlineStyle: 'none' as TextStyle['outlineStyle'],
|
|
569
|
+
outlineWidth: 0,
|
|
570
|
+
outlineColor: 'transparent',
|
|
571
|
+
}
|
|
572
|
+
: {}),
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const placeholderColor = '#888a8d'
|
|
576
|
+
|
|
577
|
+
const itemBaseTextStyle: TextStyle = {
|
|
578
|
+
flex: 1,
|
|
579
|
+
color: itemTextTokens.foreground,
|
|
580
|
+
fontFamily: itemTextTokens.fontFamily,
|
|
581
|
+
fontSize: itemTextTokens.fontSize,
|
|
582
|
+
lineHeight: itemTextTokens.lineHeight,
|
|
583
|
+
fontWeight: '400',
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ---------------- Support text ----------------
|
|
587
|
+
const supportStatus: SupportTextStatus = isInvalid ? 'Error' : 'Neutral'
|
|
588
|
+
const supportLabel = isInvalid && errorMessage ? errorMessage : supportText
|
|
589
|
+
|
|
590
|
+
// ---------------- Accessibility ----------------
|
|
591
|
+
const resolvedA11yLabel =
|
|
592
|
+
accessibilityLabel || label || placeholder || 'Search'
|
|
593
|
+
const a11yProps: AccessibilityProps & { [key: string]: any } = {
|
|
594
|
+
accessibilityRole: IS_WEB ? undefined : 'search',
|
|
595
|
+
accessibilityLabel: resolvedA11yLabel,
|
|
596
|
+
accessibilityState: { disabled: isDisabled, expanded: isOpen },
|
|
597
|
+
}
|
|
598
|
+
if (accessibilityHint) a11yProps.accessibilityHint = accessibilityHint
|
|
599
|
+
|
|
600
|
+
// ---------------- Suggestion highlight ----------------
|
|
601
|
+
const renderHighlighted = useCallback(
|
|
602
|
+
(optLabel: string) => {
|
|
603
|
+
const trimmed = query.trim()
|
|
604
|
+
if (!highlightMatch || trimmed.length === 0) {
|
|
605
|
+
return <Text style={itemBaseTextStyle}>{optLabel}</Text>
|
|
606
|
+
}
|
|
607
|
+
const idx = optLabel.toLowerCase().indexOf(trimmed.toLowerCase())
|
|
608
|
+
if (idx < 0) {
|
|
609
|
+
return <Text style={itemBaseTextStyle}>{optLabel}</Text>
|
|
610
|
+
}
|
|
611
|
+
const before = optLabel.slice(0, idx)
|
|
612
|
+
const match = optLabel.slice(idx, idx + trimmed.length)
|
|
613
|
+
const after = optLabel.slice(idx + trimmed.length)
|
|
614
|
+
return (
|
|
615
|
+
<Text style={itemBaseTextStyle}>
|
|
616
|
+
{before}
|
|
617
|
+
<Text style={{ fontWeight: '700' }}>{match}</Text>
|
|
618
|
+
{after}
|
|
619
|
+
</Text>
|
|
620
|
+
)
|
|
621
|
+
},
|
|
622
|
+
[query, highlightMatch, itemBaseTextStyle]
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
// ---------------- Render ----------------
|
|
626
|
+
return (
|
|
627
|
+
<View
|
|
628
|
+
ref={rootRef}
|
|
629
|
+
style={[wrapperStyle, style]}
|
|
630
|
+
pointerEvents={isDisabled ? 'none' : 'auto'}
|
|
631
|
+
testID={testID}
|
|
632
|
+
>
|
|
633
|
+
{label != null && (
|
|
634
|
+
<View style={styles.labelRow}>
|
|
635
|
+
<Text style={labelTextStyle}>{label}</Text>
|
|
636
|
+
{isRequired && <Text style={requiredIndicatorStyle}> *</Text>}
|
|
637
|
+
</View>
|
|
638
|
+
)}
|
|
639
|
+
|
|
640
|
+
<View style={styles.anchor}>
|
|
641
|
+
<Pressable
|
|
642
|
+
style={[inputRowStyle, inputStyle]}
|
|
643
|
+
onPress={() => inputRef.current?.focus()}
|
|
644
|
+
accessible={false}
|
|
645
|
+
>
|
|
646
|
+
<RNTextInput
|
|
647
|
+
ref={inputRef}
|
|
648
|
+
style={[inputTextStyles, inputTextStyle]}
|
|
649
|
+
value={query}
|
|
650
|
+
onChangeText={handleChangeText}
|
|
651
|
+
onFocus={handleFocus}
|
|
652
|
+
onBlur={handleBlur}
|
|
653
|
+
placeholder={placeholder}
|
|
654
|
+
placeholderTextColor={placeholderColor}
|
|
655
|
+
editable={interactive}
|
|
656
|
+
autoCapitalize="none"
|
|
657
|
+
autoComplete="off"
|
|
658
|
+
autoCorrect={false}
|
|
659
|
+
{...a11yProps}
|
|
660
|
+
{...(IS_WEB
|
|
661
|
+
? {
|
|
662
|
+
accessibilityRole: 'search' as const,
|
|
663
|
+
'aria-autocomplete': 'list' as const,
|
|
664
|
+
'aria-expanded': isOpen,
|
|
665
|
+
}
|
|
666
|
+
: {})}
|
|
667
|
+
/>
|
|
668
|
+
</Pressable>
|
|
669
|
+
|
|
670
|
+
{isOpen && (
|
|
671
|
+
<View
|
|
672
|
+
style={[
|
|
673
|
+
styles.popup,
|
|
674
|
+
{ top: '100%', marginTop: menuOffset },
|
|
675
|
+
]}
|
|
676
|
+
// Keep taps from dismissing the keyboard before the
|
|
677
|
+
// item's press handler runs on native.
|
|
678
|
+
>
|
|
679
|
+
<Dropdown
|
|
680
|
+
modes={modes}
|
|
681
|
+
maxHeight={menuMaxHeight}
|
|
682
|
+
style={menuStyle}
|
|
683
|
+
accessibilityLabel={`${resolvedA11yLabel} suggestions`}
|
|
684
|
+
>
|
|
685
|
+
{hasSuggestions
|
|
686
|
+
? suggestions.map((opt) => {
|
|
687
|
+
const isSelected = opt.value === currentValue
|
|
688
|
+
return (
|
|
689
|
+
<DropdownItem
|
|
690
|
+
key={`sg-${opt.value}`}
|
|
691
|
+
value={opt.value}
|
|
692
|
+
selected={isSelected}
|
|
693
|
+
disabled={opt.disabled ?? false}
|
|
694
|
+
onPress={handleSelect}
|
|
695
|
+
modes={modes}
|
|
696
|
+
>
|
|
697
|
+
{renderItem
|
|
698
|
+
? renderItem(opt, {
|
|
699
|
+
query,
|
|
700
|
+
isSelected,
|
|
701
|
+
})
|
|
702
|
+
: renderHighlighted(opt.label)}
|
|
703
|
+
</DropdownItem>
|
|
704
|
+
)
|
|
705
|
+
})
|
|
706
|
+
: showEmpty && (
|
|
707
|
+
<View style={styles.emptyRow}>
|
|
708
|
+
<Text
|
|
709
|
+
style={[
|
|
710
|
+
itemBaseTextStyle,
|
|
711
|
+
{ color: placeholderColor },
|
|
712
|
+
]}
|
|
713
|
+
>
|
|
714
|
+
{emptyMessage}
|
|
715
|
+
</Text>
|
|
716
|
+
</View>
|
|
717
|
+
)}
|
|
718
|
+
</Dropdown>
|
|
719
|
+
</View>
|
|
720
|
+
)}
|
|
721
|
+
</View>
|
|
722
|
+
|
|
723
|
+
{supportLabel != null && supportLabel !== '' && (
|
|
724
|
+
<SupportText
|
|
725
|
+
label={supportLabel}
|
|
726
|
+
status={supportStatus}
|
|
727
|
+
modes={modes}
|
|
728
|
+
/>
|
|
729
|
+
)}
|
|
730
|
+
</View>
|
|
731
|
+
)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const styles = {
|
|
735
|
+
labelRow: {
|
|
736
|
+
flexDirection: 'row',
|
|
737
|
+
alignItems: 'baseline',
|
|
738
|
+
} as ViewStyle,
|
|
739
|
+
anchor: {
|
|
740
|
+
position: 'relative',
|
|
741
|
+
width: '100%',
|
|
742
|
+
zIndex: 1,
|
|
743
|
+
} as ViewStyle,
|
|
744
|
+
popup: {
|
|
745
|
+
position: 'absolute',
|
|
746
|
+
left: 0,
|
|
747
|
+
right: 0,
|
|
748
|
+
zIndex: 1000,
|
|
749
|
+
} as ViewStyle,
|
|
750
|
+
emptyRow: {
|
|
751
|
+
paddingHorizontal: 12,
|
|
752
|
+
paddingVertical: 12,
|
|
753
|
+
} as ViewStyle,
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export default SuggestiveSearch
|