jfs-components 0.0.77 → 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.
Files changed (70) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +55 -55
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
  4. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  5. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  6. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  7. package/lib/commonjs/components/FormField/FormField.js +14 -1
  8. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
  9. package/lib/commonjs/components/ListItem/ListItem.js +25 -10
  10. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  11. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  12. package/lib/commonjs/components/Stepper/Step.js +47 -60
  13. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  14. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  15. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  16. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  17. package/lib/commonjs/components/Title/Title.js +10 -2
  18. package/lib/commonjs/components/index.js +28 -0
  19. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  20. package/lib/commonjs/icons/registry.js +1 -1
  21. package/lib/module/components/Accordion/Accordion.js +56 -56
  22. package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
  23. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  24. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  25. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  26. package/lib/module/components/FormField/FormField.js +16 -3
  27. package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
  28. package/lib/module/components/ListItem/ListItem.js +25 -10
  29. package/lib/module/components/MessageField/MessageField.js +313 -0
  30. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  31. package/lib/module/components/Stepper/Step.js +48 -61
  32. package/lib/module/components/Stepper/StepLabel.js +40 -10
  33. package/lib/module/components/Stepper/Stepper.js +15 -17
  34. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  35. package/lib/module/components/TextInput/TextInput.js +17 -2
  36. package/lib/module/components/Title/Title.js +10 -2
  37. package/lib/module/components/index.js +4 -0
  38. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  39. package/lib/module/icons/registry.js +1 -1
  40. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  41. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  42. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  43. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  44. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  45. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  46. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  47. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  48. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  49. package/lib/typescript/src/components/index.d.ts +7 -3
  50. package/lib/typescript/src/icons/registry.d.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/components/Accordion/Accordion.tsx +113 -73
  53. package/src/components/ActionFooter/ActionFooter.tsx +56 -4
  54. package/src/components/Checkbox/Checkbox.tsx +22 -9
  55. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  56. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  57. package/src/components/FormField/FormField.tsx +19 -3
  58. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  59. package/src/components/ListItem/ListItem.tsx +21 -10
  60. package/src/components/MessageField/MessageField.tsx +543 -0
  61. package/src/components/NavArrow/NavArrow.tsx +81 -17
  62. package/src/components/Stepper/Step.tsx +52 -51
  63. package/src/components/Stepper/StepLabel.tsx +46 -9
  64. package/src/components/Stepper/Stepper.tsx +20 -15
  65. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  66. package/src/components/TextInput/TextInput.tsx +14 -1
  67. package/src/components/Title/Title.tsx +13 -2
  68. package/src/components/index.ts +7 -3
  69. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  70. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,543 @@
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ Pressable,
6
+ TextInput as RNTextInput,
7
+ type StyleProp,
8
+ type TextInputProps as RNTextInputProps,
9
+ type TextStyle,
10
+ type ViewStyle,
11
+ } from 'react-native'
12
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
13
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
14
+ import { EMPTY_MODES } from '../../utils/react-utils'
15
+ import { useFormContext } from '../Form/Form'
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Visual state of the textarea. Mirrors the `FormField States` collection so
23
+ * MessageField slots into the same theming pipeline as FormField. The state
24
+ * is always derived from props (`isInvalid`, `isDisabled`, `isReadOnly` and
25
+ * focus) and is locked in `modes['FormField States']` — passing that key in
26
+ * `modes` is intentionally ignored to keep interactive behaviour and visual
27
+ * state in sync.
28
+ */
29
+ export type MessageFieldState =
30
+ | 'Idle'
31
+ | 'Active'
32
+ | 'Read Only'
33
+ | 'Error'
34
+ | 'Disabled'
35
+
36
+ export type MessageFieldProps = {
37
+ /** Label rendered above the textarea. */
38
+ label?: string
39
+ /** Placeholder text shown when the textarea is empty. */
40
+ placeholder?: string
41
+ /**
42
+ * Current value of the textarea (controlled). When provided, the consumer
43
+ * is responsible for updating it via `onChangeText`.
44
+ */
45
+ value?: string
46
+ /** Initial value when used uncontrolled. Ignored when `value` is provided. */
47
+ defaultValue?: string
48
+ /** Called whenever the text changes. Fires for both controlled and uncontrolled use. */
49
+ onChangeText?: (text: string) => void
50
+ /**
51
+ * Form field name. When the field is rendered inside a `<Form>`, this is
52
+ * the key used to look up server-side `validationErrors` and to clear
53
+ * the error when the value changes.
54
+ */
55
+ name?: string
56
+ /**
57
+ * Maximum number of characters accepted. Drives the counter when
58
+ * `showCounter` is not explicitly false.
59
+ */
60
+ maxLength?: number
61
+ /**
62
+ * Controls visibility of the character counter.
63
+ * - Default: counter is shown when `maxLength` is provided.
64
+ * - `true`: always show counter (shows `<count>/<maxLength>` when
65
+ * `maxLength` is set, or just `<count>` otherwise).
66
+ * - `false`: never show counter.
67
+ */
68
+ showCounter?: boolean
69
+ /**
70
+ * Number of visible text rows. When provided, overrides the default
71
+ * `messageField/textarea/height` token to derive the textarea height as
72
+ * `rows * lineHeight + 2 * padding`.
73
+ */
74
+ rows?: number
75
+ /** Renders a required indicator (asterisk) next to the label. */
76
+ isRequired?: boolean
77
+ /** Disables interaction and dims the field. */
78
+ isDisabled?: boolean
79
+ /** Marks the field as invalid and resolves to the `Error` state token mode. */
80
+ isInvalid?: boolean
81
+ /** Read-only, non-interactive but not dimmed. */
82
+ isReadOnly?: boolean
83
+ /** Auto-focus the textarea on mount. */
84
+ autoFocus?: boolean
85
+ /** Modes for design token resolution (e.g. `{ 'Color Mode': 'Light' }`). */
86
+ modes?: Record<string, any>
87
+ /** Style overrides for the outermost wrapper. */
88
+ style?: StyleProp<ViewStyle>
89
+ /** Style overrides for the textarea container (border/padding/etc). */
90
+ textareaStyle?: StyleProp<ViewStyle>
91
+ /** Style overrides for the input text. */
92
+ inputStyle?: StyleProp<TextStyle>
93
+ /** Accessibility label. Defaults to `label` or `placeholder`. */
94
+ accessibilityLabel?: string
95
+ /** Accessibility hint. */
96
+ accessibilityHint?: string
97
+ /** Test identifier. */
98
+ testID?: string
99
+ /** Called when the textarea receives focus. */
100
+ onFocus?: RNTextInputProps['onFocus']
101
+ /** Called when the textarea loses focus. */
102
+ onBlur?: RNTextInputProps['onBlur']
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Token helpers
107
+ // ---------------------------------------------------------------------------
108
+
109
+ function toNumber(value: unknown, fallback: number): number {
110
+ if (typeof value === 'number' && Number.isFinite(value)) return value
111
+ if (typeof value === 'string') {
112
+ const parsed = parseFloat(value)
113
+ if (Number.isFinite(parsed)) return parsed
114
+ }
115
+ return fallback
116
+ }
117
+
118
+ function toFontWeight(
119
+ value: unknown,
120
+ fallback: TextStyle['fontWeight'],
121
+ ): TextStyle['fontWeight'] {
122
+ if (typeof value === 'number') return value.toString() as TextStyle['fontWeight']
123
+ if (typeof value === 'string' && value.length > 0) return value as TextStyle['fontWeight']
124
+ return fallback
125
+ }
126
+
127
+ function firstError(error: string | string[] | undefined): string | undefined {
128
+ if (!error) return undefined
129
+ if (Array.isArray(error)) return error[0]
130
+ return error
131
+ }
132
+
133
+ function useMessageFieldTokens(modes: Record<string, any>) {
134
+ return useMemo(() => {
135
+ const wrapperGap = toNumber(getVariableByName('messageField/gap', modes), 8)
136
+
137
+ const labelColor =
138
+ (getVariableByName('messageField/label/foreground', modes) as string) ||
139
+ '#000000'
140
+ const labelFontFamily =
141
+ (getVariableByName('messageField/label/fontFamily', modes) as string) ||
142
+ 'JioType Var'
143
+ const labelFontSize = toNumber(
144
+ getVariableByName('messageField/label/fontSize', modes),
145
+ 14,
146
+ )
147
+ const labelLineHeight = toNumber(
148
+ getVariableByName('messageField/label/lineHeight', modes),
149
+ 17,
150
+ )
151
+ const labelFontWeight = toFontWeight(
152
+ getVariableByName('messageField/label/fontWeight', modes),
153
+ '500',
154
+ )
155
+
156
+ const textareaBackground =
157
+ (getVariableByName('messageField/textarea/background', modes) as string) ||
158
+ '#ffffff'
159
+ const textareaBorderColor =
160
+ (getVariableByName('messageField/textarea/border/color', modes) as string) ||
161
+ '#b5b6b7'
162
+ const textareaBorderSize = toNumber(
163
+ getVariableByName('messageField/textarea/border/size', modes),
164
+ 1.5,
165
+ )
166
+ const textareaRadius = toNumber(
167
+ getVariableByName('messageField/textarea/radius', modes),
168
+ 8,
169
+ )
170
+ const textareaPadding = toNumber(
171
+ getVariableByName('messageField/textarea/padding', modes),
172
+ 12,
173
+ )
174
+ const textareaHeight = toNumber(
175
+ getVariableByName('messageField/textarea/height', modes),
176
+ 108,
177
+ )
178
+ const textareaGap = toNumber(
179
+ getVariableByName('messageField/textarea/gap', modes),
180
+ 0,
181
+ )
182
+
183
+ // `messageField/text/foreground` is the input text color. It also
184
+ // serves as the placeholder color — in mode-aware token sets it
185
+ // resolves to a muted/idle color when empty and shifts darker via
186
+ // the `FormField States` cascade once typed-state tokens land. We
187
+ // never re-route this through another token (e.g. the counter
188
+ // color) because that conflates two semantically distinct tokens.
189
+ const inputTextColor =
190
+ (getVariableByName('messageField/text/foreground', modes) as string) ||
191
+ '#707275'
192
+ const inputFontFamily =
193
+ (getVariableByName('messageField/text/fontFamily', modes) as string) ||
194
+ 'JioType Var'
195
+ const inputFontSize = toNumber(
196
+ getVariableByName('messageField/text/fontSize', modes),
197
+ 16,
198
+ )
199
+ const inputLineHeight = toNumber(
200
+ getVariableByName('messageField/text/lineHeight', modes),
201
+ 21,
202
+ )
203
+ const inputFontWeight = toFontWeight(
204
+ getVariableByName('messageField/text/fontWeight', modes),
205
+ '400',
206
+ )
207
+
208
+ const counterColor =
209
+ (getVariableByName('messageField/maxLength/foreground', modes) as string) ||
210
+ '#24262b'
211
+ const counterFontFamily =
212
+ (getVariableByName('messageField/maxLength/fontFamily', modes) as string) ||
213
+ 'JioType Var'
214
+ const counterFontSize = toNumber(
215
+ getVariableByName('messageField/maxLength/fontSize', modes),
216
+ 14,
217
+ )
218
+ const counterLineHeight = toNumber(
219
+ getVariableByName('messageField/maxLength/lineHeight', modes),
220
+ 18,
221
+ )
222
+ const counterFontWeight = toFontWeight(
223
+ getVariableByName('messageField/maxLength/fontWeight', modes),
224
+ '400',
225
+ )
226
+
227
+ return {
228
+ wrapperGap,
229
+ labelColor,
230
+ labelFontFamily,
231
+ labelFontSize,
232
+ labelLineHeight,
233
+ labelFontWeight,
234
+ textareaBackground,
235
+ textareaBorderColor,
236
+ textareaBorderSize,
237
+ textareaRadius,
238
+ textareaPadding,
239
+ textareaHeight,
240
+ textareaGap,
241
+ inputTextColor,
242
+ inputFontFamily,
243
+ inputFontSize,
244
+ inputLineHeight,
245
+ inputFontWeight,
246
+ counterColor,
247
+ counterFontFamily,
248
+ counterFontSize,
249
+ counterLineHeight,
250
+ counterFontWeight,
251
+ }
252
+ }, [modes])
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Component
257
+ // ---------------------------------------------------------------------------
258
+
259
+ const REQUIRED_INDICATOR_COLOR = '#d93d3d'
260
+
261
+ function MessageField({
262
+ label,
263
+ placeholder,
264
+ value,
265
+ defaultValue,
266
+ onChangeText,
267
+ name,
268
+ maxLength,
269
+ showCounter,
270
+ rows,
271
+ isRequired = false,
272
+ isDisabled = false,
273
+ isInvalid = false,
274
+ isReadOnly = false,
275
+ autoFocus = false,
276
+ modes: propModes = EMPTY_MODES,
277
+ style,
278
+ textareaStyle,
279
+ inputStyle,
280
+ accessibilityLabel,
281
+ accessibilityHint,
282
+ testID,
283
+ onFocus,
284
+ onBlur,
285
+ }: MessageFieldProps) {
286
+ const formCtx = useFormContext()
287
+ const formError =
288
+ name && formCtx ? firstError(formCtx.validationErrors[name]) : undefined
289
+ const resolvedIsInvalid = isInvalid || Boolean(formError)
290
+
291
+ const isControlled = value !== undefined
292
+ const [uncontrolledValue, setUncontrolledValue] = useState<string>(
293
+ defaultValue ?? '',
294
+ )
295
+ const currentValue = isControlled ? (value as string) : uncontrolledValue
296
+
297
+ const [isFocused, setIsFocused] = useState(false)
298
+ const interactive = !isDisabled && !isReadOnly
299
+
300
+ // Ref to the native textarea so tapping anywhere in the (padded) textarea
301
+ // container focuses it on the FIRST tap, fixing the Android "two taps to
302
+ // open the keyboard" issue.
303
+ const inputRef = useRef<RNTextInput>(null)
304
+ const focusInput = useCallback(() => {
305
+ if (!interactive) return
306
+ inputRef.current?.focus()
307
+ }, [interactive])
308
+
309
+ const { modes: globalModes } = useTokens()
310
+ const baseModes = useMemo(
311
+ () => ({ ...globalModes, ...propModes }),
312
+ [globalModes, propModes],
313
+ )
314
+
315
+ // FormField States cascade — error > disabled > read only > active (focus)
316
+ // > idle. Always derived from props and locked into the modes object so
317
+ // consumers cannot pass `modes={{ 'FormField States': ... }}` and get out
318
+ // of sync with the component's actual interactive behaviour.
319
+ const stateMode: MessageFieldState = useMemo(() => {
320
+ if (resolvedIsInvalid) return 'Error'
321
+ if (isDisabled) return 'Disabled'
322
+ if (isReadOnly) return 'Read Only'
323
+ if (isFocused) return 'Active'
324
+ return 'Idle'
325
+ }, [resolvedIsInvalid, isDisabled, isReadOnly, isFocused])
326
+
327
+ const modes = useMemo(
328
+ () => ({
329
+ ...baseModes,
330
+ 'FormField States': stateMode,
331
+ }),
332
+ [baseModes, stateMode],
333
+ )
334
+
335
+ const tokens = useMessageFieldTokens(modes)
336
+
337
+ // ---------- Event handlers ---------------------------------------------
338
+ const handleFocus = useCallback<NonNullable<RNTextInputProps['onFocus']>>(
339
+ (e) => {
340
+ setIsFocused(true)
341
+ onFocus?.(e)
342
+ },
343
+ [onFocus],
344
+ )
345
+
346
+ const handleBlur = useCallback<NonNullable<RNTextInputProps['onBlur']>>(
347
+ (e) => {
348
+ setIsFocused(false)
349
+ onBlur?.(e)
350
+ },
351
+ [onBlur],
352
+ )
353
+
354
+ const handleChangeText = useCallback(
355
+ (next: string) => {
356
+ if (!isControlled) {
357
+ setUncontrolledValue(next)
358
+ }
359
+ onChangeText?.(next)
360
+ if (name && formCtx) formCtx.onFieldChange(name)
361
+ },
362
+ [isControlled, onChangeText, name, formCtx],
363
+ )
364
+
365
+ // ---------- Derived layout values --------------------------------------
366
+ const computedHeight = useMemo(() => {
367
+ if (rows && rows > 0) {
368
+ return Math.round(
369
+ rows * tokens.inputLineHeight + 2 * tokens.textareaPadding,
370
+ )
371
+ }
372
+ return tokens.textareaHeight
373
+ }, [rows, tokens.inputLineHeight, tokens.textareaPadding, tokens.textareaHeight])
374
+
375
+ const shouldShowCounter = useMemo(() => {
376
+ if (showCounter === false) return false
377
+ if (showCounter === true) return true
378
+ return typeof maxLength === 'number'
379
+ }, [showCounter, maxLength])
380
+
381
+ const counterText = useMemo(() => {
382
+ const count = currentValue.length
383
+ if (typeof maxLength === 'number') return `${count}/${maxLength}`
384
+ return `${count}`
385
+ }, [currentValue.length, maxLength])
386
+
387
+ // ---------- Styles -----------------------------------------------------
388
+ const wrapperStyle: ViewStyle = useMemo(
389
+ () => ({
390
+ gap: tokens.wrapperGap,
391
+ width: '100%',
392
+ }),
393
+ [tokens.wrapperGap],
394
+ )
395
+
396
+ const labelRowStyle: ViewStyle = useMemo(
397
+ () => ({ flexDirection: 'row', alignItems: 'baseline' }),
398
+ [],
399
+ )
400
+
401
+ const labelTextStyle: TextStyle = useMemo(
402
+ () => ({
403
+ color: tokens.labelColor,
404
+ fontFamily: tokens.labelFontFamily,
405
+ fontSize: tokens.labelFontSize,
406
+ lineHeight: tokens.labelLineHeight,
407
+ fontWeight: tokens.labelFontWeight,
408
+ }),
409
+ [
410
+ tokens.labelColor,
411
+ tokens.labelFontFamily,
412
+ tokens.labelFontSize,
413
+ tokens.labelLineHeight,
414
+ tokens.labelFontWeight,
415
+ ],
416
+ )
417
+
418
+ const requiredIndicatorStyle: TextStyle = useMemo(
419
+ () => ({ ...labelTextStyle, color: REQUIRED_INDICATOR_COLOR }),
420
+ [labelTextStyle],
421
+ )
422
+
423
+ const textareaContainerStyle: ViewStyle = useMemo(
424
+ () => ({
425
+ backgroundColor: tokens.textareaBackground,
426
+ borderColor: tokens.textareaBorderColor,
427
+ borderWidth: tokens.textareaBorderSize,
428
+ borderStyle: 'solid',
429
+ borderRadius: tokens.textareaRadius,
430
+ padding: tokens.textareaPadding,
431
+ height: computedHeight,
432
+ width: '100%',
433
+ overflow: 'hidden',
434
+ // The gap token is for content within the textarea (icons, etc.);
435
+ // we keep it so downstream layouts that pass children align.
436
+ gap: tokens.textareaGap,
437
+ }),
438
+ [
439
+ tokens.textareaBackground,
440
+ tokens.textareaBorderColor,
441
+ tokens.textareaBorderSize,
442
+ tokens.textareaRadius,
443
+ tokens.textareaPadding,
444
+ computedHeight,
445
+ tokens.textareaGap,
446
+ ],
447
+ )
448
+
449
+ const inputTextStyle: TextStyle = useMemo(
450
+ () => ({
451
+ flex: 1,
452
+ color: tokens.inputTextColor,
453
+ fontFamily: tokens.inputFontFamily,
454
+ fontSize: tokens.inputFontSize,
455
+ lineHeight: tokens.inputLineHeight,
456
+ fontWeight: tokens.inputFontWeight,
457
+ padding: 0,
458
+ margin: 0,
459
+ textAlignVertical: 'top',
460
+ // Disable the default web focus ring; the textarea border
461
+ // already encodes focus state.
462
+ outlineStyle: 'none' as TextStyle['outlineStyle'],
463
+ outlineWidth: 0,
464
+ outlineColor: 'transparent',
465
+ }),
466
+ [
467
+ tokens.inputTextColor,
468
+ tokens.inputFontFamily,
469
+ tokens.inputFontSize,
470
+ tokens.inputLineHeight,
471
+ tokens.inputFontWeight,
472
+ ],
473
+ )
474
+
475
+ const counterTextStyle: TextStyle = useMemo(
476
+ () => ({
477
+ color: tokens.counterColor,
478
+ fontFamily: tokens.counterFontFamily,
479
+ fontSize: tokens.counterFontSize,
480
+ lineHeight: tokens.counterLineHeight,
481
+ fontWeight: tokens.counterFontWeight,
482
+ textAlign: 'right',
483
+ width: '100%',
484
+ }),
485
+ [
486
+ tokens.counterColor,
487
+ tokens.counterFontFamily,
488
+ tokens.counterFontSize,
489
+ tokens.counterLineHeight,
490
+ tokens.counterFontWeight,
491
+ ],
492
+ )
493
+
494
+ const resolvedA11yLabel =
495
+ accessibilityLabel || label || placeholder || 'Message field'
496
+
497
+ return (
498
+ <View
499
+ style={[wrapperStyle, style]}
500
+ pointerEvents={isDisabled ? 'none' : 'auto'}
501
+ testID={testID}
502
+ accessible={false}
503
+ >
504
+ {label != null && label !== '' && (
505
+ <View style={labelRowStyle}>
506
+ <Text style={labelTextStyle}>{label}</Text>
507
+ {isRequired && <Text style={requiredIndicatorStyle}> *</Text>}
508
+ </View>
509
+ )}
510
+
511
+ <Pressable
512
+ style={[textareaContainerStyle, textareaStyle]}
513
+ onPress={focusInput}
514
+ accessible={false}
515
+ >
516
+ <RNTextInput
517
+ ref={inputRef}
518
+ multiline
519
+ value={currentValue}
520
+ onChangeText={handleChangeText}
521
+ onFocus={handleFocus}
522
+ onBlur={handleBlur}
523
+ placeholder={placeholder ?? ''}
524
+ placeholderTextColor={tokens.inputTextColor}
525
+ editable={interactive}
526
+ maxLength={maxLength}
527
+ autoFocus={autoFocus}
528
+ accessibilityLabel={resolvedA11yLabel}
529
+ accessibilityHint={accessibilityHint}
530
+ style={[inputTextStyle, inputStyle]}
531
+ />
532
+ </Pressable>
533
+
534
+ {shouldShowCounter && (
535
+ <Text style={counterTextStyle} accessibilityElementsHidden>
536
+ {counterText}
537
+ </Text>
538
+ )}
539
+ </View>
540
+ )
541
+ }
542
+
543
+ export default MessageField
@@ -1,21 +1,48 @@
1
1
  import React, { useMemo } from 'react'
2
- import { View, type ViewStyle } from 'react-native'
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ View,
6
+ type PressableStateCallbackType,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native'
3
10
  import Svg, { Polyline } from 'react-native-svg'
4
11
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
12
+ import {
13
+ usePressableWebSupport,
14
+ type SafePressableProps,
15
+ } from '../../utils/web-platform-utils'
5
16
  import { EMPTY_MODES } from '../../utils/react-utils'
6
17
 
7
18
  type NavArrowDirection = 'Back' | 'Forward' | 'Down'
8
19
 
9
- type NavArrowProps = {
20
+ /** Minimum touch target per iOS HIG / Material accessibility guidance. */
21
+ const MIN_TOUCH_TARGET = 44
22
+ const IS_IOS = Platform.OS === 'ios'
23
+ const PRESS_DELAY = IS_IOS ? 130 : 0
24
+
25
+ const touchTargetStyle: ViewStyle = {
26
+ minWidth: MIN_TOUCH_TARGET,
27
+ minHeight: MIN_TOUCH_TARGET,
28
+ alignItems: 'center',
29
+ justifyContent: 'center',
30
+ }
31
+
32
+ type NavArrowProps = SafePressableProps & {
10
33
  /** Direction of the arrow: 'Back' (left chevron), 'Forward' (right chevron), or 'Down' */
11
34
  direction?: NavArrowDirection
12
35
  /** Modes used to resolve design tokens */
13
36
  modes?: Record<string, any>
14
37
  /** Optional additional container style */
15
- style?: ViewStyle
38
+ style?: StyleProp<ViewStyle>
16
39
  /** Accessibility label for the arrow */
17
40
  accessibilityLabel?: string
18
- } & Omit<React.ComponentProps<typeof View>, 'style' | 'accessibilityLabel'>
41
+ /** Called when the arrow is pressed. Expands the hit area to at least 44×44. */
42
+ onPress?: () => void
43
+ /** Disables press interaction when `onPress` is provided */
44
+ disabled?: boolean
45
+ }
19
46
 
20
47
  interface NavArrowTokens {
21
48
  iconColor: string
@@ -75,6 +102,8 @@ function NavArrow({
75
102
  modes = EMPTY_MODES,
76
103
  style,
77
104
  accessibilityLabel,
105
+ onPress,
106
+ disabled = false,
78
107
  ...rest
79
108
  }: NavArrowProps) {
80
109
  const tokens = useMemo(() => resolveNavArrowTokens(modes), [modes])
@@ -91,7 +120,6 @@ function NavArrow({
91
120
  backgroundColor: tokens.backgroundColor,
92
121
  alignItems: 'center',
93
122
  justifyContent: 'center',
94
- ...(style || {}),
95
123
  }
96
124
 
97
125
  const chevronW = isDown ? tokens.iconHeight : tokens.iconWidth
@@ -116,7 +144,7 @@ function NavArrow({
116
144
  }
117
145
 
118
146
  return { containerStyle, svgWidth, svgHeight, points }
119
- }, [tokens, direction, style])
147
+ }, [tokens, direction])
120
148
 
121
149
  const defaultAccessibilityLabel =
122
150
  accessibilityLabel ||
@@ -126,23 +154,59 @@ function NavArrow({
126
154
  ? 'Go forward'
127
155
  : 'Go down')
128
156
 
157
+ const webProps = usePressableWebSupport({
158
+ restProps: rest,
159
+ onPress,
160
+ disabled,
161
+ accessibilityLabel: defaultAccessibilityLabel,
162
+ })
163
+
164
+ const chevron = (
165
+ <Svg
166
+ width={computed.svgWidth}
167
+ height={computed.svgHeight}
168
+ viewBox={`0 0 ${computed.svgWidth} ${computed.svgHeight}`}
169
+ >
170
+ <Polyline
171
+ points={computed.points}
172
+ stroke={tokens.iconColor}
173
+ strokeWidth={tokens.strokeWeight}
174
+ strokeLinecap="round"
175
+ strokeLinejoin="round"
176
+ fill="none"
177
+ />
178
+ </Svg>
179
+ )
180
+
181
+ if (onPress) {
182
+ return (
183
+ <Pressable
184
+ onPress={onPress}
185
+ disabled={disabled}
186
+ accessibilityRole="button"
187
+ accessibilityLabel={defaultAccessibilityLabel}
188
+ accessibilityState={{ disabled }}
189
+ unstable_pressDelay={PRESS_DELAY}
190
+ style={({ pressed }: PressableStateCallbackType) => [
191
+ touchTargetStyle,
192
+ style,
193
+ pressed && !disabled ? { opacity: 0.7 } : null,
194
+ ]}
195
+ {...webProps}
196
+ >
197
+ <View style={computed.containerStyle}>{chevron}</View>
198
+ </Pressable>
199
+ )
200
+ }
201
+
129
202
  return (
130
203
  <View
131
- style={computed.containerStyle}
204
+ style={[computed.containerStyle, style]}
132
205
  accessibilityRole="image"
133
206
  accessibilityLabel={defaultAccessibilityLabel}
134
207
  {...rest}
135
208
  >
136
- <Svg width={computed.svgWidth} height={computed.svgHeight} viewBox={`0 0 ${computed.svgWidth} ${computed.svgHeight}`}>
137
- <Polyline
138
- points={computed.points}
139
- stroke={tokens.iconColor}
140
- strokeWidth={tokens.strokeWeight}
141
- strokeLinecap="round"
142
- strokeLinejoin="round"
143
- fill="none"
144
- />
145
- </Svg>
209
+ {chevron}
146
210
  </View>
147
211
  )
148
212
  }